From 83aff80f35b06dd4a847ffcfce7d6d50bb724eef Mon Sep 17 00:00:00 2001 From: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:19:18 +0200 Subject: [PATCH] fix(raster): support sample counts beyond stack capacity --- crates/raster/src/lib.rs | 61 ++++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/crates/raster/src/lib.rs b/crates/raster/src/lib.rs index 694cdb5751..2e3dc53811 100644 --- a/crates/raster/src/lib.rs +++ b/crates/raster/src/lib.rs @@ -117,6 +117,16 @@ pub struct Surface { tiles: BTreeMap>, } +/// Use stack storage when it fits, otherwise reuse a dynamically sized sample buffer. +fn sample_buffer<'a>(channels: usize, stack: &'a mut [f32], heap: &'a mut Vec) -> &'a mut [f32] { + if let Some(samples) = stack.get_mut(..channels) { + samples + } else { + heap.resize(channels, 0.0); + heap + } +} + /// Tiles of one pixel value met while moving a surface ([`Surface::translated`]): each scanned /// once, and one tile kept per value so the targets share it. #[derive(Default)] @@ -239,10 +249,10 @@ impl Surface { decode_pixel(&self.format, &self.default_pixel) } - /// Decode the default pixel into fixed storage without allocating. - fn default_samples(&self) -> [f32; 8] { - let mut px = [0.0; 8]; - for (i, v) in px.iter_mut().enumerate().take(self.channels()) { + /// Decode the default pixel, using dynamic storage only when the stack buffer is too small. + fn default_samples<'a>(&self, stack: &'a mut [f32], heap: &'a mut Vec) -> &'a mut [f32] { + let px = sample_buffer(self.channels(), stack, heap); + for (i, v) in px.iter_mut().enumerate() { *v = read_sample(&self.default_pixel, self.format.sample, i); } px @@ -427,8 +437,9 @@ impl Surface { fn read_samples_into(&self, r: Rect, out: &mut [f32]) { let n = self.channels(); let w = r.width() as usize; - let dp = self.default_samples(); - let dp = &dp[..n]; + let mut stack = [0.0; 8]; + let mut heap = Vec::new(); + let dp = self.default_samples(&mut stack, &mut heap); let zero = self.default_pixel.iter().all(|&b| b == 0); for tc in r.tiles() { let tr = tc.rect().intersect(&r); @@ -467,7 +478,10 @@ impl Surface { debug_assert_eq!(out.len(), w * r.height() as usize); let fmt = self.format; let n = fmt.channels(); - let dp = to_rgba(&fmt, &self.default_samples()); + let mut stack = [0.0; 8]; + let mut heap = Vec::new(); + let px = self.default_samples(&mut stack, &mut heap); + let dp = to_rgba(&fmt, px); for tc in r.tiles() { let tr = tc.rect().intersect(&r); let tile = self.tiles.get(&tc); @@ -511,12 +525,11 @@ impl Surface { } } _ => { - let mut px = [0.0f32; 8]; for (i, d) in dst.iter_mut().enumerate() { - for (c, v) in px.iter_mut().enumerate().take(n) { + for (c, v) in px.iter_mut().enumerate() { *v = read_sample(&t.data, fmt.sample, base + i * n + c); } - *d = to_rgba(&fmt, &px[..n]); + *d = to_rgba(&fmt, px); } } } @@ -532,7 +545,9 @@ impl Surface { let fmt = self.format; let rgba8 = matches!((fmt.mode, fmt.sample, fmt.alpha), (ColorMode::Rgb, SampleType::U8, true)); let q = |v: f32| (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u8; - let dp = to_rgba(&fmt, &self.default_samples()); + let mut stack = [0.0; 8]; + let mut heap = Vec::new(); + let dp = to_rgba(&fmt, self.default_samples(&mut stack, &mut heap)); let dp8 = [q(dp[0]), q(dp[1]), q(dp[2]), q(dp[3])]; let mut row = Vec::new(); for tc in r.tiles() { @@ -1293,6 +1308,24 @@ mod tests { } } + /// Sample counts above the stack capacity use writable storage without truncating channels. + #[test] + fn sample_buffer_keeps_stack_fast_path_and_supports_dynamic_channels() { + for channels in [1, 8, 9, 32] { + let mut stack = [0.0; 8]; + let mut heap = Vec::new(); + let samples = sample_buffer(channels, &mut stack, &mut heap); + assert_eq!(samples.len(), channels); + samples.fill(0.25); + if channels <= stack.len() { + assert!(heap.is_empty()); + assert_eq!(&stack[..channels], vec![0.25; channels]); + } else { + assert_eq!(heap, vec![0.25; channels]); + } + } + } + /// Compare sparse reads and converted accessors with the previous allocating path. #[test] fn zero_alloc_accessors_match_read_region() { @@ -1310,6 +1343,12 @@ mod tests { .flat_map(|fmt| [0.0, 0.25, -0.0].map(|default| (fmt, default))) { let mut s = Surface::with_default(fmt, &vec![default; fmt.channels()]); + let mut stack = [0.0; 8]; + let mut heap = Vec::new(); + for capacity in [0, stack.len()] { + let decoded = s.default_samples(&mut stack[..capacity], &mut heap); + assert!(decoded.iter().map(|v| v.to_bits()).eq(s.default_pixel().into_iter().map(f32::to_bits))); + } let r = Rect::new(-300, -20, 300, 40); for (i, (x, y)) in [(-299, -19), (0, 0), (255, 39), (256, 10), (299, 0)].into_iter().enumerate() { let px: Vec = (0..fmt.channels()).map(|c| ((i * 3 + c) % 5) as f32 / 4.0).collect();