//! Criterion benchmark: `composite_scratch` buffer pooling guard. //! //! ## Purpose //! Verifies that the ~33 MB `composite_scratch` buffer inside //! `render_composite_region()` is allocated once and reused across calls. //! If a regression replaces the pooled `Option>` with a fresh //! `Vec::new()` on every call, this benchmark detects the allocation storm. //! //! ## Logic & Workflow //! 1. Creates a 3840×2160, 10-layer document with all layers filled. //! 2. Performs a stroke + composite to warm the scratch buffer. //! 3. **cold_composite**: Measures a `render_composite_region()` call on a //! fresh engine where `composite_scratch` is `None`. //! 4. **warm_composite**: Measures the same call after the buffer has been //! allocated (should be faster — no allocation, just memset + composite). //! //! ## Arguments & Returns //! Standard criterion benchmark — run with: //! ```bash //! cargo bench -p hcie-engine-api --bench composite_scratch_pooling //! ``` //! //! ## Side Effects / Dependencies //! Allocates ~400 MB (10 layers × 4K RGBA + scratch + tile caches). 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 to create (worst-case workload). const LAYERS: usize = 10; /// Builds a multi-layer 4K document ready for compositing. /// /// **Purpose:** Creates the worst-case composite scenario with every layer /// filled so tile and pixel compositing operate on real data. /// **Returns:** `(Engine, top_layer_id)`. fn setup_multilayer_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) } /// Forces a dirty region and composites it. /// /// **Purpose:** Exercises the `render_composite_region()` path including /// scratch buffer allocation/reuse, tile compositing, and dirty rect handling. /// **Arguments:** `engine` — mutable engine, `layer_id` — target layer. /// **Side Effects:** Paints a short stroke segment to create a dirty region, /// then calls `render_composite_region()`. fn stroke_and_composite(engine: &mut Engine, layer_id: u64) { let cx = W as f32 / 2.0; let cy = H as f32 / 2.0; engine.begin_stroke(layer_id, cx, cy); engine.stroke_to(layer_id, cx + 50.0, cy + 50.0, 0.8); let (region, ptr, _size) = engine.render_composite_region(); assert!(!ptr.is_null()); let _ = region; engine.end_stroke(layer_id); std::thread::sleep(std::time::Duration::from_millis(10)); engine.commit_pending_history(); } /// Benchmark group: cold vs warm `composite_scratch` allocation. /// /// **Purpose:** Detects regressions where `composite_scratch` is dropped or /// reallocated on every `render_composite_region()` call. When pooling works, /// the warm benchmark avoids a ~33 MB allocation and should be measurably faster. /// /// **Logic & Workflow:** /// - `cold_composite`: Fresh engine, `composite_scratch = None`. First call /// allocates the buffer. /// - `warm_composite`: Engine with pre-allocated scratch buffer. Measures /// pure composite cost without allocation overhead. fn bench_composite_scratch_pooling(c: &mut Criterion) { let mut group = c.benchmark_group("composite_scratch_pooling"); // ── Cold: composite on a fresh engine (scratch allocation included) ── group.bench_function("cold_composite", |b| { b.iter_with_setup( || setup_multilayer_engine(), |(mut engine, layer_id)| { stroke_and_composite(&mut engine, layer_id); }, ); }); // ── Warm: composite on a pre-warmed engine (scratch already pooled) ── group.bench_function("warm_composite", |b| { let (mut engine, layer_id) = setup_multilayer_engine(); // Warm up: allocate the scratch buffer stroke_and_composite(&mut engine, layer_id); b.iter(|| { // Paint a new segment and composite — scratch should be reused let cx = W as f32 / 2.0; let cy = H as f32 / 2.0; engine.begin_stroke(layer_id, cx, cy); engine.stroke_to(layer_id, cx + 30.0, cy - 30.0, 0.7); let (region, ptr, _size) = engine.render_composite_region(); assert!(!ptr.is_null()); let _ = region; engine.end_stroke(layer_id); std::thread::sleep(std::time::Duration::from_millis(5)); engine.commit_pending_history(); }); }); group.finish(); } criterion_group!(benches, bench_composite_scratch_pooling); criterion_main!(benches);