147 lines
5.4 KiB
Rust
147 lines
5.4 KiB
Rust
|
|
//! Criterion benchmark: `below_cache` reuse across consecutive strokes.
|
|||
|
|
//!
|
|||
|
|
//! ## Purpose
|
|||
|
|
//! Verifies that painting multiple strokes on the **same layer** reuses the
|
|||
|
|
//! pre-built `below_cache` (composite of all layers below the active one)
|
|||
|
|
//! instead of rebuilding it from scratch on every `begin_stroke()`.
|
|||
|
|
//!
|
|||
|
|
//! On a 4K 10-layer document the below-layer composite costs ~15–25 ms.
|
|||
|
|
//! If `below_cache_dirty` is incorrectly set to `true` after every
|
|||
|
|
//! `end_stroke()`, each subsequent stroke pays that cost again.
|
|||
|
|
//!
|
|||
|
|
//! ## Logic & Workflow
|
|||
|
|
//! 1. Creates a 3840×2160, 10-layer document (all filled).
|
|||
|
|
//! 2. **first_stroke**: Benchmarks a full stroke including below_cache build.
|
|||
|
|
//! 3. **second_stroke_same_layer**: Benchmarks a stroke on the same layer
|
|||
|
|
//! where `below_cache` should be reused (much cheaper `begin_stroke`).
|
|||
|
|
//!
|
|||
|
|
//! When the cache works correctly, the second stroke's `begin_stroke()` is
|
|||
|
|
//! essentially free (just a dirty-flag check), while the first one must
|
|||
|
|
//! composite 9 below-layers into a 33 MB buffer.
|
|||
|
|
//!
|
|||
|
|
//! ## Arguments & Returns
|
|||
|
|
//! Standard criterion benchmark — run with:
|
|||
|
|
//! ```bash
|
|||
|
|
//! cargo bench -p hcie-engine-api --bench below_cache_reuse
|
|||
|
|
//! ```
|
|||
|
|
//!
|
|||
|
|
//! ## Side Effects / Dependencies
|
|||
|
|
//! Allocates ~400 MB (10 layers × 4K RGBA + below_cache + composite_scratch).
|
|||
|
|
|
|||
|
|
use criterion::{criterion_group, criterion_main, Criterion};
|
|||
|
|
use hcie_engine_api::{BrushStyle, BrushTip, Engine};
|
|||
|
|
|
|||
|
|
/// Canvas dimensions — 4K UHD.
|
|||
|
|
const W: u32 = 3840;
|
|||
|
|
const H: u32 = 2160;
|
|||
|
|
/// Number of raster layers.
|
|||
|
|
const LAYERS: usize = 10;
|
|||
|
|
|
|||
|
|
/// Builds a 10-layer 4K document. The top layer is the active drawing target.
|
|||
|
|
///
|
|||
|
|
/// **Purpose:** Creates a worst-case scenario where the below-layer composite
|
|||
|
|
/// is expensive (9 filled layers below the active one).
|
|||
|
|
/// **Returns:** `(Engine, top_layer_id)`.
|
|||
|
|
fn setup_10layer_engine() -> (Engine, u64) {
|
|||
|
|
let mut engine = Engine::new(W, H);
|
|||
|
|
|
|||
|
|
let bg_id = engine.active_layer_id();
|
|||
|
|
engine.set_active_layer(bg_id);
|
|||
|
|
engine.draw_filled_rect_rgba(0, 0, W, H, [240, 240, 240, 255]);
|
|||
|
|
|
|||
|
|
let mut layer_ids = vec![bg_id];
|
|||
|
|
for i in 1..LAYERS {
|
|||
|
|
let id = engine.add_layer(&format!("Layer {}", i));
|
|||
|
|
engine.set_active_layer(id);
|
|||
|
|
let color = match i % 4 {
|
|||
|
|
0 => [180, 80, 80, 255],
|
|||
|
|
1 => [80, 180, 80, 255],
|
|||
|
|
2 => [80, 80, 180, 255],
|
|||
|
|
_ => [160, 140, 100, 255],
|
|||
|
|
};
|
|||
|
|
engine.draw_filled_rect_rgba(0, 0, W, H, color);
|
|||
|
|
layer_ids.push(id);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let top_id = *layer_ids.last().unwrap();
|
|||
|
|
engine.set_active_layer(top_id);
|
|||
|
|
|
|||
|
|
let mut tip = BrushTip::default();
|
|||
|
|
tip.style = BrushStyle::Round;
|
|||
|
|
tip.size = 24.0;
|
|||
|
|
tip.opacity = 1.0;
|
|||
|
|
tip.hardness = 0.85;
|
|||
|
|
tip.spacing = 0.1;
|
|||
|
|
engine.set_brush_tip(tip);
|
|||
|
|
engine.set_color([220, 60, 60, 255]);
|
|||
|
|
engine.set_eraser(false);
|
|||
|
|
|
|||
|
|
(engine, top_id)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Performs a short stroke: begin → 5 segments → composite → end.
|
|||
|
|
///
|
|||
|
|
/// **Purpose:** Exercises the full `begin_stroke()` (below_cache build/reuse)
|
|||
|
|
/// through `end_stroke()` lifecycle.
|
|||
|
|
/// **Arguments:** `engine` — mutable engine, `layer_id` — target layer,
|
|||
|
|
/// `offset` — slight position offset to avoid degenerate repeat painting.
|
|||
|
|
/// **Side Effects:** Mutates layer pixels, triggers composite, commits history.
|
|||
|
|
fn do_stroke(engine: &mut Engine, layer_id: u64, offset: f32) {
|
|||
|
|
let cx = W as f32 / 2.0 + offset;
|
|||
|
|
let cy = H as f32 / 2.0 + offset;
|
|||
|
|
engine.begin_stroke(layer_id, cx, cy);
|
|||
|
|
for i in 1..=5 {
|
|||
|
|
let d = i as f32 * 8.0;
|
|||
|
|
engine.stroke_to(layer_id, cx + d, cy + d, 0.8);
|
|||
|
|
}
|
|||
|
|
let (_region, ptr, _size) = engine.render_composite_region();
|
|||
|
|
assert!(!ptr.is_null());
|
|||
|
|
engine.end_stroke(layer_id);
|
|||
|
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
|||
|
|
engine.commit_pending_history();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Benchmark group: below_cache build (first stroke) vs reuse (second stroke).
|
|||
|
|
///
|
|||
|
|
/// **Purpose:** Detects regressions where `below_cache_dirty` is incorrectly
|
|||
|
|
/// set to `true` in `end_stroke()` or where `below_cache` is dropped to `None`,
|
|||
|
|
/// forcing a full 9-layer re-composite on every stroke.
|
|||
|
|
///
|
|||
|
|
/// **Logic & Workflow:**
|
|||
|
|
/// - `first_stroke_cache_build`: On a fresh engine, `begin_stroke()` must
|
|||
|
|
/// build the below_cache from scratch (composite 9 layers → 33 MB buffer).
|
|||
|
|
/// - `second_stroke_cache_reuse`: After the first stroke on the same layer,
|
|||
|
|
/// `begin_stroke()` should find `below_cache_dirty == false` and
|
|||
|
|
/// `below_cache_active_idx == current`, skipping the entire composite.
|
|||
|
|
fn bench_below_cache_reuse(c: &mut Criterion) {
|
|||
|
|
let mut group = c.benchmark_group("below_cache_reuse");
|
|||
|
|
|
|||
|
|
// ── First stroke: cache must be built from scratch ──
|
|||
|
|
group.bench_function("first_stroke_cache_build", |b| {
|
|||
|
|
b.iter_with_setup(
|
|||
|
|
|| setup_10layer_engine(),
|
|||
|
|
|(mut engine, layer_id)| {
|
|||
|
|
do_stroke(&mut engine, layer_id, 0.0);
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ── Second stroke on same layer: cache should be reused ──
|
|||
|
|
group.bench_function("second_stroke_cache_reuse", |b| {
|
|||
|
|
let (mut engine, layer_id) = setup_10layer_engine();
|
|||
|
|
// Build the cache with the first stroke
|
|||
|
|
do_stroke(&mut engine, layer_id, 0.0);
|
|||
|
|
|
|||
|
|
let mut offset = 0.0_f32;
|
|||
|
|
b.iter(|| {
|
|||
|
|
offset += 5.0;
|
|||
|
|
do_stroke(&mut engine, layer_id, offset);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
group.finish();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
criterion_group!(benches, bench_below_cache_reuse);
|
|||
|
|
criterion_main!(benches);
|