use darkly::engine::types::StrokeOp;
use darkly::engine::DarklyEngine;
use darkly::gpu::context::GpuContext;
use darkly::gpu::test_utils::test_device;
fn fresh_engine() -> DarklyEngine {
let (device, queue) = test_device();
let gpu = GpuContext::new_headless(device, queue);
DarklyEngine::new(gpu, 256, 256)
}
fn paint_short_stroke(engine: &mut DarklyEngine, layer_id: darkly::layer::LayerId) {
engine.begin_stroke(layer_id);
for step in 0..10 {
engine.stroke_to(StrokeOp::BrushStroke {
x: step as f32 * 20.0 + 10.0,
y: 128.0,
pressure: 1.0,
x_tilt: 0.0,
y_tilt: 0.0,
rotation: 0.0,
tangential_pressure: 0.0,
time_ms: step as f64 * 16.0,
cr: 1.0,
cg: 0.0,
cb: 0.0,
ca: 1.0,
});
}
engine.end_stroke();
}
fn has_painted_pixels(bytes: &[u8]) -> bool {
bytes.chunks_exact(4).any(|p| {
let r = p[0];
let g = p[1];
let b = p[2];
r > 200 || g.abs_diff(b) > 50
})
}
#[test]
fn paint_stroke_auto_queues_thumbnail_readback() {
let mut engine = fresh_engine();
let layer_id = engine.add_raster_layer(None);
engine.render(0.0);
engine.test_flush_readbacks();
let v0 = engine.thumbnail_version();
paint_short_stroke(&mut engine, layer_id);
engine.render(0.016);
engine.test_flush_readbacks();
let v1 = engine.thumbnail_version();
assert!(
v1 > v0,
"thumbnail_version should increase after paint+render+flush; v0={v0} v1={v1}"
);
let cached = engine
.test_thumbnail_cache_peek(layer_id)
.expect("auto-queue path should have populated the layer thumbnail cache");
assert!(
!cached.is_empty(),
"cached thumbnail bytes should be non-empty"
);
assert!(
has_painted_pixels(&cached),
"cached thumbnail should reflect the painted stroke (saw pure-checkerboard bytes)"
);
}
#[test]
fn fill_background_auto_queues_thumbnail_readback() {
let mut engine = fresh_engine();
let layer_id = engine.add_raster_layer(None);
engine.render(0.0);
engine.test_flush_readbacks();
let v0 = engine.thumbnail_version();
engine.fill_background(layer_id);
engine.render(0.016);
engine.test_flush_readbacks();
let v1 = engine.thumbnail_version();
assert!(
v1 > v0,
"thumbnail_version should increase after fill_background; v0={v0} v1={v1}"
);
let cached = engine
.test_thumbnail_cache_peek(layer_id)
.expect("auto-queue path should have populated the layer thumbnail cache");
assert!(
!cached.is_empty(),
"cached thumbnail bytes should be non-empty after fill_background"
);
}
#[test]
fn undo_auto_queues_thumbnail_readback() {
let mut engine = fresh_engine();
let layer_id = engine.add_raster_layer(None);
engine.render(0.0);
engine.test_flush_readbacks();
paint_short_stroke(&mut engine, layer_id);
engine.render(0.016);
engine.test_flush_readbacks();
let v_after_paint = engine.thumbnail_version();
let painted = engine
.test_thumbnail_cache_peek(layer_id)
.expect("paint path populated the cache");
assert!(
has_painted_pixels(&painted),
"post-paint thumbnail should show the stroke"
);
engine.undo();
engine.render(0.032);
engine.test_flush_readbacks();
let v_after_undo = engine.thumbnail_version();
assert!(
v_after_undo > v_after_paint,
"thumbnail_version should increase after undo+render+flush; \
paint={v_after_paint} undo={v_after_undo}"
);
let post_undo = engine
.test_thumbnail_cache_peek(layer_id)
.expect("undo path repopulated the cache");
assert!(
!has_painted_pixels(&post_undo),
"post-undo thumbnail should no longer show the painted stroke \
— auto-queue must have re-read the restored (empty) layer texture"
);
}
#[test]
fn layer_thumbnail_does_not_auto_queue_readback() {
let mut engine = fresh_engine();
let layer_id = engine.add_raster_layer(None);
engine.fill_background(layer_id);
engine.render(0.0);
engine.test_flush_readbacks();
let v_settled = engine.thumbnail_version();
for _ in 0..50 {
let _ = engine.node_thumbnail(layer_id, 36, 36);
engine.render(0.016);
engine.test_flush_readbacks();
}
assert_eq!(
engine.thumbnail_version(),
v_settled,
"layer_thumbnail must not auto-queue readbacks; calling it 50× \
followed by render+flush each time should not bump the version. \
Bug repro: legacy auto-queue + epoch sync = per-dab readback storm."
);
}
#[test]
fn brush_stroke_queues_thumbnail_readback_only_at_end() {
let mut engine = fresh_engine();
let layer_id = engine.add_raster_layer(None);
engine.render(0.0);
engine.test_flush_readbacks();
let v_baseline = engine.thumbnail_version();
engine.begin_stroke(layer_id);
for step in 0..20 {
engine.stroke_to(StrokeOp::BrushStroke {
x: step as f32 * 10.0 + 10.0,
y: 128.0,
pressure: 1.0,
x_tilt: 0.0,
y_tilt: 0.0,
rotation: 0.0,
tangential_pressure: 0.0,
time_ms: step as f64 * 16.0,
cr: 1.0,
cg: 0.0,
cb: 0.0,
ca: 1.0,
});
engine.render(step as f32 * 0.016);
engine.test_flush_readbacks();
}
let v_mid_stroke = engine.thumbnail_version();
assert_eq!(
v_mid_stroke,
v_baseline,
"no thumbnail readback may complete mid-stroke. Bumps observed: {} \
(every bump is a full-doc GPU→CPU transfer; per-dab cadence at 4K = ~1.3 GB/s)",
v_mid_stroke - v_baseline
);
engine.end_stroke();
engine.render(0.5);
engine.test_flush_readbacks();
assert_eq!(
engine.thumbnail_version() - v_baseline,
1,
"exactly one thumbnail readback per stroke (queued by end_stroke, \
completed on the post-end frame). Bumps observed: {}",
engine.thumbnail_version() - v_baseline
);
}