//! Criterion benchmark: conditional `effects_dirty` skip guard. //! //! ## Purpose //! Verifies that `render_composite_region()` / `apply_effects_and_sync_tiles()` //! skips the expensive effects pipeline for layers that have **no effects or //! styles**. If a regression removes the `effects.is_empty() && styles.is_empty()` //! early-exit or unconditionally sets `effects_dirty = true`, composite cost //! on effect-free documents will increase dramatically. //! //! ## Logic & Workflow //! 1. Creates a 3840×2160, 10-layer document (all filled, no effects). //! 2. **no_effects**: Benchmarks `render_composite_region()` where every layer //! has empty `effects` and `styles` vectors → the effects pipeline is //! completely skipped. //! 3. **with_one_effect**: Same document but one layer has a DropShadow style //! → the effects pipeline runs for that single layer. //! //! When the conditional skip works, `no_effects` should be measurably faster //! than `with_one_effect` because no `apply_layer_effects` calls are made. //! //! ## Arguments & Returns //! Standard criterion benchmark — run with: //! ```bash //! cargo bench -p hcie-engine-api --bench effects_skip //! ``` //! //! ## Side Effects / Dependencies //! Allocates ~400 MB (10 layers × 4K RGBA + composite buffers). use criterion::{criterion_group, criterion_main, Criterion}; use hcie_engine_api::{BrushStyle, BrushTip, Engine, LayerStyle}; /// 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 with NO effects on any layer. /// /// **Purpose:** Creates the baseline scenario where the effects pipeline /// should be completely skipped during compositing. /// **Returns:** `(Engine, top_layer_id, all_layer_ids)`. fn setup_no_effects_engine() -> (Engine, u64, Vec) { 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, layer_ids) } /// Performs a stroke + composite cycle and returns. /// /// **Purpose:** Creates a dirty region and composites it, exercising the /// effects pipeline path. /// **Arguments:** `engine` — mutable engine, `layer_id` — target layer, /// `offset` — position variation. /// **Side Effects:** Mutates layer pixels, triggers composite. fn stroke_and_composite(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: effects-free composite vs single-effect composite. /// /// **Purpose:** Detects regressions where the effects pipeline runs /// unnecessarily on layers with no effects, causing wasted CPU cycles /// (apply_layer_effects + 33 MB buffer clone per affected layer). /// /// **Logic & Workflow:** /// - `no_effects_composite`: All 10 layers have empty effects/styles. /// `apply_effects_and_sync_tiles()` should skip the effects pass entirely. /// - `with_one_dropshadow`: Layer 5 has a DropShadow style enabled. /// The effects pipeline runs for that single layer but skips the other 9. /// This should be slightly slower than `no_effects_composite` but not 10× /// slower (which would indicate the skip logic is broken). fn bench_effects_skip(c: &mut Criterion) { let mut group = c.benchmark_group("effects_skip"); // ── No effects on any layer ── group.bench_function("no_effects_composite", |b| { let (mut engine, layer_id, _ids) = setup_no_effects_engine(); // Warm up composite scratch stroke_and_composite(&mut engine, layer_id, 0.0); let mut offset = 0.0_f32; b.iter(|| { offset += 3.0; stroke_and_composite(&mut engine, layer_id, offset); }); }); // ── One layer has a DropShadow effect ── group.bench_function("with_one_dropshadow", |b| { let (mut engine, layer_id, layer_ids) = setup_no_effects_engine(); // Add a DropShadow style to layer 5 (middle of the stack) let target_layer_id = layer_ids[5]; engine.update_layer_style( target_layer_id, LayerStyle::DropShadow { enabled: true, opacity: 0.6, angle: 135.0, distance: 5.0, spread: 0.0, size: 10.0, color: [0, 0, 0, 255], blend_mode: hcie_engine_api::BlendMode::Normal, }, ); // Warm up stroke_and_composite(&mut engine, layer_id, 0.0); let mut offset = 0.0_f32; b.iter(|| { offset += 3.0; stroke_and_composite(&mut engine, layer_id, offset); }); }); group.finish(); } criterion_group!(benches, bench_effects_skip); criterion_main!(benches);