1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
//! Regression test for the GPU undo-storage corruption bug.
//!
//! On master, [`RegionStore`] kept a 256 MB ring buffer of undo bytes that
//! evicted on its own schedule, silently invalidating [`UndoRegionEntry`]
//! values the undo stack still held. After enough commits to wrap the ring,
//! the canvas reverted to a glitchy / squished pattern instead of the saved
//! pre-stroke state.
//!
//! This test drives full-canvas commits past the ring capacity and asserts
//! that an undo all the way back to the original state recovers the original
//! pixels byte-for-byte. On master it fails (corrupted pixels); on the
//! per-entry redesign it passes.
//!
//! Run with: `cargo test -p darkly --test gpu_undo_round_trip_preserves_pixels -- --test-threads=1`
use darkly::engine::DarklyEngine;
use darkly::gpu::context::GpuContext;
use darkly::gpu::test_utils::test_device;
fn test_engine(width: u32, height: u32) -> DarklyEngine {
let (device, queue) = test_device();
let gpu = GpuContext::new_headless(device, queue);
DarklyEngine::new(gpu, width, height)
}
/// Paint 20 full-canvas commits across a 2048² canvas (~320 MB of undo
/// pixels — well past the legacy 256 MB ring), then undo all 20 times.
/// The final canvas must match the initial post-A state pixel-for-pixel.
#[test]
fn gpu_undo_round_trip_preserves_pixels() {
// 2048² RGBA = 16 MB per commit. 20 commits = 320 MB, which wraps the
// legacy 256 MB ring (the bug surfaces on commit 17 and later).
let (w, h) = (2048, 2048);
let mut engine = test_engine(w, h);
let layer = engine.add_raster_layer(None);
// Anchor state A — solid red across the layer. This is the snapshot
// every subsequent fill's `save_region` captures, so it's also what the
// final undo step must reproduce.
engine.fill_background_color(layer, [255, 0, 0, 255]);
engine.render(0.0);
engine.test_flush_readbacks();
let pixels_a = engine.test_readback_layer(layer);
// 20 distinctive successor fills. Each commits a fresh 16 MB undo
// entry. The legacy ring wraps somewhere around commit 17.
for i in 0..20u8 {
let color = [i.wrapping_mul(12), 200, 0, 255];
engine.fill_background_color(layer, color);
engine.render(0.0);
engine.test_flush_readbacks();
}
// Undo all 20 fills. After the last undo the layer must equal
// pixels_a — every intermediate restore must produce valid pixels for
// the round-trip to land correctly.
for _ in 0..20 {
engine.undo();
engine.render(0.0);
engine.test_flush_readbacks();
}
let pixels_after = engine.test_readback_layer(layer);
assert_eq!(
pixels_a.len(),
pixels_after.len(),
"readback lengths must match"
);
let mismatches = pixels_a
.iter()
.zip(pixels_after.iter())
.filter(|(a, b)| a != b)
.count();
assert_eq!(
mismatches,
0,
"post-undo canvas must equal the saved pre-fill state: {mismatches} mismatched bytes \
out of {} ({}%)",
pixels_a.len(),
(mismatches as f64 / pixels_a.len() as f64) * 100.0
);
}