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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//! Reproducer: the same scene rasterises to two different images.
//!
//! Renders one unchanged `PlotComposition` at one size, repeatedly, with
//! a fresh `VelloRenderer` each time, and counts how many distinct
//! outputs come back. It should always be 1. On the machine this was
//! found on it is 2, split roughly evenly, with a single pixel differing
//! by one unit in its green and blue channels.
//!
//! Run with:
//!
//! ```sh
//! cargo run --example aa_nondeterminism
//! cargo run --release --example aa_nondeterminism
//! ```
//!
//! Notes on what has been ruled out, so nobody re-treads it:
//!
//! - **Not the scene.** Teeing the same render pass into a
//! `RecordingScene` alongside the Vello scene gives a byte-identical op
//! stream every time. The draw calls are deterministic; only the pixels
//! are not.
//! - **Not renderer reuse.** Each render constructs its own
//! `VelloRenderer`, so nothing carries over in backend state.
//! - **Not accumulated float drift.** There are exactly two outcomes,
//! never three, however many times it runs.
//! - **Not the plot's own caches.** Two renders back to back, with no
//! mutation between them, already disagree.
//!
//! The composition below is the smallest one found that still triggers
//! it. Both ingredients matter and neither is interesting in itself:
//! removing the composition caption stops it, and so does removing the
//! point geoms. The caption is not near the differing pixel — it only
//! shifts the layout enough to move a mark's antialiased edge onto a
//! subpixel position that triggers the bug. That suggests the real
//! trigger is a particular edge geometry rather than any of these
//! features.
use hephaestus::backend::vello::VelloRenderer;
use hephaestus::color::{rgb8, Color};
use hephaestus::composition::{beside, stack, Patch};
use hephaestus::geometry::Size;
use hephaestus::plot::{scale, Plot, PlotComposition, PointGeom};
use hephaestus::scene::SceneBuilder;
use hephaestus::Renderer;
/// Renders per trial. The flip rate is roughly even, so a handful would
/// do; this many just makes a clean run convincing.
const TRIALS: usize = 60;
const SIZE: (u32, u32) = (1200, 500);
fn build() -> PlotComposition {
let comp = || {
stack(
beside(Patch::new("a"), Patch::new("b")),
beside(Patch::new("c"), Patch::new("d")),
)
};
let xs: Vec<f64> = (0..40).map(|i| f64::from(i) * 0.5).collect();
let ys: Vec<f64> = xs.iter().map(|x| 10.0 + 8.0 * (x * 0.2).sin()).collect();
let mut view = PlotComposition::new(&comp())
// Removing this caption makes the output deterministic.
.caption("Composition caption")
.add_scale("t", scale::continuous(0.0..=20.0))
.add_scale("v", scale::continuous(0.0..=20.0));
for name in ["a", "b", "c", "d"] {
let mut plot = Plot::new(&comp(), name).bind("x", "t").bind("y", "v");
// Removing these geoms also makes the output deterministic.
plot.add_geom(
PointGeom::builder()
.set("x", xs.clone())
.set("y", ys.clone())
.set("fill", rgb8(200, 30, 30))
.set("size", 4.0_f64)
.build(),
);
view = view.with_plot(plot);
}
view
}
fn render(comp: &mut PlotComposition, w: u32, h: u32) -> Vec<u8> {
let mut renderer = VelloRenderer::new().expect("a working wgpu adapter");
renderer.scene().clear();
comp.render(
renderer.scene(),
Size::new(f64::from(w), f64::from(h)),
96.0,
);
let mut buf = vec![0u8; (w * h * 4) as usize];
renderer
.render_to_buffer(w, h, Color::WHITE, &mut buf)
.expect("render to buffer");
buf
}
fn main() {
let (w, h) = SIZE;
let mut comp = build();
// Keep one representative buffer per distinct output, so the
// difference can be reported rather than just counted.
let mut distinct: Vec<(Vec<u8>, usize)> = Vec::new();
for _ in 0..TRIALS {
let buf = render(&mut comp, w, h);
match distinct.iter_mut().find(|(b, _)| *b == buf) {
Some((_, count)) => *count += 1,
None => distinct.push((buf, 1)),
}
}
distinct.sort_by_key(|(_, c)| std::cmp::Reverse(*c));
let counts: Vec<String> = distinct.iter().map(|(_, c)| c.to_string()).collect();
println!(
"{} distinct outputs over {TRIALS} renders of the same scene at {w}x{h} (counts {})",
distinct.len(),
counts.join("/")
);
if distinct.len() == 1 {
println!("deterministic on this machine");
return;
}
let (a, _) = &distinct[0];
let (b, _) = &distinct[1];
for i in 0..a.len() {
if a[i] != b[i] {
let px = i / 4;
println!(
" pixel ({}, {}) channel {}: {} vs {}",
px % w as usize,
px / w as usize,
i % 4,
a[i],
b[i]
);
}
}
}