//! 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);