aa_nondeterminism/aa_nondeterminism.rs
1//! Reproducer: the same scene rasterises to two different images.
2//!
3//! Renders one unchanged `PlotComposition` at one size, repeatedly, with
4//! a fresh `VelloRenderer` each time, and counts how many distinct
5//! outputs come back. It should always be 1. On the machine this was
6//! found on it is 2, split roughly evenly, with a single pixel differing
7//! by one unit in its green and blue channels.
8//!
9//! Run with:
10//!
11//! ```sh
12//! cargo run --example aa_nondeterminism
13//! cargo run --release --example aa_nondeterminism
14//! ```
15//!
16//! Notes on what has been ruled out, so nobody re-treads it:
17//!
18//! - **Not the scene.** Teeing the same render pass into a
19//! `RecordingScene` alongside the Vello scene gives a byte-identical op
20//! stream every time. The draw calls are deterministic; only the pixels
21//! are not.
22//! - **Not renderer reuse.** Each render constructs its own
23//! `VelloRenderer`, so nothing carries over in backend state.
24//! - **Not accumulated float drift.** There are exactly two outcomes,
25//! never three, however many times it runs.
26//! - **Not the plot's own caches.** Two renders back to back, with no
27//! mutation between them, already disagree.
28//!
29//! The composition below is the smallest one found that still triggers
30//! it. Both ingredients matter and neither is interesting in itself:
31//! removing the composition caption stops it, and so does removing the
32//! point geoms. The caption is not near the differing pixel — it only
33//! shifts the layout enough to move a mark's antialiased edge onto a
34//! subpixel position that triggers the bug. That suggests the real
35//! trigger is a particular edge geometry rather than any of these
36//! features.
37
38use hephaestus::backend::vello::VelloRenderer;
39use hephaestus::color::{rgb8, Color};
40use hephaestus::composition::{beside, stack, Patch};
41use hephaestus::geometry::Size;
42use hephaestus::plot::{scale, Plot, PlotComposition, PointGeom};
43use hephaestus::scene::SceneBuilder;
44use hephaestus::Renderer;
45
46/// Renders per trial. The flip rate is roughly even, so a handful would
47/// do; this many just makes a clean run convincing.
48const TRIALS: usize = 60;
49const SIZE: (u32, u32) = (1200, 500);
50
51fn build() -> PlotComposition {
52 let comp = || {
53 stack(
54 beside(Patch::new("a"), Patch::new("b")),
55 beside(Patch::new("c"), Patch::new("d")),
56 )
57 };
58
59 let xs: Vec<f64> = (0..40).map(|i| f64::from(i) * 0.5).collect();
60 let ys: Vec<f64> = xs.iter().map(|x| 10.0 + 8.0 * (x * 0.2).sin()).collect();
61
62 let mut view = PlotComposition::new(&comp())
63 // Removing this caption makes the output deterministic.
64 .caption("Composition caption")
65 .add_scale("t", scale::continuous(0.0..=20.0))
66 .add_scale("v", scale::continuous(0.0..=20.0));
67
68 for name in ["a", "b", "c", "d"] {
69 let mut plot = Plot::new(&comp(), name).bind("x", "t").bind("y", "v");
70 // Removing these geoms also makes the output deterministic.
71 plot.add_geom(
72 PointGeom::builder()
73 .set("x", xs.clone())
74 .set("y", ys.clone())
75 .set("fill", rgb8(200, 30, 30))
76 .set("size", 4.0_f64)
77 .build(),
78 );
79 view = view.with_plot(plot);
80 }
81 view
82}
83
84fn render(comp: &mut PlotComposition, w: u32, h: u32) -> Vec<u8> {
85 let mut renderer = VelloRenderer::new().expect("a working wgpu adapter");
86 renderer.scene().clear();
87 comp.render(
88 renderer.scene(),
89 Size::new(f64::from(w), f64::from(h)),
90 96.0,
91 );
92 let mut buf = vec![0u8; (w * h * 4) as usize];
93 renderer
94 .render_to_buffer(w, h, Color::WHITE, &mut buf)
95 .expect("render to buffer");
96 buf
97}
98
99fn main() {
100 let (w, h) = SIZE;
101 let mut comp = build();
102
103 // Keep one representative buffer per distinct output, so the
104 // difference can be reported rather than just counted.
105 let mut distinct: Vec<(Vec<u8>, usize)> = Vec::new();
106 for _ in 0..TRIALS {
107 let buf = render(&mut comp, w, h);
108 match distinct.iter_mut().find(|(b, _)| *b == buf) {
109 Some((_, count)) => *count += 1,
110 None => distinct.push((buf, 1)),
111 }
112 }
113 distinct.sort_by_key(|(_, c)| std::cmp::Reverse(*c));
114
115 let counts: Vec<String> = distinct.iter().map(|(_, c)| c.to_string()).collect();
116 println!(
117 "{} distinct outputs over {TRIALS} renders of the same scene at {w}x{h} (counts {})",
118 distinct.len(),
119 counts.join("/")
120 );
121
122 if distinct.len() == 1 {
123 println!("deterministic on this machine");
124 return;
125 }
126
127 let (a, _) = &distinct[0];
128 let (b, _) = &distinct[1];
129 for i in 0..a.len() {
130 if a[i] != b[i] {
131 let px = i / 4;
132 println!(
133 " pixel ({}, {}) channel {}: {} vs {}",
134 px % w as usize,
135 px / w as usize,
136 i % 4,
137 a[i],
138 b[i]
139 );
140 }
141 }
142}