2026-07-09 02:59:53 +03:00
|
|
|
//! 4K multi-layer brush-stroke performance benchmark.
|
|
|
|
|
//!
|
|
|
|
|
//! ## Purpose
|
|
|
|
|
//! Measures the per-segment cost of painting on a large (3840x2160), multi-layer
|
|
|
|
|
//! document using the public `Engine` API. This benchmark is intentionally
|
|
|
|
|
//! **non-gating** — it only prints timing information so engineers can compare
|
|
|
|
|
//! before/after optimizations manually. It must never break the deterministic
|
|
|
|
|
//! golden visual regression suite.
|
|
|
|
|
//!
|
|
|
|
|
//! ## Logic & Workflow
|
|
|
|
|
//! 1. Creates a 3840x2160 document.
|
|
|
|
|
//! 2. Fills 10 raster layers with semi-random opaque color so that every layer
|
|
|
|
|
//! contributes to the composite (worst-case workload for the painter).
|
|
|
|
|
//! 3. Selects the top layer and begins a brush stroke.
|
|
|
|
|
//! 4. Issues 100 `stroke_to` calls across the canvas, calling
|
|
|
|
|
//! `render_composite_region` after every segment to mimic the GUI render loop.
|
|
|
|
|
//! 5. Ends the stroke and prints the average time per segment, per composite,
|
|
|
|
|
//! and the total wall-clock time.
|
|
|
|
|
//!
|
|
|
|
|
//! ## Arguments & Returns
|
|
|
|
|
//! This is a standard Rust test: `cargo test -p hcie-engine-api --test performance_stroke_4k -- --nocapture`.
|
|
|
|
|
//! It returns nothing and asserts only that the engine remains in a valid state.
|
|
|
|
|
//!
|
|
|
|
|
//! ## Side Effects / Dependencies
|
|
|
|
|
//! - Allocates ~400MB of pixel/tile/mask buffers.
|
|
|
|
|
//! - Uses `std::time::Instant` for measurement; results depend on the host CPU.
|
|
|
|
|
|
2026-07-16 22:10:22 +03:00
|
|
|
use hcie_engine_api::{BrushStyle, BrushTip, Engine};
|
2026-07-09 02:59:53 +03:00
|
|
|
|
|
|
|
|
const W: u32 = 3840;
|
|
|
|
|
const H: u32 = 2160;
|
|
|
|
|
const LAYERS: usize = 10;
|
|
|
|
|
const SEGMENTS: usize = 100;
|
|
|
|
|
const COMPOSITE_EVERY_N_SEGMENTS: usize = 1;
|
|
|
|
|
|
|
|
|
|
fn make_solid_color(r: u8, g: u8, b: u8) -> [u8; 4] {
|
|
|
|
|
[r, g, b, 255]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn benchmark_4k_stroke_on_multilayer_document() {
|
|
|
|
|
let mut engine = Engine::new(W, H);
|
|
|
|
|
|
|
|
|
|
// Fill the default background layer.
|
|
|
|
|
let bg_id = engine.active_layer_id();
|
|
|
|
|
engine.set_active_layer(bg_id);
|
|
|
|
|
engine.draw_filled_rect_rgba(0, 0, W, H, make_solid_color(240, 240, 240));
|
|
|
|
|
|
|
|
|
|
let mut layer_ids = vec![bg_id];
|
|
|
|
|
|
|
|
|
|
// Create and fill additional raster layers so every layer contributes.
|
|
|
|
|
for i in 1..LAYERS {
|
|
|
|
|
let id = engine.add_layer(&format!("Layer {}", i));
|
|
|
|
|
engine.set_active_layer(id);
|
|
|
|
|
let color = match i % 4 {
|
|
|
|
|
0 => make_solid_color(180, 80, 80),
|
|
|
|
|
1 => make_solid_color(80, 180, 80),
|
|
|
|
|
2 => make_solid_color(80, 80, 180),
|
|
|
|
|
_ => make_solid_color(160, 140, 100),
|
|
|
|
|
};
|
|
|
|
|
engine.draw_filled_rect_rgba(0, 0, W, H, color);
|
|
|
|
|
layer_ids.push(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Switch to the top layer for painting.
|
|
|
|
|
let top_id = *layer_ids.last().expect("at least one layer");
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
// Start the stroke roughly in the center.
|
|
|
|
|
let start_x = W as f32 / 2.0;
|
|
|
|
|
let start_y = H as f32 / 2.0;
|
|
|
|
|
engine.begin_stroke(top_id, start_x, start_y);
|
|
|
|
|
|
|
|
|
|
let mut segment_times = Vec::with_capacity(SEGMENTS);
|
|
|
|
|
let mut composite_times = Vec::with_capacity(SEGMENTS);
|
|
|
|
|
|
|
|
|
|
let total_start = std::time::Instant::now();
|
|
|
|
|
for i in 0..SEGMENTS {
|
|
|
|
|
let t = (i as f32) / SEGMENTS as f32;
|
|
|
|
|
let angle = t * 6.28 * 2.0;
|
|
|
|
|
let radius = 400.0 * t;
|
|
|
|
|
let x = start_x + angle.cos() * radius;
|
|
|
|
|
let y = start_y + angle.sin() * radius;
|
|
|
|
|
|
|
|
|
|
let seg_start = std::time::Instant::now();
|
|
|
|
|
engine.stroke_to(top_id, x, y, 0.8);
|
|
|
|
|
segment_times.push(seg_start.elapsed().as_micros() as f64);
|
|
|
|
|
|
|
|
|
|
if (i + 1) % COMPOSITE_EVERY_N_SEGMENTS == 0 {
|
|
|
|
|
let comp_start = std::time::Instant::now();
|
|
|
|
|
let (region, ptr, _size) = engine.render_composite_region();
|
|
|
|
|
// Ensure the engine actually produced a buffer pointer; do not
|
|
|
|
|
// optimize away the call.
|
|
|
|
|
assert!(!ptr.is_null());
|
|
|
|
|
let comp_us = comp_start.elapsed().as_micros() as f64;
|
|
|
|
|
composite_times.push(comp_us);
|
|
|
|
|
// Keep the region alive so the compiler doesn't discard it.
|
|
|
|
|
let _ = region;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
engine.end_stroke(top_id);
|
|
|
|
|
let total_us = total_start.elapsed().as_micros() as f64;
|
|
|
|
|
|
|
|
|
|
let avg_segment_us = segment_times.iter().sum::<f64>() / segment_times.len().max(1) as f64;
|
2026-07-16 22:10:22 +03:00
|
|
|
let avg_composite_us =
|
|
|
|
|
composite_times.iter().sum::<f64>() / composite_times.len().max(1) as f64;
|
2026-07-09 02:59:53 +03:00
|
|
|
let max_segment_us = segment_times.iter().copied().fold(0.0, f64::max);
|
|
|
|
|
let max_composite_us = composite_times.iter().copied().fold(0.0, f64::max);
|
|
|
|
|
|
|
|
|
|
println!("\n=== 4K Multi-Layer Stroke Benchmark ===");
|
2026-07-16 22:10:22 +03:00
|
|
|
println!(
|
|
|
|
|
"Canvas: {}x{}, Layers: {}, Segments: {}",
|
|
|
|
|
W, H, LAYERS, SEGMENTS
|
|
|
|
|
);
|
2026-07-09 02:59:53 +03:00
|
|
|
println!("Average stroke_to: {:>8.1} us", avg_segment_us);
|
|
|
|
|
println!("Max stroke_to: {:>8.1} us", max_segment_us);
|
|
|
|
|
println!("Average composite: {:>8.1} us", avg_composite_us);
|
|
|
|
|
println!("Max composite: {:>8.1} us", max_composite_us);
|
2026-07-16 22:10:22 +03:00
|
|
|
println!(
|
|
|
|
|
"Total wall time: {:>8.1} us ({:.2} s)",
|
|
|
|
|
total_us,
|
|
|
|
|
total_us / 1_000_000.0
|
|
|
|
|
);
|
|
|
|
|
println!(
|
|
|
|
|
"Segments per second: {:>8.1}",
|
|
|
|
|
SEGMENTS as f64 / (total_us / 1_000_000.0)
|
|
|
|
|
);
|
2026-07-09 02:59:53 +03:00
|
|
|
|
|
|
|
|
// Soft sanity check: the engine must still report a valid top layer and the
|
|
|
|
|
// document must be marked modified after painting.
|
|
|
|
|
assert_eq!(engine.active_layer_id(), top_id);
|
|
|
|
|
assert!(engine.is_modified());
|
|
|
|
|
}
|