2026-07-24 17:29:33 +03:00
|
|
|
|
//! 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<Vec<u8>>` 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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 05:20:12 +03:00
|
|
|
|
criterion_group!(benches, bench_composite_scratch_pooling);
|
2026-07-24 17:29:33 +03:00
|
|
|
|
criterion_main!(benches);
|