Skip to main content

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}