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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#![cfg(feature = "testing")]
//! More shapes than the instance buffer was made for.
//!
//! `Renderer::ensure_instance_capacity` grows the buffer when a frame carries
//! more instances than it holds — and since #398 it must rebuild the bind
//! group too, because that is what now names the buffer. Under the vertex
//! attribute layout it did not have to: a vertex buffer was named at draw
//! time, so replacing it could not leave a stale binding behind. A bind group
//! holds the buffer it was made from, so forgetting to rebuild one leaves the
//! shader reading the *old, smaller* buffer, and every instance past its end
//! reads whatever the clamp gives back.
//!
//! Nothing was exercising that. The buffer starts at 256 instances and no test
//! in the suite had ever drawn more, so `ensure_instance_capacity` could be
//! replaced with `()` — its whole body deleted — and the suite stayed green.
//! CI's mutation run is what said so.
//!
//! So: draw well past 256, and check the ones at the end are on screen. A
//! frame that overflows the buffer and does not grow it fails wgpu's own
//! validation on the write; one that grows it without rebuilding the bind
//! group draws the late shapes wrong.
mod common;
use guido::prelude::*;
/// Comfortably past the 256 the buffer starts at, and past the 512 one
/// doubling gives, so the growth runs more than once.
const SHAPES: usize = 600;
const CELL: f32 = 8.0;
const ACROSS: usize = 30;
/// A grid of small squares, the last of them a colour nothing else uses.
///
/// The marker is what says the *late* instances arrived: a stale binding
/// leaves the shapes past the old capacity reading another instance's data,
/// and the one thing no other instance can supply is this colour.
fn grid() -> Container {
let mut rows = container().layout(Flex::column().spacing(0.0));
for r in 0..SHAPES.div_ceil(ACROSS) {
let mut row = container().layout(Flex::row().spacing(0.0));
for c in 0..ACROSS {
let i = r * ACROSS + c;
if i >= SHAPES {
break;
}
let last = i == SHAPES - 1;
row = row.child(container().width(CELL).height(CELL).background(if last {
Color::rgb(1.0, 0.0, 1.0)
} else {
Color::rgb(0.0, 0.3, 0.6)
}));
}
rows = rows.child(row);
}
rows
}
#[test]
fn the_shapes_past_the_buffers_first_size_are_still_drawn() {
let Some(mut app) = common::headless() else {
return;
};
let width = (ACROSS as f32 * CELL) as u32;
let height = (SHAPES.div_ceil(ACROSS) as f32 * CELL) as u32;
let config = SurfaceConfig::new()
.height(height)
.anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT);
let surface = app.surface(config, || {
container().width(fill()).height(fill()).child(grid())
});
app.configure(surface, width, height, 1.0);
app.step();
// The middle of the very last cell.
let last = SHAPES - 1;
let x = ((last % ACROSS) as f32 * CELL + CELL / 2.0) as u32;
let y = ((last / ACROSS) as f32 * CELL + CELL / 2.0) as u32;
let [r, g, b, _] = app.read_pixel(surface, x, y);
assert!(
r > 200 && g < 60 && b > 200,
"instance {last} of {SHAPES} is the magenta cell at ({x}, {y}) and it \
reads as ({r}, {g}, {b}) — the instance buffer grew past its first \
size and something is still reading the old one"
);
// And an early cell is untouched, so the test is not passing on a frame
// that drew nothing but the marker.
let [er, eg, eb, _] = app.read_pixel(surface, (CELL / 2.0) as u32, (CELL / 2.0) as u32);
assert!(
er < 60 && eg > 40 && eb > 100,
"the first cell should still be the ordinary blue, got ({er}, {eg}, {eb})"
);
}