diff --git a/crates/trace/src/layers.rs b/crates/trace/src/layers.rs index 1a592cd292..04f687b46b 100644 --- a/crates/trace/src/layers.rs +++ b/crates/trace/src/layers.rs @@ -496,7 +496,8 @@ impl Layers<'_> { runs_inside([hole], |y, a, b| { #[cfg(test)] round.live_reads.fetch_add(1, Ordering::Relaxed); - !dead.get(y).is_some_and(|runs| meets(runs, a, b)) + let row = y.saturating_mul(self.w); + !meets(dead, row.saturating_add(a), row.saturating_add(b)) }) }; let mut marked = vec![]; @@ -739,17 +740,13 @@ impl Layers<'_> { // Two bytes a pixel for the marks. let marks: Vec = (0..n).map(|_| AtomicU16::new(0)).collect(); let untraced = |l: u16| l == TRANSPARENT || self.ignored(l); - // Eight bytes a run of untraced pixels, at most four a pixel (where every other pixel is one), and a list a row. - let mut dead = self.traced.iter().any(|&l| untraced(l)).then(|| dead_runs(self.w, self.traced, untraced)); - let dead_bytes = dead.as_ref().map_or(0, |rows| { - rows.iter().fold(0usize, |s, r| { - s.saturating_add(size_of::>()).saturating_add(r.capacity().saturating_mul(size_of::<(u32, u32)>())) - }) - }); + // Eight bytes a run of untraced pixels, at most four a pixel (where every other pixel is one). + let mut dead = self.traced.iter().any(|&l| untraced(l)).then(|| dead_runs(self.traced, untraced)); + let dead_bytes = dead.as_ref().map_or(0, |runs| runs.capacity().saturating_mul(size_of::<(u32, u32)>())); #[cfg(test)] let probe = PROBE.with(|p| { let mut p = p.borrow_mut(); - (p.live_reads, p.areas) = (0, vec![]); + (p.live_reads, p.areas, p.dead_bytes) = (0, vec![], dead_bytes); p.clone() }); #[cfg(test)] @@ -1074,9 +1071,9 @@ struct Round<'a> { /// it; 0 where none does. marks: &'a [AtomicU16], /// In the first round, where a hole may hold a pixel that the layers do not trace (transparent, or a color left - /// out), the runs of such pixels in each row, left to right as `(x0, x1)`: an area is filled only where it holds - /// none. `None` in later rounds and in images without such pixels. - dead: Option<&'a [Vec<(u32, u32)>]>, + /// out), the runs of such pixels (see [`dead_runs`]): an area is filled only where it holds none. `None` in later + /// rounds and in images without such pixels. + dead: Option<&'a [(u32, u32)]>, /// Tests: the reads made to check a hole against `dead`. #[cfg(test)] live_reads: &'a AtomicUsize, @@ -1129,6 +1126,8 @@ pub(crate) struct Probe { pub(crate) budget: Option, /// Counted: the reads the first round made to check that a hole holds only pixels the layers trace. pub(crate) live_reads: usize, + /// Counted: the bytes the runs of untraced pixels take. + pub(crate) dead_bytes: usize, /// Counted: each area of a later round, as `(round, cost, filled)`, in the order the budget was charged. pub(crate) areas: Vec<(usize, u64, bool)>, } @@ -1138,25 +1137,21 @@ thread_local! { pub(crate) static PROBE: std::cell::RefCell = std::cell::RefCell::new(Probe::default()); } -/// The runs of the pixels in each row of a `w` pixels wide image for which `dead` holds, left to right as `(x0, x1)`. -fn dead_runs(w: usize, labels: &[u16], dead: impl Fn(u16) -> bool) -> Vec> { - labels - .chunks(w.max(1)) - .map(|row| { - let mut runs: Vec<(u32, u32)> = vec![]; - for (x, &l) in row.iter().enumerate() { - let x = u32::try_from(x).unwrap_or(u32::MAX); - if !dead(l) { - continue; - } - match runs.last_mut() { - Some(r) if r.1 == x => r.1 = x.saturating_add(1), - _ => runs.push((x, x.saturating_add(1))), - } - } - runs - }) - .collect() +/// The runs of the pixels of `labels` for which `dead` holds, in order as `(i0, i1)` (indices into `labels`, so that a +/// run goes on into the next row; one list for the image, whatever its height). +fn dead_runs(labels: &[u16], dead: impl Fn(u16) -> bool) -> Vec<(u32, u32)> { + let mut runs: Vec<(u32, u32)> = vec![]; + for (i, &l) in labels.iter().enumerate() { + let i = u32::try_from(i).unwrap_or(u32::MAX); + if !dead(l) { + continue; + } + match runs.last_mut() { + Some(r) if r.1 == i => r.1 = i.saturating_add(1), + _ => runs.push((i, i.saturating_add(1))), + } + } + runs } /// Whether any of the runs `runs` (left to right, apart) meets the pixels `a..b`. diff --git a/crates/trace/src/tests.rs b/crates/trace/src/tests.rs index 46d3c7604b..ce343ceb68 100644 --- a/crates/trace/src/tests.rs +++ b/crates/trace/src/tests.rs @@ -485,6 +485,22 @@ fn overlapping_checks_nested_holes_around_a_transparent_center_by_their_rows() { assert!(probe.live_reads > 0 && probe.live_reads < 37 * 300, "{} reads", probe.live_reads); } +/// Overlapping: in a tall, narrow image that is transparent but for a black block with a white center, the runs of +/// untraced pixels take a few bytes for the whole image, not a list for each row. +#[test] +fn overlapping_holds_the_untraced_pixels_of_a_tall_narrow_image_in_a_few_runs() { + let img = Raster::from_fn(3, 20_000, |x, y| match y { + 100..110 if x == 1 && (104..106).contains(&y) => WHITE, + 100..110 => BLACK, + _ => [0, 0, 0, 0], + }); + let params = + TraceParams { mode: Mode::Color, palette: Palette::Limited, colors: 2, noise: 0, method: Method::Overlapping, ..TraceParams::default() }; + let (res, probe) = probed(layers::Probe::default(), || trace(&img, ¶ms)); + assert_draws(&img, &res, &[]); + assert!(probe.dead_bytes > 0 && probe.dead_bytes <= 64, "{} bytes", probe.dead_bytes); +} + /// Overlapping: an enclosed area past what is left of the work budget keeps its holes and is charged nothing, so a /// smaller area after it in the same round is still filled. #[test]