From 7fe10eea1af0a0fad7170c8057228268784496ec Mon Sep 17 00:00:00 2001 From: yoriswind <164435053+yoriswind@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:48:34 -0700 Subject: [PATCH 1/3] perf(raster): fill fallible default tiles by row --- crates/raster/src/lib.rs | 8 +++- crates/raster/src/tests.rs | 83 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/crates/raster/src/lib.rs b/crates/raster/src/lib.rs index af3c6d48e2..c74951263b 100644 --- a/crates/raster/src/lib.rs +++ b/crates/raster/src/lib.rs @@ -67,9 +67,15 @@ impl Tile { data.try_reserve_exact(bytes).map_err(|_| AllocationError::NotEnoughMemory { bytes })?; data.resize(bytes, 0); if pixel.iter().any(|&b| b != 0) { - for px in data.chunks_exact_mut(pixel.len()) { + // Fill the first row, then copy it to the other rows without another allocation. + let row_len = TILE_SIZE as usize * format.bytes_per_pixel(); + let (row, rest) = data.split_at_mut(row_len); + for px in row.chunks_exact_mut(pixel.len()) { px.copy_from_slice(pixel); } + for dst in rest.chunks_exact_mut(row_len) { + dst.copy_from_slice(row); + } } Ok(Tile { data: data.into_boxed_slice() }) } diff --git a/crates/raster/src/tests.rs b/crates/raster/src/tests.rs index 47e78008fd..6bdccb711d 100644 --- a/crates/raster/src/tests.rs +++ b/crates/raster/src/tests.rs @@ -355,6 +355,89 @@ fn fallible_tile_edits_move_weakly_cached_pixels_and_copy_shared_pixels() { } } +#[test] +fn fallible_default_tiles_preserve_every_encoded_pixel() { + for mode in [ + ColorMode::Bitmap, + ColorMode::Grayscale, + ColorMode::Indexed, + ColorMode::Rgb, + ColorMode::Cmyk, + ColorMode::Lab, + ColorMode::Multichannel, + ColorMode::Duotone, + ] { + for sample in SampleType::ALL { + for alpha in [false, true] { + let fmt = PixelFormat::new(mode, sample, alpha); + let channels = fmt.channels(); + let defaults = [ + ("zero", vec![0.0; channels]), + ("mixed channels", (1..=channels).map(|i| i as f32 / 8.0).collect()), + ("negative zero", vec![-0.0; channels]), + ]; + for (name, values) in defaults { + let mut surface = Surface::with_default(fmt, &values); + let expected = surface.default_pixel.repeat((TILE_SIZE * TILE_SIZE) as usize); + assert_eq!(surface.try_default_tile().unwrap().bytes(), expected, "{fmt:?}, {name}"); + let coord = TileCoord::new(-1, 2); + assert_eq!(surface.try_tile_mut(coord).unwrap().bytes(), expected); + let snapshot = surface.clone(); + surface.try_tile_mut(coord).unwrap().bytes_mut()[0] ^= 0xff; + assert_eq!(snapshot.tile(coord).unwrap().bytes(), expected, "copy-on-write: {fmt:?}"); + assert_ne!(surface.tile(coord).unwrap().bytes(), expected); + } + } + } + } +} + +/// Tile initialization only: 384 tiles cover a 6000 x 4000 image. Each tile is dropped before +/// allocating the next, so this is not an end-to-end image-open or peak-memory measurement. +#[test] +#[ignore = "manual 24 MP release benchmark"] +fn benchmark_fallible_default_tiles() { + use std::{hint::black_box, time::Instant}; + // Isolate the unchanged float-zero control from the other cases' different CPU loads. + let zero_only = std::env::var_os("PHOTOCRAFT_TILE_FILL_ZERO_ONLY").is_some(); + let count = Rect::new(0, 0, 6000, 4000).tile_count(); + for fmt in [PixelFormat::GRAY8, PixelFormat::RGBA8, PixelFormat::CMYKA8, PixelFormat::RGBA16, PixelFormat::RGBA32F] { + if zero_only && fmt != PixelFormat::RGBA32F { + continue; + } + for value in [0.0, 0.25] { + if zero_only && value != 0.0 { + continue; + } + let values: Vec = (0..fmt.channels()).map(|i| value * (i + 1) as f32).collect(); + let surface = Surface::with_default(fmt, &values); + for trial in 0..7 { + let start = Instant::now(); + for _ in 0..count { + black_box(black_box(&surface).try_default_tile().unwrap()); + } + println!("tile_fill,{fmt:?},{value},{trial},{:.6}", start.elapsed().as_secs_f64() * 1000.0); + } + } + } + if zero_only { + return; + } + // A fallible region write also converts and stores the incoming samples. Measure that + // caller separately so tile-initialization gains are not presented as editing speedups. + let rect = Rect::new(0, 0, 6000, 4000); + let samples = vec![0.75; 6000 * 4000]; + for trial in 0..7 { + let mut mask = Surface::with_default(PixelFormat::GRAY8, &[1.0]); + let start = Instant::now(); + mask.try_write_region(rect, black_box(&samples)).unwrap(); + let elapsed = start.elapsed().as_secs_f64() * 1000.0; + assert!(mask.to_interleaved(rect).iter().all(|&b| b == 191)); + black_box(&mask); + println!("tile_write,Gray8 mask,1,{trial},{elapsed:.6}"); + } +} + #[test] fn fill_rect_shares_covered_tiles_and_edits_stay_local() { // A 300000² background used to materialize ~360 GB of tiles (#705). From ecba9b109544e600081332502be891d1b625f535 Mon Sep 17 00:00:00 2001 From: yoriswind <164435053+yoriswind@users.noreply.github.com> Date: Fri, 30 Oct 2026 16:01:36 -0700 Subject: [PATCH 2/3] docs(raster): document fallible tile initialization --- crates/raster/src/lib.rs | 1 + crates/raster/src/tests.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/raster/src/lib.rs b/crates/raster/src/lib.rs index c74951263b..0a7bf3e4fd 100644 --- a/crates/raster/src/lib.rs +++ b/crates/raster/src/lib.rs @@ -60,6 +60,7 @@ pub struct Tile { } impl Tile { + /// Fill a new tile with one encoded pixel, reporting size or allocation failures. fn try_filled(format: &PixelFormat, pixel: &[u8]) -> Result { let n = (TILE_SIZE * TILE_SIZE) as usize; let bytes = n.checked_mul(format.bytes_per_pixel()).ok_or(AllocationError::SizeOverflow)?; diff --git a/crates/raster/src/tests.rs b/crates/raster/src/tests.rs index 6bdccb711d..7c72937c0d 100644 --- a/crates/raster/src/tests.rs +++ b/crates/raster/src/tests.rs @@ -355,6 +355,7 @@ fn fallible_tile_edits_move_weakly_cached_pixels_and_copy_shared_pixels() { } } +/// Check encoded defaults across formats and keep cloned tiles isolated during edits. #[test] fn fallible_default_tiles_preserve_every_encoded_pixel() { for mode in [ From 6ba677e356a74aca1d957c5ef1d4f6733d660216 Mon Sep 17 00:00:00 2001 From: yoriswind <164435053+yoriswind@users.noreply.github.com> Date: Mon, 2 Nov 2026 22:48:37 -0800 Subject: [PATCH 3/3] ci: retrigger checks after contributor approval