From 2046c646fae9a283d8d439a8eb90bce1b7a91cd4 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 08:42:53 -0800 Subject: [PATCH] Image Trace: blend settling caps its pairs of label changes at the size of a label copy The pairs a band or group of strips keeps for its changed labels now grow by doubling only up to the count that fits in a copy of its labels, so they never take more memory than that copy. The next change moves them into the copy. - Tests: most of a share changing gives the same labels as when few change, for both settle passes and every share size (the copy path); both settle passes return a picture with no pixels unchanged. --- crates/trace/src/quantize.rs | 4 +- crates/trace/src/quantize/cut_edge.rs | 61 +++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/crates/trace/src/quantize.rs b/crates/trace/src/quantize.rs index e89f3eda3b..9a7031256a 100644 --- a/crates/trace/src/quantize.rs +++ b/crates/trace/src/quantize.rs @@ -374,9 +374,9 @@ mod tests { [c[0], c[1], c[2], if y + 2 >= h { 160 } else { 255 }] }); let all = w as usize * h as usize; - // One row per share, a few rows per share, and the whole picture as one. let opaque: Vec = img.rgba.as_chunks::<4>().0.iter().map(|p| p[3] >= 128).collect(); - let own: Vec> = [1, 3 * 173 + 5, all].iter().map(|&s| own_colours_among_by(&img, &opaque, s)).collect(); + // One row per share, a few rows per share, and the whole picture as one. + let own: Vec> = [1, 3 * w as usize + 5, all].iter().map(|&s| own_colours_among_by(&img, &opaque, s)).collect(); assert_eq!(own[0].len(), all); assert!(own[0].iter().any(|&o| !o), "the edges have blend pixels"); assert!(own.iter().all(|o| *o == own[0])); diff --git a/crates/trace/src/quantize/cut_edge.rs b/crates/trace/src/quantize/cut_edge.rs index 2155d6d2af..265c0d9b2c 100644 --- a/crates/trace/src/quantize/cut_edge.rs +++ b/crates/trace/src/quantize/cut_edge.rs @@ -89,7 +89,8 @@ fn settled_by(mut q: Quantized, img: &Raster, own: &[bool], per_share: usize) -> /// The labels a pass changes in one part of a picture, `len` pixels long, counting each pixel from /// the part's first. They are kept as pairs of a pixel and its new label until the pairs would take /// more memory than a copy of the part's labels, and as that copy, with the changes made, after -/// that. A part takes at most about as much memory as a copy of its labels either way. +/// that. The pairs take at most as much memory as the copy, and a part holds both only while it +/// moves the pairs into the copy. struct Changes { len: usize, few: Vec<(usize, u16)>, @@ -110,16 +111,23 @@ impl Changes { } return; } - self.few.push((at, to)); - if self.few.len() * size_of::<(usize, u16)>() > self.len * size_of::() { - let mut copy = labels(); - for (at, to) in std::mem::take(&mut self.few) { - if let Some(l) = copy.get_mut(at) { - *l = to; - } + // The most pairs that take no more memory than a copy of the part's labels. The pairs grow + // by doubling up to that many, and no further. + let most = self.len * size_of::() / size_of::<(usize, u16)>(); + if self.few.len() < most { + if self.few.len() == self.few.capacity() { + self.few.reserve_exact(self.few.capacity().max(4).min(most - self.few.len())); } - self.copy = Some(copy); + self.few.push((at, to)); + return; } + let mut copy = labels(); + for (at, to) in std::mem::take(&mut self.few).into_iter().chain([(at, to)]) { + if let Some(l) = copy.get_mut(at) { + *l = to; + } + } + self.copy = Some(copy); } fn is_empty(&self) -> bool { @@ -789,6 +797,7 @@ mod tests { changes.set(at * 3, 99, || labels.clone()); } assert!(changes.copy.is_none()); + assert!(changes.few.capacity() <= pairs, "the pairs take no more memory than a copy of the labels"); changes.set(63, 98, || labels.clone()); assert!(changes.few.is_empty() && changes.copy.is_some()); changes.set(1, 97, || labels.clone()); @@ -834,6 +843,40 @@ mod tests { } } + #[test] + fn blend_pixels_settle_the_same_when_most_of_a_share_changes() { + // Columns of red and blue own pixels, two blend pixels between each pair: one nearer to red + // and one nearer to blue, both labeled with an olive. Half the pixels of each band of rows and + // of each group of strips change, more than the eighth at which a share's changes are copied + // out (see [`Changes`]). 44 columns, so that the last strip of 7 is short. + let (w, h) = (44u32, 5u32); + let (red, blue) = ([200u8, 30, 40], [20u8, 60, 180]); + let mix = |t: f64| [0, 1, 2].map(|k| (f64::from(red[k]) + t * (f64::from(blue[k]) - f64::from(red[k]))).round() as u8); + let img = Raster::from_fn(w, h, |x, _| { + let c = [red, mix(0.3), mix(0.7), blue][x as usize % 4]; + [c[0], c[1], c[2], 255] + }); + let own: Vec = (0..w * h).map(|i| matches!(i % w % 4, 0 | 3)).collect(); + let labels = (0..w * h).map(|i| [0, 2, 2, 1][(i % w % 4) as usize]).collect(); + let q = Quantized { labels, palette: vec![red, blue, [120, 120, 60]] }; + let expected = Quantized { labels: (0..w * h).map(|i| [0, 0, 1, 1][(i % w % 4) as usize]).collect(), palette: vec![red, blue] }; + let all = (w * h) as usize; + for s in [1, 3 * 7 * h as usize, all] { + assert_eq!(settled_by(q.clone(), &img, &own, s), expected, "settled, {s} pixels per share"); + assert_eq!(settled_wide_by(q.clone(), &img, &own, false, s), expected, "settled_wide, {s} pixels per share"); + } + } + + #[test] + fn a_picture_with_no_pixels_settles_to_itself() { + let q = Quantized { labels: vec![], palette: vec![[0, 0, 0], [255, 255, 255]] }; + for (w, h) in [(u32::MAX, 0), (0, u32::MAX)] { + let img = Raster::new(w, h, vec![]).unwrap(); + assert_eq!(settled_by(q.clone(), &img, &[], PIXELS_PER_SHARE), q, "settled, {w} x {h}"); + assert_eq!(settled_wide_by(q.clone(), &img, &[], false, PIXELS_PER_SHARE), q, "settled_wide, {w} x {h}"); + } + } + #[test] fn a_gray_blend_takes_the_closer_of_two_pairs_it_lies_between_despite_rounding() { // Gray 105 in the middle, with own pixels of gray 50 to its left, 200 to its right and black