mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-12 21:23:02 +00:00
* fix(io): charge importers for the whole tiles surfaces keep * test(io): size the affinity and SVG pixel budgets in whole tiles * address review: charge retained tiles after prune, logical scratch in SVG peak check * address review: charge only the tiles a raster layer keeps * fix PsdImportBudget initializer after merging main * address review: leave the multichannel tile budget to the PSD preflight * address review: charge a shape cache for the tiles it keeps
1344 lines
62 KiB
Rust
1344 lines
62 KiB
Rust
/// Manually compare allocating reads with the previous implementation.
|
||
#[test]
|
||
#[ignore = "manual 24 MP release benchmark"]
|
||
fn benchmark_sparse_reads() {
|
||
use std::{hint::black_box, time::Instant};
|
||
let rect = Rect::new(0, 0, 6000, 4000);
|
||
for depth in [SampleType::U8, SampleType::U16, SampleType::F32] {
|
||
for default in [0.0, 0.25] {
|
||
let mut s = Surface::with_default(PixelFormat::GRAY8.with_sample(depth), &[default]);
|
||
s.fill_rect(Rect::new(257, 257, 266, 266), &[0.75]);
|
||
assert_eq!(s.read_region(rect), previous_read_region(&s, rect));
|
||
for trial in 0..6 {
|
||
let mut paths = [("main", previous_read_region as fn(&Surface, Rect) -> Vec<f32>), ("split", Surface::read_region)];
|
||
if trial % 2 == 1 {
|
||
paths.reverse();
|
||
}
|
||
for (name, path) in paths {
|
||
let t = Instant::now();
|
||
let out = black_box(path)(&s, rect);
|
||
let elapsed = t.elapsed().as_secs_f64() * 1000.0;
|
||
black_box(&out);
|
||
println!("sparse,{depth:?}-{default},{trial},{name},{elapsed:.6}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/// Previous allocating read path, retained as the bitwise-equivalence oracle.
|
||
fn previous_read_region(surface: &Surface, r: Rect) -> Vec<f32> {
|
||
let n = surface.channels();
|
||
let w = r.width() as usize;
|
||
let mut out = vec![0.0f32; w * r.height() as usize * n];
|
||
let dp = surface.default_pixel();
|
||
for tc in r.tiles() {
|
||
let tr = tc.rect().intersect(&r);
|
||
let tile = surface.tiles.get(&tc);
|
||
for y in tr.y0..tr.y1 {
|
||
for x in tr.x0..tr.x1 {
|
||
let o = (((y - r.y0) as usize) * w + (x - r.x0) as usize) * n;
|
||
match tile {
|
||
Some(t) => {
|
||
let base = (((y - tc.ty * TILE_SIZE) as usize) * TILE_SIZE as usize + (x - tc.tx * TILE_SIZE) as usize) * n;
|
||
for i in 0..n {
|
||
out[o + i] = read_sample(&t.bytes(), surface.format.sample, base + i);
|
||
}
|
||
}
|
||
None => out[o..o + n].copy_from_slice(&dp),
|
||
}
|
||
}
|
||
}
|
||
}
|
||
out
|
||
}
|
||
/// Distinct tile allocations in `s`.
|
||
fn distinct_tiles(s: &Surface) -> usize {
|
||
let mut seen: Vec<*const Tile> = s.tiles().map(|(_, t)| Arc::as_ptr(t)).collect();
|
||
seen.sort();
|
||
seen.dedup();
|
||
seen.len()
|
||
}
|
||
|
||
/// Uniform tiles written pixel by pixel end up as one shared tile per value, across
|
||
/// surfaces, with every pixel unchanged; editing one splits it off.
|
||
#[test]
|
||
fn uniform_tiles_are_shared() {
|
||
let mut interner = TileInterner::default();
|
||
let r = Rect::new(0, 0, 1024, 600);
|
||
let mut bytes = Vec::new();
|
||
for y in 0..600 {
|
||
for x in 0..1024 {
|
||
// White, except a gradient in the tiles of column 3.
|
||
bytes.extend(if x >= 768 { [x as u8, y as u8, 0, 255] } else { [255, 255, 255, 255] });
|
||
}
|
||
}
|
||
let mut a = Surface::from_interleaved(PixelFormat::RGBA8, r, &bytes);
|
||
let mut b = Surface::from_interleaved(PixelFormat::RGBA8, r, &bytes);
|
||
let before = a.to_interleaved(r);
|
||
assert_eq!(distinct_tiles(&a), 12);
|
||
// Rows 512..600 cover only part of their tiles: the rest is the default pixel, so those
|
||
// tiles are not uniform.
|
||
let saved = a.share_uniform_tiles(&mut interner);
|
||
assert_eq!(distinct_tiles(&a), 1 + 3 + 3);
|
||
assert_eq!(saved, 5 * TILE_SIZE as usize * TILE_SIZE as usize * 4);
|
||
b.share_uniform_tiles(&mut interner);
|
||
assert_eq!(interner.len(), 1);
|
||
assert!(Arc::ptr_eq(a.tile(TileCoord { tx: 0, ty: 0 }).unwrap(), b.tile(TileCoord { tx: 2, ty: 1 }).unwrap()));
|
||
assert_eq!(a.to_interleaved(r), before);
|
||
a.write_pixel(5, 5, &[0.0, 0.0, 0.0, 1.0]);
|
||
assert_eq!(b.pixel(5, 5), vec![1.0, 1.0, 1.0, 1.0]);
|
||
assert_eq!(a.pixel(5, 5), vec![0.0, 0.0, 0.0, 1.0]);
|
||
assert_eq!(a.pixel(300, 5), vec![1.0, 1.0, 1.0, 1.0]);
|
||
}
|
||
|
||
/// Converting or tone-mapping a surface keeps a shared tile shared (#4531) and gives the
|
||
/// same pixels as converting each pixel: a filled background is one tile across the grid,
|
||
/// plus some distinct tiles.
|
||
#[test]
|
||
fn conversion_keeps_shared_tiles_shared() {
|
||
let mut s = Surface::new(PixelFormat::RGBA8);
|
||
s.fill_rect(Rect::new(0, 0, 2048, 1024), &[0.9, 0.5, 0.2, 1.0]);
|
||
s.fill_rect(Rect::new(10, 10, 20, 20), &[0.1, 0.2, 0.3, 0.5]);
|
||
let shared_before = distinct_tiles(&s);
|
||
assert!(shared_before <= 2 && s.tile_count() == 32, "{shared_before} distinct of {}", s.tile_count());
|
||
let r = Rect::new(0, 0, 2048, 1024);
|
||
for to in [PixelFormat::RGBA16, PixelFormat::RGBA32F] {
|
||
for out in [s.convert(to), s.try_convert(to).expect("memory")] {
|
||
assert_eq!(distinct_tiles(&out), shared_before, "{to:?}: still shared");
|
||
assert_eq!(out.tile_count(), 32);
|
||
let (a, b) = (out.read_region(r), s.read_region(r));
|
||
for (pa, pb) in a.as_chunks::<4>().0.iter().zip(b.as_chunks::<4>().0) {
|
||
let want = convert_pixel(&s.format(), &to, pb);
|
||
assert!(pa.iter().zip(&want).all(|(x, y)| (x - y).abs() < 1e-6), "{to:?}: {pa:?} vs {want:?}");
|
||
}
|
||
}
|
||
}
|
||
let mut m = s.clone();
|
||
m.map_color(|v| v * 0.5);
|
||
assert_eq!(distinct_tiles(&m), shared_before, "map_color keeps it shared");
|
||
assert!((m.pixel(1000, 500)[0] - s.pixel(1000, 500)[0] * 0.5).abs() < 1.0 / 255.0);
|
||
assert_eq!(s.pixel(1000, 500), vec![s.pixel(1000, 500)[0], s.pixel(1000, 500)[1], s.pixel(1000, 500)[2], 1.0], "the source is untouched");
|
||
}
|
||
|
||
use super::*;
|
||
use photocraft_color::PixelFormat;
|
||
use proptest::prelude::*;
|
||
|
||
fn write_region_reference(surface: &mut Surface, r: Rect, data: &[f32]) {
|
||
let n = surface.channels();
|
||
let w = r.width() as usize;
|
||
assert_eq!(data.len(), w * r.height() as usize * n, "region data length mismatch");
|
||
let sample = surface.format.sample;
|
||
for tc in r.tiles() {
|
||
let tr = tc.rect().intersect(&r);
|
||
let t = surface.tile_mut(tc);
|
||
for y in tr.y0..tr.y1 {
|
||
for x in tr.x0..tr.x1 {
|
||
let o = (((y - r.y0) as usize) * w + (x - r.x0) as usize) * n;
|
||
let base = (((y - tc.ty * TILE_SIZE) as usize) * TILE_SIZE as usize + (x - tc.tx * TILE_SIZE) as usize) * n;
|
||
for i in 0..n {
|
||
write_sample(&mut t.bytes_mut(), sample, base + i, data[o + i]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Consumers identify tiles by their `Arc` pointer (e.g. ui-egui's histogram cache keys), so a
|
||
/// write through COW must hand the written copy a *new* tile, never keep the shared pointer:
|
||
/// mutating in place would make two different surfaces hash alike. This pins that invariant
|
||
/// for both `tile_mut` and `try_tile_mut`.
|
||
#[test]
|
||
fn writing_a_shared_surface_replaces_the_tile_not_its_bytes() {
|
||
let fmt = PixelFormat::GRAY8;
|
||
let mut a = Surface::with_default(fmt, &[0.0]);
|
||
a.write_pixel(10, 10, &[0.5]);
|
||
let coord = a.tiles().next().map(|(c, _)| *c).expect("the write allocated a tile");
|
||
let before = Arc::as_ptr(a.tile(coord).expect("tile exists")) as usize;
|
||
for mutate in ["tile_mut", "try_tile_mut"] {
|
||
let mut b = a.clone();
|
||
let ptr = Arc::as_ptr(b.tile(coord).expect("clone shares the tile")) as usize;
|
||
assert_eq!(ptr, before, "{}: the clone shares the tile before writing", mutate);
|
||
match mutate {
|
||
"tile_mut" => b.tile_mut(coord).bytes_mut().fill(255),
|
||
_ => b.try_tile_mut(coord).expect("allocation of a modest tile cannot fail").bytes_mut().fill(128),
|
||
}
|
||
let after = Arc::as_ptr(b.tile(coord).expect("tile exists")) as usize;
|
||
assert_ne!(after, ptr, "{}: a COW write must allocate a new tile", mutate);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn region_writes_match_scalar_bytes_for_every_format() {
|
||
let special = [
|
||
0.0,
|
||
-0.0,
|
||
1.0,
|
||
-1.0,
|
||
2.0,
|
||
f32::INFINITY,
|
||
f32::NEG_INFINITY,
|
||
f32::NAN,
|
||
f32::from_bits(0x7fc12345),
|
||
f32::from_bits(0xffc12345),
|
||
f32::MIN_POSITIVE,
|
||
f32::from_bits(1),
|
||
0.5 / 255.0,
|
||
0.5 / 65535.0,
|
||
];
|
||
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 mut actual = Surface::with_default(fmt, &vec![0.25; fmt.channels()]);
|
||
actual.write_pixel(900, 900, &vec![0.75; fmt.channels()]);
|
||
let mut expected = actual.clone();
|
||
for rect in [Rect::EMPTY, Rect::new(-270, -2, 270, 3), Rect::new(256, 256, 512, 257), Rect::new(-64, -64, 0, 0), Rect::new(255, 250, 258, 270)]
|
||
{
|
||
let data: Vec<_> = (0..rect.width() as usize * rect.height() as usize * fmt.channels())
|
||
.map(|i| {
|
||
if i % 3 == 0 {
|
||
special[i / 3 % special.len()]
|
||
} else {
|
||
// Integer rounding boundaries and deterministic arbitrary f32 bit patterns.
|
||
let bits = (i as u32).wrapping_mul(2654435761);
|
||
if i % 3 == 1 { ((bits % 65536) as f32 + 0.5) / 65535.0 } else { f32::from_bits(bits) }
|
||
}
|
||
})
|
||
.collect();
|
||
let snapshot = actual.clone();
|
||
let snapshot_bytes = snapshot.to_interleaved(rect.inflate(1));
|
||
let mut par = actual.clone();
|
||
actual.write_region(rect, &data);
|
||
par.write_region_par(rect, &data);
|
||
write_region_reference(&mut expected, rect, &data);
|
||
assert_eq!(actual, expected, "{fmt:?}, {rect:?}");
|
||
assert_eq!(par, expected, "parallel {fmt:?}, {rect:?}");
|
||
assert_eq!(snapshot.to_interleaved(rect.inflate(1)), snapshot_bytes, "COW snapshot changed");
|
||
// Repeat against already materialised, unshared tiles too.
|
||
actual.write_region(rect, &data);
|
||
par.write_region_par(rect, &data);
|
||
assert_eq!(actual, expected);
|
||
assert_eq!(par, expected);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Mask builds call `write_region` inside a `OnceLock` initializer, where entering Rayon can
|
||
/// run another tile's job that waits on the same lock. With every global worker held, a write
|
||
/// that queued work on Rayon could not finish.
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
#[test]
|
||
fn write_region_does_not_enter_rayon() {
|
||
use std::sync::{Barrier, mpsc};
|
||
let workers = rayon::current_num_threads();
|
||
let held = Arc::new(Barrier::new(workers + 1));
|
||
let release = Arc::new(Barrier::new(workers + 1));
|
||
let (h, rel) = (held.clone(), release.clone());
|
||
rayon::spawn_broadcast(move |_| {
|
||
h.wait();
|
||
rel.wait();
|
||
});
|
||
held.wait();
|
||
let (tx, rx) = mpsc::channel();
|
||
std::thread::spawn(move || {
|
||
let r = Rect::new(0, 0, 2 * TILE_SIZE, 2 * TILE_SIZE);
|
||
let mut s = Surface::new(PixelFormat::RGBA8);
|
||
s.write_region(r, &vec![0.5; (r.width() * r.height() * 4) as usize]);
|
||
tx.send(()).ok();
|
||
});
|
||
let done = rx.recv_timeout(std::time::Duration::from_secs(5)).is_ok();
|
||
release.wait();
|
||
assert!(done, "write_region waited on the Rayon pool");
|
||
}
|
||
|
||
#[test]
|
||
fn from_rgba_into_matches_from_rgba() {
|
||
for fmt in [PixelFormat::RGBA8, PixelFormat::GRAY8, PixelFormat::GRAYA8, PixelFormat::CMYKA8, PixelFormat::new(ColorMode::Lab, SampleType::F32, true)] {
|
||
let mut buf = [0.0f32; 8];
|
||
let n = from_rgba_into(&fmt, [0.2, 0.5, 0.7, 0.4], &mut buf);
|
||
assert_eq!(&buf[..n], &from_rgba(&fmt, [0.2, 0.5, 0.7, 0.4])[..]);
|
||
}
|
||
}
|
||
|
||
/// 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() {
|
||
for (fmt, default) in [
|
||
PixelFormat::RGBA8,
|
||
PixelFormat::RGBA16,
|
||
PixelFormat::RGBA32F,
|
||
PixelFormat::GRAYA8,
|
||
PixelFormat::CMYKA8,
|
||
PixelFormat::GRAY8,
|
||
PixelFormat::GRAY8.with_sample(SampleType::F32),
|
||
PixelFormat::new(ColorMode::Lab, SampleType::U16, true),
|
||
]
|
||
.into_iter()
|
||
.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<f32> = (0..fmt.channels()).map(|c| ((i * 3 + c) % 5) as f32 / 4.0).collect();
|
||
s.write_pixel(x, y, &px);
|
||
}
|
||
let region = s.read_region(r);
|
||
assert!(region.iter().map(|v| v.to_bits()).eq(previous_read_region(&s, r).into_iter().map(f32::to_bits)));
|
||
let mut into = vec![9.0; 3];
|
||
s.read_region_into(r, &mut into);
|
||
assert_eq!(region, into);
|
||
let mut rgba = vec![[0.0; 4]; (r.width() * r.height()) as usize];
|
||
s.read_rgba_into(r, &mut rgba);
|
||
let mut rgba8 = vec![[0; 4]; rgba.len()];
|
||
s.read_rgba8_into(r, &mut rgba8);
|
||
let n = fmt.channels();
|
||
for (i, p) in region.chunks_exact(n).enumerate() {
|
||
let want = to_rgba(&fmt, p);
|
||
assert_eq!(rgba8[i], want.map(|v| (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u8));
|
||
for c in 0..4 {
|
||
assert!((rgba[i][c] - want[c]).abs() < 1e-6, "{fmt:?} px {i}");
|
||
}
|
||
let (x, y) = (r.x0 + (i % r.width() as usize) as i32, r.y0 + (i / r.width() as usize) as i32);
|
||
for c in 0..n {
|
||
let bits = s.sample_channel(x, y, c).to_bits();
|
||
assert_eq!(bits, p[c].to_bits());
|
||
assert_eq!(bits, into[i * n + c].to_bits());
|
||
}
|
||
}
|
||
assert!(s.has_tiles_in(Rect::new(0, 0, 1, 1)));
|
||
assert!(!s.has_tiles_in(Rect::new(1000, 1000, 1001, 1001)));
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn fallible_region_reads_report_size_overflow() {
|
||
let surface = Surface::new(PixelFormat::RGBA8);
|
||
let huge = Rect::new(i32::MIN, i32::MIN, i32::MAX, i32::MAX);
|
||
assert_eq!(surface.try_read_region(huge), Err(AllocationError::SizeOverflow));
|
||
assert_eq!(surface.try_to_interleaved(huge), Err(AllocationError::SizeOverflow));
|
||
}
|
||
|
||
#[test]
|
||
fn fallible_region_write_rejects_bad_input_without_mutating_surface() {
|
||
let mut surface = Surface::new(PixelFormat::RGBA8);
|
||
let before = surface.clone();
|
||
assert_eq!(surface.try_write_region(Rect::new(0, 0, 2, 2), &[1.0]), Err(AllocationError::SizeOverflow));
|
||
assert_eq!(surface, before);
|
||
}
|
||
|
||
#[test]
|
||
fn fallible_interleaved_write_matches_the_infallible_one_and_rejects_bad_lengths() {
|
||
for fmt in [PixelFormat::RGBA8, PixelFormat::RGBA16, PixelFormat::RGBA32F] {
|
||
// Across tile edges, at a negative origin.
|
||
let r = Rect::new(-3, 250, 300, 517);
|
||
let bytes: Vec<u8> = (0..r.width() as usize * r.height() as usize * fmt.bytes_per_pixel()).map(|i| (i * 31 % 251) as u8).collect();
|
||
let mut a = Surface::new(fmt);
|
||
a.write_interleaved(r, &bytes);
|
||
let mut b = Surface::new(fmt);
|
||
b.try_write_interleaved(r, &bytes).unwrap();
|
||
assert_eq!(a, b);
|
||
let before = b.clone();
|
||
let got = bytes.len() - 1;
|
||
assert_eq!(b.try_write_interleaved(r, &bytes[1..]), Err(AllocationError::LengthMismatch { expected: bytes.len(), got }));
|
||
assert_eq!(b, before);
|
||
// Tiles an undo snapshot shares are copied, not written through.
|
||
let again: Vec<u8> = bytes.iter().map(|v| v ^ 0x5a).collect();
|
||
b.try_write_interleaved(r, &again).unwrap();
|
||
a.write_interleaved(r, &again);
|
||
assert_eq!(a, b);
|
||
assert_eq!(before.to_interleaved(r), bytes);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn fallible_tile_edits_move_weakly_cached_pixels_and_copy_shared_pixels() {
|
||
for fmt in [PixelFormat::RGBA8, PixelFormat::RGBA16, PixelFormat::RGBA32F] {
|
||
let mut surface = Surface::new(fmt);
|
||
let coord = TileCoord::new(0, 0);
|
||
let pixels = surface.try_tile_mut(coord).unwrap().bytes().as_ptr();
|
||
let weak = Arc::downgrade(surface.tile(coord).unwrap());
|
||
let tile = surface.try_tile_mut(coord).unwrap();
|
||
assert_eq!(tile.bytes().as_ptr(), pixels);
|
||
assert!(weak.upgrade().is_none());
|
||
let snapshot = surface.clone();
|
||
surface.try_write_region(Rect::new(0, 0, 1, 1), &[1.0; 4]).unwrap();
|
||
assert_eq!(snapshot.pixel(0, 0), vec![0.0; 4]);
|
||
assert_eq!(surface.pixel(0, 0), vec![1.0; 4]);
|
||
}
|
||
}
|
||
|
||
/// Check encoded defaults across formats and keep cloned tiles isolated during edits.
|
||
#[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.as_slice(), "{fmt:?}, {name}");
|
||
let coord = TileCoord::new(-1, 2);
|
||
assert_eq!(&*surface.try_tile_mut(coord).unwrap().bytes(), expected.as_slice());
|
||
let snapshot = surface.clone();
|
||
surface.try_tile_mut(coord).unwrap().bytes_mut()[0] ^= 0xff;
|
||
assert_eq!(&*snapshot.tile(coord).unwrap().bytes(), expected.as_slice(), "copy-on-write: {fmt:?}");
|
||
assert_ne!(&*surface.tile(coord).unwrap().bytes(), expected.as_slice());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 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<f32> = (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).
|
||
let mut s = Surface::new(PixelFormat::RGBA8);
|
||
s.fill_rect(Rect::new(0, 0, 300_000, 300_000), &[1.0, 0.5, 0.0, 1.0]);
|
||
let t = |s: &Surface, x, y| s.tile(TileCoord::new(x, y)).unwrap().clone();
|
||
assert!(Arc::ptr_eq(&t(&s, 0, 0), &t(&s, 1000, 900)));
|
||
// 300000 is not a multiple of 256: the edge tile is partial and stays its own.
|
||
let edge = 300_000 / TILE_SIZE;
|
||
assert!(!Arc::ptr_eq(&t(&s, 0, 0), &t(&s, edge, 0)));
|
||
assert_eq!(s.pixel(299_999, 299_999), vec![1.0, 128.0 / 255.0, 0.0, 1.0]);
|
||
assert_eq!(s.pixel(300_000, 0), vec![0.0; 4]);
|
||
s.write_pixel(5, 5, &[0.0, 0.0, 1.0, 1.0]);
|
||
assert_eq!(s.pixel(5, 5), vec![0.0, 0.0, 1.0, 1.0]);
|
||
assert_eq!(s.pixel(300, 5), vec![1.0, 128.0 / 255.0, 0.0, 1.0]);
|
||
assert!(!Arc::ptr_eq(&t(&s, 0, 0), &t(&s, 1, 0)));
|
||
}
|
||
|
||
#[test]
|
||
fn empty_surface_reads_default() {
|
||
let s = Surface::new(PixelFormat::RGBA8);
|
||
assert_eq!(s.pixel(10, -500), vec![0.0; 4]);
|
||
let m = Surface::with_default(PixelFormat::GRAY8, &[1.0]);
|
||
assert_eq!(m.pixel(123_456, -9), vec![1.0]);
|
||
assert_eq!(m.tile_count(), 0);
|
||
}
|
||
|
||
#[test]
|
||
fn write_read_pixel_every_depth() {
|
||
for sample in SampleType::ALL {
|
||
let f = PixelFormat::RGBA8.with_sample(sample);
|
||
let mut s = Surface::new(f);
|
||
s.write_pixel(-3, 700, &[0.2, 0.4, 0.6, 1.0]);
|
||
let p = s.pixel(-3, 700);
|
||
for (a, b) in p.iter().zip([0.2, 0.4, 0.6, 1.0]) {
|
||
assert!((a - b).abs() <= 1.0 / 255.0, "{sample:?}");
|
||
}
|
||
assert_eq!(s.tile_count(), 1);
|
||
assert_eq!(s.tile_bounds(), TileCoord::containing(-3, 700).rect());
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn copy_on_write_shares_untouched_tiles() {
|
||
let mut a = Surface::new(PixelFormat::RGBA8);
|
||
a.fill_rect(Rect::new(0, 0, 512, 512), &[1.0, 0.0, 0.0, 1.0]);
|
||
let mut b = a.clone();
|
||
b.write_pixel(10, 10, &[0.0, 1.0, 0.0, 1.0]);
|
||
// a unchanged
|
||
assert_eq!(a.pixel(10, 10), vec![1.0, 0.0, 0.0, 1.0]);
|
||
assert_eq!(b.pixel(10, 10), vec![0.0, 1.0, 0.0, 1.0]);
|
||
// only tile (0,0) was copied
|
||
let shared = a.tiles().filter(|(c, t)| Arc::ptr_eq(t, b.tile(**c).unwrap())).count();
|
||
assert_eq!(shared, 3);
|
||
}
|
||
|
||
#[test]
|
||
fn content_bounds_and_prune() {
|
||
let mut s = Surface::new(PixelFormat::RGBA8);
|
||
s.fill_rect(Rect::new(300, 10, 310, 20), &[1.0, 1.0, 1.0, 1.0]);
|
||
s.write_pixel(-5, -5, &[0.0, 0.0, 0.0, 1.0]);
|
||
assert_eq!(s.content_bounds(), Rect::new(-5, -5, 310, 20));
|
||
// erase the single pixel -> its tile becomes default and is pruned
|
||
s.write_pixel(-5, -5, &[0.0, 0.0, 0.0, 0.0]);
|
||
s.prune();
|
||
assert_eq!(s.tile_count(), 1);
|
||
assert_eq!(s.content_bounds(), Rect::new(300, 10, 310, 20));
|
||
}
|
||
|
||
#[test]
|
||
fn prune_in_batches_drops_what_prune_drops() {
|
||
let mut whole = Surface::new(PixelFormat::RGBA8);
|
||
whole.fill_rect(Rect::new(300, 10, 310, 20), &[1.0, 1.0, 1.0, 1.0]);
|
||
whole.write_pixel(-5, -5, &[0.0, 0.0, 0.0, 1.0]);
|
||
whole.write_pixel(-5, -5, &[0.0, 0.0, 0.0, 0.0]);
|
||
whole.fill_rect(Rect::new(600, 600, 610, 610), &[1.0, 0.0, 0.0, 1.0]);
|
||
let mut stepped = whole.clone();
|
||
whole.prune();
|
||
let mut cursor = stepped.prune_cursor();
|
||
let mut calls = 0;
|
||
while !stepped.prune_batch(&mut cursor, 1) {
|
||
calls += 1;
|
||
assert!(calls < 100, "the prune scan never finished");
|
||
}
|
||
assert!(calls >= 2, "one tile per call visits every tile over several calls");
|
||
assert_eq!(stepped.tile_count(), whole.tile_count());
|
||
assert_eq!(stepped.content_bounds(), whole.content_bounds());
|
||
assert_eq!(stepped.to_interleaved(whole.content_bounds()), whole.to_interleaved(whole.content_bounds()));
|
||
}
|
||
|
||
#[test]
|
||
fn interleaved_roundtrip_crossing_tiles() {
|
||
let r = Rect::new(-20, 250, 280, 262);
|
||
let bpp = PixelFormat::RGBA16.bytes_per_pixel();
|
||
let bytes: Vec<u8> = (0..r.width() as usize * r.height() as usize * bpp).map(|i| (i * 31 % 251) as u8).collect();
|
||
let s = Surface::from_interleaved(PixelFormat::RGBA16, r, &bytes);
|
||
assert_eq!(s.to_interleaved(r), bytes);
|
||
assert_eq!(s.tile_count(), 6); // x tiles -1..=1, y tiles 0..=1
|
||
}
|
||
|
||
#[test]
|
||
fn region_outside_reads_default() {
|
||
let s = Surface::with_default(PixelFormat::GRAY8, &[1.0]);
|
||
let v = s.read_region(Rect::new(0, 0, 3, 3));
|
||
assert_eq!(v, vec![1.0; 9]);
|
||
}
|
||
|
||
#[test]
|
||
fn convert_depths_and_models() {
|
||
let mut s = Surface::new(PixelFormat::RGBA8);
|
||
s.write_pixel(1, 1, &[1.0, 1.0, 1.0, 1.0]);
|
||
let g = s.convert(PixelFormat::GRAYA8);
|
||
assert!((g.pixel(1, 1)[0] - 1.0).abs() < 1e-2);
|
||
let f = s.convert(PixelFormat::RGBA32F);
|
||
assert_eq!(f.pixel(1, 1), vec![1.0, 1.0, 1.0, 1.0]);
|
||
let c = s.convert(PixelFormat::CMYKA8);
|
||
let p = c.pixel(1, 1);
|
||
assert!(p[3] < 0.01 && (p[4] - 1.0).abs() < 1e-6);
|
||
let back = c.convert(PixelFormat::RGBA8);
|
||
assert_eq!(back.pixel(1, 1), vec![1.0, 1.0, 1.0, 1.0]);
|
||
}
|
||
|
||
#[test]
|
||
fn rgba_from_gray_and_cmyk() {
|
||
let mut g = Surface::new(PixelFormat::GRAYA8);
|
||
g.write_pixel(0, 0, &[0.5, 1.0]);
|
||
let v = g.rgba(0, 0);
|
||
assert!((v[0] - v[2]).abs() < 1e-6 && (v[3] - 1.0).abs() < 1e-6);
|
||
}
|
||
|
||
#[test]
|
||
fn repeated_cmyk_reads_match_individual_pixels_bitwise() {
|
||
let rect = Rect::new(-2, -1, 515, 2);
|
||
for depth in SampleType::ALL {
|
||
for alpha in [false, true] {
|
||
let fmt = PixelFormat::new(ColorMode::Cmyk, depth, alpha);
|
||
let mut s = Surface::with_default(fmt, &[0.2, 0.4, 0.6, 0.3, 0.7]);
|
||
for (i, x) in (-1..514).enumerate() {
|
||
let c = if i / 2 % 256 == 0 { -0.0 } else { (i / 2 % 256) as f32 / 255.0 };
|
||
s.write_pixel(x, 0, &[c, 0.4, 0.6, 0.3, (i % 9) as f32 / 8.0]);
|
||
}
|
||
let mut out = vec![[0.0; 4]; (rect.width() * rect.height()) as usize];
|
||
s.read_rgba_into(rect, &mut out);
|
||
for (i, actual) in out.iter().enumerate() {
|
||
let expected = s.rgba(rect.x0 + (i % rect.width() as usize) as i32, rect.y0 + (i / rect.width() as usize) as i32);
|
||
assert_eq!(actual.map(f32::to_bits), expected.map(f32::to_bits), "{depth:?}, alpha={alpha}, pixel={i}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn rgba_reads_match_individual_pixels_bitwise_in_every_model_and_depth() {
|
||
// Across tile edges and a missing tile; float samples include -0, out-of-range and NaN.
|
||
let rect = Rect::new(-3, -1, 300, 2);
|
||
for mode in [ColorMode::Rgb, ColorMode::Grayscale, ColorMode::Lab, ColorMode::Cmyk] {
|
||
for depth in SampleType::ALL {
|
||
for alpha in [false, true] {
|
||
let fmt = PixelFormat::new(mode, depth, alpha);
|
||
let mut s = Surface::with_default(fmt, &vec![0.25; fmt.channels()]);
|
||
let values = [-0.0, 0.1, 0.5, 1.0, 1.5, -0.25, f32::NAN, 0.75];
|
||
for (i, x) in (-2..260).enumerate() {
|
||
let px: Vec<f32> = (0..fmt.channels()).map(|c| values[(i + c * 3) % values.len()]).collect();
|
||
s.write_pixel(x, 0, &px);
|
||
}
|
||
let mut out = vec![[0.0; 4]; (rect.width() * rect.height()) as usize];
|
||
s.read_rgba_into(rect, &mut out);
|
||
for (i, actual) in out.iter().enumerate() {
|
||
let expected = s.rgba(rect.x0 + (i % rect.width() as usize) as i32, rect.y0 + (i / rect.width() as usize) as i32);
|
||
assert_eq!(actual.map(f32::to_bits), expected.map(f32::to_bits), "{fmt:?}, pixel={i}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn tile_content_bounds_scans_against_the_given_pixel() {
|
||
let mut s = Surface::new(PixelFormat::GRAY8);
|
||
s.write_pixel(3, 4, &[1.0]);
|
||
s.write_pixel(200, 9, &[1.0]);
|
||
let t = s.tile(TileCoord::new(0, 0)).expect("written tile");
|
||
assert_eq!(t.content_bounds(&[0]), Some((3, 4, 201, 10)));
|
||
assert_eq!(t.content_bounds(&[255]).map(|b| (b.0, b.1)), Some((0, 0)));
|
||
// A malformed pixel is no content, not a panic.
|
||
assert_eq!(t.content_bounds(&[]), None);
|
||
}
|
||
|
||
#[test]
|
||
fn tile_content_bounds_match_a_per_pixel_scan() {
|
||
// One differing byte at each position of a pixel, at and around the compare-block
|
||
// boundaries, in every pixel size; and a pixel that differs from `dp` in every byte.
|
||
fn per_pixel(t: &Tile, dp: &[u8]) -> Option<(u16, u16, u16, u16)> {
|
||
let (bpp, ts) = (dp.len(), TILE_SIZE as usize);
|
||
let mut b: Option<(usize, usize, usize, usize)> = None;
|
||
for (i, px) in t.bytes().chunks_exact(bpp).enumerate() {
|
||
if px != dp {
|
||
let (x, y) = (i % ts, i / ts);
|
||
let (x0, y0, x1, y1) = b.unwrap_or((x, y, x, y));
|
||
b = Some((x0.min(x), y0.min(y), x1.max(x), y1.max(y)));
|
||
}
|
||
}
|
||
b.map(|(x0, y0, x1, y1)| (x0 as u16, y0 as u16, x1 as u16 + 1, y1 as u16 + 1))
|
||
}
|
||
for bpp in [1, 2, 3, 4, 6, 8, 16] {
|
||
let dp: Vec<u8> = (0..bpp).map(|i| (i * 37 + 5) as u8).collect();
|
||
let row = TILE_SIZE as usize * bpp;
|
||
for spots in [vec![], vec![0], vec![row - 1], vec![63, 64], vec![row * 7 + 127, row * 7 + 128, row * 200 + 5], vec![row * 255 + row / 2]] {
|
||
for byte in 0..bpp {
|
||
let mut data = dp.repeat(TILE_SIZE as usize * TILE_SIZE as usize);
|
||
for &s in &spots {
|
||
data[s - s % bpp + byte] ^= 0x5a;
|
||
}
|
||
let t = Tile::from_vec(data);
|
||
assert_eq!(t.content_bounds(&dp), per_pixel(&t, &dp), "bpp {bpp}, spots {spots:?}, byte {byte}");
|
||
}
|
||
}
|
||
let mut data = dp.repeat(TILE_SIZE as usize * TILE_SIZE as usize);
|
||
data[row * 3 + 9 * bpp..row * 3 + 10 * bpp].iter_mut().for_each(|b| *b = !*b);
|
||
let t = Tile::from_vec(data);
|
||
assert_eq!(t.content_bounds(&dp), Some((9, 3, 10, 4)), "bpp {bpp}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn translated_matches_reading_the_moved_pixels() {
|
||
// Any offset (whole tiles, negative, across tile edges), every depth, a non-zero default.
|
||
for fmt in [PixelFormat::RGBA8, PixelFormat::RGBA16, PixelFormat::RGBA32F, PixelFormat::GRAY8] {
|
||
let mut s = Surface::with_default(fmt, &vec![0.25; fmt.channels()]);
|
||
let r = Rect::new(-37, 200, 530, 300);
|
||
let v: Vec<f32> = (0..r.width() * r.height() * fmt.channels() as u32).map(|i| (i % 251) as f32 / 250.0).collect();
|
||
s.write_region(r, &v);
|
||
for (dx, dy) in [(0, 0), (256, -512), (1, 0), (-3, 7), (300, -257), (-1000, 999)] {
|
||
let t = s.translated(dx, dy, s.content_bounds());
|
||
let moved = r.translate(dx, dy).inflate(3);
|
||
assert_eq!(t.read_region(moved), s.read_region(moved.translate(-dx, -dy)), "{fmt:?} by ({dx}, {dy})");
|
||
assert_eq!(t.default_pixel(), s.default_pixel());
|
||
assert_eq!(t.to_interleaved(r.translate(dx, dy)), s.to_interleaved(r), "{fmt:?} by ({dx}, {dy}): not an exact copy");
|
||
}
|
||
// Whole tiles: the tiles are shared, not copied.
|
||
let t = s.translated(512, -256, s.content_bounds());
|
||
assert!(s.tiles().zip(t.tiles()).all(|((_, a), (_, b))| Arc::ptr_eq(a, b)));
|
||
// Tiles without content (allocated, default pixels) are left behind.
|
||
let mut padded = s.clone();
|
||
padded.fill_rect(Rect::new(2000, 2000, 2100, 2100), &s.default_pixel());
|
||
assert_eq!(padded.translated(5, 5, s.content_bounds()).tile_count(), s.translated(5, 5, s.content_bounds()).tile_count());
|
||
}
|
||
// A solid fill moved off the tile grid keeps sharing its one tile inside, at every depth
|
||
// and over a non-zero default pixel, and reads exactly like the copied pixels.
|
||
for fmt in [PixelFormat::RGBA8, PixelFormat::RGBA16, PixelFormat::RGBA32F, PixelFormat::GRAY8] {
|
||
let mut s = Surface::with_default(fmt, &vec![0.25; fmt.channels()]);
|
||
let r = Rect::new(-300, -10, 1300, 1030);
|
||
s.fill_rect(r, &vec![0.75; fmt.channels()]);
|
||
let fill = s.tile(TileCoord::new(1, 1)).cloned().expect("an inner fill tile");
|
||
for (dx, dy) in [(10, 7), (-1, 0), (0, 300), (-257, 513)] {
|
||
let t = s.translated(dx, dy, s.content_bounds());
|
||
let moved = r.translate(dx, dy);
|
||
assert_eq!(t.read_region(moved.inflate(3)), s.read_region(moved.inflate(3).translate(-dx, -dy)), "{fmt:?} by ({dx}, {dy})");
|
||
// Targets read only from the fill's whole tiles (x -256..1280, y 0..1024).
|
||
let whole = Rect::new(-256, 0, 1280, 1024).translate(dx, dy);
|
||
let inner: Vec<_> = t.tiles().filter(|(c, _)| whole.contains_rect(&c.rect())).collect();
|
||
assert!(!inner.is_empty() && inner.iter().all(|(_, a)| Arc::ptr_eq(a, &fill)), "{fmt:?} by ({dx}, {dy}): inner tiles copied");
|
||
}
|
||
// Nudged back and forth, the inside keeps sharing one tile (copies of uniform edge
|
||
// tiles count as that value too) and the pixels stay exact.
|
||
let mut t = s.clone();
|
||
for k in 0..8 {
|
||
let d = if k % 2 == 0 { 10 } else { -10 };
|
||
t = t.translated(d, d / 2, t.content_bounds());
|
||
}
|
||
// (Targets whose last window read uniform tiles only: one tile in from the fill's whole
|
||
// tiles on the sides the last move came from.)
|
||
let inner: Vec<_> = t.tiles().filter(|(c, _)| Rect::new(-256, 0, 1024, 768).contains_rect(&c.rect())).map(|(_, a)| a).collect();
|
||
assert!(inner.len() >= 4 && inner.iter().all(|a| Arc::ptr_eq(a, inner[0])), "{fmt:?}: repeated moves copied the inside");
|
||
assert_eq!(t.read_region(r.inflate(3)), s.read_region(r.inflate(3)), "{fmt:?}: back where it started");
|
||
}
|
||
// Two solid fills of different values side by side: windows across the seam are
|
||
// copied (each tile is uniform, but not with the same value), the rest shared.
|
||
for fmt in [PixelFormat::RGBA8, PixelFormat::RGBA32F] {
|
||
let mut s = Surface::new(fmt);
|
||
s.fill_rect(Rect::new(0, 0, 1024, 768), &vec![0.2; fmt.channels()]);
|
||
s.fill_rect(Rect::new(1024, 0, 2048, 768), &vec![0.9; fmt.channels()]);
|
||
let t = s.translated(10, 7, s.content_bounds());
|
||
let all = Rect::new(-20, -20, 2100, 800);
|
||
assert_eq!(t.read_region(all), s.read_region(all.translate(-10, -7)), "{fmt:?}: two fills");
|
||
let seam = t.tile(TileCoord::new(4, 1)).expect("seam tile");
|
||
let (left, right) = (t.tile(TileCoord::new(2, 1)).expect("left"), t.tile(TileCoord::new(6, 1)).expect("right"));
|
||
assert!(!Arc::ptr_eq(seam, left) && !Arc::ptr_eq(seam, right) && !Arc::ptr_eq(left, right), "{fmt:?}: seam aliased");
|
||
}
|
||
// An offset of i32::MIN saturates its negation, so the source window isn't the exact
|
||
// preimage: no shortcut may apply (a zero pre-fill would leak into the output).
|
||
let mut s = Surface::with_default(PixelFormat::GRAY8, &[0.5]);
|
||
s.fill_rect(Rect::new(i32::MAX - 1024, 0, i32::MAX - 512, 512), &[0.25]);
|
||
let (fill, default) = (s.sample_channel(i32::MAX - 600, 10, 0), s.default_pixel()[0]);
|
||
for (dx, dy) in [(i32::MIN, 1), (1, i32::MIN), (i32::MIN, 0)] {
|
||
let t = s.translated(dx, dy, s.content_bounds());
|
||
for (c, _) in t.tiles() {
|
||
assert!(t.read_region(c.rect()).iter().all(|v| *v == fill || *v == default), "({dx}, {dy}): tile {c:?} holds a pre-fill");
|
||
}
|
||
}
|
||
// One tile shared but not uniform: each target is copied, not aliased.
|
||
let mut s = Surface::new(PixelFormat::GRAY8);
|
||
s.write_pixel(3, 4, &[1.0]);
|
||
let odd = s.tile(TileCoord::new(0, 0)).cloned().expect("written tile");
|
||
let grid: Vec<_> = (0..9).map(|i| (TileCoord::new(i % 3, i / 3), odd.clone())).collect();
|
||
s.put_tiles(grid);
|
||
let t = s.translated(10, 7, s.content_bounds());
|
||
let all = Rect::new(-20, -20, 800, 800);
|
||
assert_eq!(t.read_region(all), s.read_region(all.translate(-10, -7)));
|
||
assert!(t.tiles().all(|(_, a)| !Arc::ptr_eq(a, &odd)));
|
||
// Offsets at the limits don't panic.
|
||
let mut s = Surface::new(PixelFormat::GRAY8);
|
||
s.write_pixel(5, 5, &[1.0]);
|
||
let all = Rect::new(i32::MIN, i32::MIN, i32::MAX, i32::MAX);
|
||
let _ = s.translated(i32::MAX, i32::MIN, all);
|
||
let _ = s.translated(i32::MIN + 1, 3, all);
|
||
}
|
||
|
||
/// Runs `steps` to the end with `more` always false, counting the calls it took.
|
||
fn run_steps(mut steps: TranslateSteps) -> (Surface, usize) {
|
||
for calls in 1..=1_000_000 {
|
||
if let Some(out) = steps.step(&mut || false) {
|
||
return (out, calls);
|
||
}
|
||
}
|
||
panic!("the shift never finished")
|
||
}
|
||
|
||
/// A shift in pieces gives the surface of the one-shot shift, bit for bit, at every depth and
|
||
/// offset (whole tiles, off the grid, negative), over a non-zero default pixel, solid fills and
|
||
/// sparse tiles.
|
||
#[test]
|
||
fn translate_steps_equal_translated() {
|
||
for fmt in [PixelFormat::RGBA8, PixelFormat::RGBA16, PixelFormat::RGBA32F, PixelFormat::GRAY8] {
|
||
let mut s = Surface::with_default(fmt, &vec![0.25; fmt.channels()]);
|
||
let r = Rect::new(-37, 200, 530, 300);
|
||
let v: Vec<f32> = (0..r.width() * r.height() * fmt.channels() as u32).map(|i| (i % 251) as f32 / 250.0).collect();
|
||
s.write_region(r, &v);
|
||
s.fill_rect(Rect::new(600, -300, 1700, 900), &vec![0.75; fmt.channels()]);
|
||
s.write_pixel(3000, 5, &vec![0.5; fmt.channels()]);
|
||
for (dx, dy) in [(0, 0), (256, -512), (1, 0), (-3, 7), (300, -257), (-1000, 999), (10, 7)] {
|
||
let content = s.content_bounds();
|
||
let (got, _) = run_steps(TranslateSteps::new(s.clone(), dx, dy, content));
|
||
assert!(got == s.translated(dx, dy, content), "{fmt:?} by ({dx}, {dy})");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Each call of a shift in pieces builds at most one tile, so a large shift takes as many calls
|
||
/// as it has source and target tiles, and none of them rebuilds everything.
|
||
#[test]
|
||
fn translate_steps_do_one_tile_per_call() {
|
||
let mut s = Surface::new(PixelFormat::RGBA8);
|
||
s.fill_rect(Rect::new(0, 0, 3 * TILE_SIZE, 2 * TILE_SIZE), &[0.1, 0.2, 0.3, 1.0]);
|
||
s.write_pixel(5, 5, &[1.0, 0.0, 0.0, 1.0]);
|
||
let content = s.content_bounds();
|
||
let sources = s.tiles().count();
|
||
let targets = s.translated(7, 3, content).tiles().count();
|
||
let (got, calls) = run_steps(TranslateSteps::new(s.clone(), 7, 3, content));
|
||
assert!(got == s.translated(7, 3, content));
|
||
// One call per source tile scanned, one to start building, one per target tile built (a few
|
||
// of them hold only the default pixel and are not kept) and one that hands the result over.
|
||
assert!(calls >= sources + targets + 2, "{calls} calls for {sources} sources and {targets} targets");
|
||
assert!(calls <= sources + targets + 2 + 8, "{calls} calls for {sources} sources and {targets} targets");
|
||
// Asked to go on, a call does more than one unit.
|
||
let mut steps = TranslateSteps::new(s.clone(), 7, 3, content);
|
||
let mut left = 4;
|
||
assert!(
|
||
steps
|
||
.step(&mut || {
|
||
left -= 1;
|
||
left > 0
|
||
})
|
||
.is_none()
|
||
);
|
||
assert!(steps.progress() > 0.0 && steps.progress() < 1.0);
|
||
}
|
||
|
||
/// Many references to one payload, moved off the tile grid: the rebuilt tiles that read the
|
||
/// same source tiles at the same offset are one tile, not a copy each.
|
||
#[test]
|
||
fn translated_shares_tiles_rebuilt_from_shared_sources() {
|
||
let fmt = PixelFormat::RGBA8;
|
||
let mut one = Surface::new(fmt);
|
||
let r = Rect::new(0, 0, TILE_SIZE, TILE_SIZE);
|
||
let px: Vec<f32> = (0..r.width() * r.height()).flat_map(|i| [(i % 251) as f32 / 255.0, (i % 7) as f32 / 7.0, 0.5, 1.0]).collect();
|
||
one.write_region(r, &px);
|
||
let payload = Arc::clone(one.tile(TileCoord::new(0, 0)).unwrap());
|
||
let mut s = Surface::new(fmt);
|
||
s.put_tiles((0..16).flat_map(|ty| (0..16).map(move |tx| TileCoord::new(tx, ty))).map(|c| (c, Arc::clone(&payload))));
|
||
let moved = s.translated(7, 5, s.content_bounds());
|
||
let distinct: std::collections::HashSet<_> = moved.tiles().map(|(_, t)| Arc::as_ptr(t)).collect();
|
||
// The 15 × 15 interior tiles read the same four source tiles at the same offset, so they are
|
||
// one tile, however many threads build them; only the 64 border tiles (cut by the
|
||
// content's edge) are built one by one.
|
||
assert!(moved.tile_count() == 289 && distinct.len() == 64 + 1, "{} tiles, {} distinct", moved.tile_count(), distinct.len());
|
||
let all = Rect::new(-50, -50, 16 * TILE_SIZE + 50, 16 * TILE_SIZE + 50);
|
||
assert_eq!(moved.read_region(all), s.read_region(all.translate(-7, -5)));
|
||
}
|
||
|
||
#[test]
|
||
fn a_surface_bounds_tile_count_is_counted_not_walked() {
|
||
let mut s = Surface::new(PixelFormat::RGBA8);
|
||
let far = Rect::new(0, 0, i32::MAX, i32::MAX);
|
||
assert_eq!(far.tile_count(), u64::from(i32::MAX as u32 / TILE_SIZE as u32 + 1).pow(2));
|
||
// Not allocated: 2^46 entries is far past any reservation.
|
||
assert!(s.try_take_tiles(far).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn translated_stores_no_tiles_of_only_the_default_pixel() {
|
||
let only_default = |t: &Surface| {
|
||
let dp = t.default_pixel();
|
||
t.tiles().filter(|(c, _)| t.read_region(c.rect()).chunks_exact(dp.len()).all(|p| p == dp.as_slice())).count()
|
||
};
|
||
// Two blocks a few tiles apart, moved off the tile grid and back: the default-pixel edges
|
||
// of their tiles land on tiles of their own, which aren't stored.
|
||
for fmt in [PixelFormat::RGBA8, PixelFormat::RGBA16, PixelFormat::RGBA32F, PixelFormat::GRAY8] {
|
||
for default in [0.0, 0.25] {
|
||
let mut s = Surface::with_default(fmt, &vec![default; fmt.channels()]);
|
||
s.fill_rect(Rect::new(0, 0, 300, 300), &vec![1.0; fmt.channels()]);
|
||
s.fill_rect(Rect::new(2000, 1000, 2300, 1300), &vec![0.5; fmt.channels()]);
|
||
let all = Rect::new(-600, -600, 3000, 2000);
|
||
let mut t = s.clone();
|
||
for (dx, dy) in [(100, 56), (-100, -56), (10, 7), (-3, 40), (130, -90), (-137, 43)] {
|
||
let moved = t.translated(dx, dy, t.content_bounds());
|
||
assert_eq!(moved.read_region(all), t.read_region(all.translate(-dx, -dy)), "{fmt:?} over {default} by ({dx}, {dy})");
|
||
assert_eq!(only_default(&moved), 0, "{fmt:?} over {default} by ({dx}, {dy})");
|
||
t = moved;
|
||
}
|
||
assert_eq!(t.read_region(all), s.read_region(all), "{fmt:?} over {default}: back where it started");
|
||
assert_eq!(t.tile_count(), s.tile_count(), "{fmt:?} over {default}: back where it started");
|
||
}
|
||
}
|
||
// Stored tiles of the default pixel inside `content`: the targets that would share one of
|
||
// them (#4414's uniform windows) are left out too.
|
||
let mut s = Surface::with_default(PixelFormat::GRAY8, &[0.25]);
|
||
s.fill_rect(Rect::new(0, 0, 1024, 1024), &[0.25]);
|
||
s.write_pixel(600, 600, &[1.0]);
|
||
let content = Rect::new(0, 0, 1024, 1024);
|
||
let t = s.translated(10, 7, content);
|
||
assert_eq!(only_default(&t), 0);
|
||
assert_eq!(t.tile_count(), 1);
|
||
assert_eq!(t.read_region(Rect::new(-20, -20, 1100, 1100)), s.read_region(Rect::new(-30, -27, 1090, 1093)));
|
||
}
|
||
|
||
proptest! {
|
||
#[test]
|
||
fn region_write_read_roundtrip(x in -600i32..600, y in -600i32..600, w in 1u32..90, h in 1u32..90, seed in any::<u32>()) {
|
||
let r = Rect::from_xywh(x, y, w, h);
|
||
let n = 4usize;
|
||
let data: Vec<f32> = (0..(w * h) as usize * n).map(|i| ((i as u32).wrapping_mul(2654435761).wrapping_add(seed) % 256) as f32 / 255.0).collect();
|
||
let mut s = Surface::new(PixelFormat::RGBA8);
|
||
s.write_region(r, &data);
|
||
let back = s.read_region(r);
|
||
for (a, b) in back.iter().zip(&data) {
|
||
prop_assert!((a - b).abs() < 1e-6);
|
||
}
|
||
prop_assert_eq!(s.content_bounds().union(&r), r);
|
||
}
|
||
|
||
#[test]
|
||
fn clone_mutation_isolated(x in -300i32..300, y in -300i32..300) {
|
||
let mut a = Surface::new(PixelFormat::RGBA8);
|
||
a.fill_rect(Rect::new(-256, -256, 256, 256), &[0.5, 0.5, 0.5, 1.0]);
|
||
let snapshot = a.clone();
|
||
a.write_pixel(x, y, &[1.0, 0.0, 0.0, 1.0]);
|
||
let expect = if Rect::new(-256, -256, 256, 256).contains(x, y) { vec![0.5f32, 0.5, 0.5, 1.0] } else { vec![0.0; 4] };
|
||
let got = snapshot.pixel(x, y);
|
||
for (g, e) in got.iter().zip(&expect) {
|
||
prop_assert!((g - e).abs() <= 1.0 / 255.0);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn strided_row_sampling_handles_extreme_coordinates_and_steps() {
|
||
for sample in SampleType::ALL {
|
||
let mut s = Surface::with_default(PixelFormat::new(ColorMode::Grayscale, sample, false), &[0.25]);
|
||
for (x, value) in [(i32::MIN, 0.1), (-1, 0.3), (0, 0.4), (i32::MAX - 1, 0.6), (i32::MAX, 0.9)] {
|
||
s.write_pixel(x, 0, &[value]);
|
||
}
|
||
for y in [0, 256] {
|
||
for (x0, step, len) in [(i32::MAX, 1, 1), (i32::MIN, i32::MAX, 3), (0, i32::MAX, 2), (i32::MAX - 1, 1, 2), (i32::MAX, i32::MAX, 0)] {
|
||
let mut out = vec![0.0; len];
|
||
s.sample_row_strided(y, x0, step, 0, &mut out);
|
||
for (i, &got) in out.iter().enumerate() {
|
||
let x = i32::try_from(i64::from(x0) + i as i64 * i64::from(step)).unwrap();
|
||
assert_eq!(got, s.sample_channel(x, y, 0), "{sample:?} y={y} x0={x0} step={step} i={i}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn strided_row_sampling_uses_default_past_coordinate_range() {
|
||
for sample in SampleType::ALL {
|
||
let mut s = Surface::with_default(PixelFormat::new(ColorMode::Grayscale, sample, false), &[0.25]);
|
||
s.write_pixel(i32::MAX, 0, &[0.75]);
|
||
s.write_pixel(i32::MIN, 0, &[1.0]);
|
||
for step in [1, i32::MAX] {
|
||
let mut out = [0.0; 4];
|
||
s.sample_row_strided(0, i32::MAX, step, 0, &mut out);
|
||
assert_eq!(out, [s.sample_channel(i32::MAX, 0, 0), s.default_pixel()[0], s.default_pixel()[0], s.default_pixel()[0]]);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn strided_row_sampling_matches_sample_channel() {
|
||
let mut s = Surface::with_default(PixelFormat::GRAY8, &[0.25]);
|
||
s.fill_rect(Rect::new(-300, 10, 200, 40), &[1.0]);
|
||
s.fill_rect(Rect::new(250, 10, 700, 20), &[0.5]);
|
||
for (y, x0, step) in [(15, -520, 7), (15, -1, 1), (30, 3, 64), (500, 0, 9)] {
|
||
let mut out = vec![0.0; 120];
|
||
s.sample_row_strided(y, x0, step, 0, &mut out);
|
||
for (i, v) in out.iter().enumerate() {
|
||
assert_eq!(*v, s.sample_channel(x0 + i as i32 * step, y, 0), "y={y} x0={x0} step={step} i={i}");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn content_bounds_with_tile_skipping() {
|
||
// Interior content only; ring tiles present but all-default (unpruned).
|
||
let mut s = Surface::new(PixelFormat::GRAY8);
|
||
s.fill_rect(Rect::new(0, 0, 1024, 1024), &[0.0]);
|
||
s.fill_rect(Rect::new(300, 310, 700, 720), &[1.0]);
|
||
assert_eq!(s.content_bounds(), Rect::new(300, 310, 700, 720));
|
||
// Full-canvas content plus a far-away speck.
|
||
let mut s = Surface::new(PixelFormat::GRAY8);
|
||
s.fill_rect(Rect::new(0, 0, 2000, 1500), &[1.0]);
|
||
s.fill_rect(Rect::new(-700, 4000, -699, 4001), &[0.5]);
|
||
assert_eq!(s.content_bounds(), Rect::new(-700, 0, 2000, 4001));
|
||
}
|
||
|
||
#[test]
|
||
fn fallible_interleaved_writes_match_encoded_pixels_and_reject_bad_lengths() {
|
||
for sample in [SampleType::U8, SampleType::U16, SampleType::F32] {
|
||
let format = PixelFormat::new(ColorMode::Rgb, sample, true);
|
||
let rect = Rect::new(-2, -1, 3, 2);
|
||
let bytes: Vec<u8> = (0..rect.width() as usize * rect.height() as usize * format.bytes_per_pixel()).map(|i| (i % 251) as u8).collect();
|
||
let mut surface = Surface::new(format);
|
||
assert!(surface.try_write_interleaved(rect, &bytes[..bytes.len() - 1]).is_err());
|
||
assert!(surface.tiles.is_empty());
|
||
surface.try_write_interleaved(rect, &bytes).unwrap();
|
||
assert_eq!(surface.to_interleaved(rect), bytes);
|
||
let mut shared = surface.clone();
|
||
let replacement = vec![0; bytes.len()];
|
||
shared.try_write_interleaved(rect, &replacement).unwrap();
|
||
assert_eq!(surface.to_interleaved(rect), bytes);
|
||
assert_eq!(shared.to_interleaved(rect), replacement);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn tile_storage_counts_whole_tiles() {
|
||
let fmt = PixelFormat::RGBA8;
|
||
let tile = (TILE_SIZE as u64).pow(2) * 4;
|
||
assert_eq!(tile_storage_bytes(Rect::new(0, 0, 1, 1), &fmt), tile);
|
||
// A one-pixel column down four tiles keeps four whole tiles; straddling an edge, five.
|
||
assert_eq!(tile_storage_bytes(Rect::new(0, 0, 1, TILE_SIZE * 4), &fmt), 4 * tile);
|
||
assert_eq!(tile_storage_bytes(Rect::new(0, -1, 1, TILE_SIZE * 4), &fmt), 5 * tile);
|
||
assert_eq!(tile_storage_bytes(Rect::EMPTY, &fmt), 0);
|
||
}
|
||
|
||
/// The byte a row-callback test writes at pixel (x, y).
|
||
fn row_byte(x: i32, y: i32) -> u8 {
|
||
(x.wrapping_mul(7).wrapping_add(y.wrapping_mul(13)) & 0xff) as u8
|
||
}
|
||
|
||
fn fill_row_bytes(y: i32, x0: i32, dst: &mut [u8]) {
|
||
for (i, b) in dst.iter_mut().enumerate() {
|
||
*b = row_byte(x0 + i as i32, y);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn write_rows_empty_rect_writes_nothing() {
|
||
let mut s = Surface::new(PixelFormat::GRAY8);
|
||
let called = std::sync::atomic::AtomicBool::new(false);
|
||
for r in [Rect::new(5, 5, 5, 9), Rect::new(0, 0, 10, 0), Rect::new(10, 10, 2, 2), Rect::EMPTY] {
|
||
s.try_write_rows_with(r, |_, _, _| called.store(true, std::sync::atomic::Ordering::Relaxed)).expect("empty is fine");
|
||
}
|
||
assert!(!called.load(std::sync::atomic::Ordering::Relaxed), "no rows in an empty rect");
|
||
assert_eq!(s.tile_count(), 0);
|
||
}
|
||
|
||
#[test]
|
||
fn write_rows_overflowing_rect_is_an_error_not_a_panic() {
|
||
let mut s = Surface::new(PixelFormat::GRAY8);
|
||
let r = Rect::new(0, 0, 1, 1);
|
||
s.try_write_rows_with(r, |_, _, dst| dst.fill(42)).unwrap();
|
||
let before = s.clone();
|
||
let huge = Rect::new(i32::MIN, i32::MIN, i32::MAX, i32::MAX);
|
||
assert!(s.try_write_rows_with(huge, |_, _, _| {}).is_err());
|
||
assert_eq!(s.tile_count(), 1);
|
||
assert_eq!(s.to_interleaved(r), vec![42]);
|
||
assert!(std::sync::Arc::ptr_eq(s.tiles.values().next().unwrap(), before.tiles.values().next().unwrap()));
|
||
}
|
||
|
||
#[test]
|
||
fn write_rows_negative_origin_and_unaligned_rect() {
|
||
let mut s = Surface::with_default(PixelFormat::GRAY8, &[0.5]);
|
||
let r = Rect::new(-300, -5, 270, 260);
|
||
s.try_write_rows_with(r, fill_row_bytes).expect("memory");
|
||
let got = s.to_interleaved(Rect::new(-320, -10, 300, 270));
|
||
let w = 620;
|
||
for (i, &b) in got.iter().enumerate() {
|
||
let (x, y) = (-320 + (i % w) as i32, -10 + (i / w) as i32);
|
||
let want = if x >= r.x0 && x < r.x1 && y >= r.y0 && y < r.y1 { row_byte(x, y) } else { 128 };
|
||
assert_eq!(b, want, "({x}, {y})");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn write_rows_matches_write_interleaved_at_every_depth() {
|
||
for fmt in [PixelFormat::RGBA8, PixelFormat::RGBA16, PixelFormat::RGBA32F, PixelFormat::GRAY8] {
|
||
let bpp = fmt.bytes_per_pixel();
|
||
let r = Rect::new(3, 2, 520, 300);
|
||
let w = r.width() as usize;
|
||
let bytes: Vec<u8> = (0..w * r.height() as usize * bpp).map(|i| (i % 251) as u8).collect();
|
||
let mut a = Surface::new(fmt);
|
||
a.write_interleaved(r, &bytes);
|
||
let mut b = Surface::new(fmt);
|
||
b.try_write_rows_with(r, |y, x0, dst| {
|
||
let o = (((y - r.y0) as usize) * w + (x0 - r.x0) as usize) * bpp;
|
||
dst.copy_from_slice(&bytes[o..o + dst.len()]);
|
||
})
|
||
.expect("memory");
|
||
assert_eq!(a.tile_count(), b.tile_count());
|
||
for (c, t) in a.tiles() {
|
||
assert!(b.tiles().any(|(c2, t2)| c2 == c && t2 == t), "{fmt:?}: tile {c:?} differs");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn write_rows_copies_tiles_a_clone_still_shares() {
|
||
let mut s = Surface::new(PixelFormat::GRAY8);
|
||
s.try_write_rows_with(Rect::new(0, 0, 300, 300), |_, _, dst| dst.fill(9)).expect("memory");
|
||
let before = s.clone();
|
||
let snapshot = before.to_interleaved(Rect::new(0, 0, 300, 300));
|
||
s.try_write_rows_with(Rect::new(100, 100, 280, 290), fill_row_bytes).expect("memory");
|
||
assert_eq!(before.to_interleaved(Rect::new(0, 0, 300, 300)), snapshot, "the clone is unchanged");
|
||
assert_eq!(s.to_interleaved(Rect::new(150, 150, 151, 151)), vec![row_byte(150, 150)]);
|
||
assert_eq!(s.to_interleaved(Rect::new(10, 10, 11, 11)), vec![9], "pixels outside the rect keep their value");
|
||
}
|
||
|
||
#[test]
|
||
fn failed_row_preparation_preserves_existing_tiles() {
|
||
let mut surface = Surface::new(PixelFormat::GRAY8);
|
||
let r = Rect::new(0, 0, 300, 1);
|
||
surface.try_write_rows_with(Rect::new(0, 0, 1, 1), |_, _, dst| dst.fill(42)).unwrap();
|
||
let before = surface.clone();
|
||
let result = surface.try_prepare_rows(r, |tiles, _, _| {
|
||
fill_tile(&mut tiles[0], r, &PixelFormat::GRAY8, &[0], &|_, _, dst: &mut [u8]| dst.fill(7), &|| Ok::<(), AllocationError>(()))?;
|
||
Err(AllocationError::NotEnoughMemory { bytes: 1 })
|
||
});
|
||
assert!(matches!(result, Err(AllocationError::NotEnoughMemory { .. })));
|
||
assert_eq!(surface.tile_count(), 1);
|
||
assert_eq!(surface.to_interleaved(Rect::new(0, 0, 1, 1)), vec![42]);
|
||
assert!(std::sync::Arc::ptr_eq(surface.tiles.values().next().unwrap(), before.tiles.values().next().unwrap()));
|
||
}
|
||
|
||
#[test]
|
||
fn cancelled_row_write_stops_during_tile_and_preserves_surface() {
|
||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||
|
||
let mut surface = Surface::new(PixelFormat::GRAY8);
|
||
let r = Rect::new(0, 0, 1, 100);
|
||
surface.try_write_rows_with(r, |_, _, dst| dst.fill(42)).unwrap();
|
||
let before = surface.clone();
|
||
let cancelled = AtomicBool::new(false);
|
||
let rows = AtomicUsize::new(0);
|
||
let check = || cancelled.load(Ordering::SeqCst);
|
||
let result = surface.try_write_rows_with_interrupt(r, &Interrupt::cancel_only(&check), |_, _, dst| {
|
||
dst.fill(7);
|
||
rows.fetch_add(1, Ordering::SeqCst);
|
||
cancelled.store(true, Ordering::SeqCst);
|
||
});
|
||
assert_eq!(result, Err(RowWriteError::Cancelled(Cancelled)));
|
||
assert_eq!(rows.load(Ordering::SeqCst), 1);
|
||
assert_eq!(surface.to_interleaved(r), before.to_interleaved(r));
|
||
assert!(std::sync::Arc::ptr_eq(surface.tiles.values().next().unwrap(), before.tiles.values().next().unwrap()));
|
||
}
|
||
|
||
/// Pre-fast-path `decode_tile_row_rgba` fallback (`read_samples` + `to_rgba`), retained as the
|
||
/// bitwise reference oracle across the full 16-bit (`0..=65535`) sample range and release bench.
|
||
fn legacy_read_rgba_into(surface: &Surface, r: Rect, out: &mut [[f32; 4]]) {
|
||
let w = r.width() as usize;
|
||
let h = r.height() as usize;
|
||
assert_eq!(out.len(), w * h);
|
||
if w == 0 || h == 0 {
|
||
return;
|
||
}
|
||
let fmt = &surface.format;
|
||
let n = fmt.channels();
|
||
let dp_rgba = to_rgba(fmt, &surface.default_pixel());
|
||
let mut row = Vec::new();
|
||
for tc in r.tiles() {
|
||
let tr = tc.rect().intersect(&r);
|
||
let span = tr.width() as usize;
|
||
let guard = surface.tiles.get(&tc).map(|t| t.bytes());
|
||
match guard.as_deref() {
|
||
Some(t) => {
|
||
for y in tr.y0..tr.y1 {
|
||
let o = ((y - r.y0) as usize) * w + (tr.x0 - r.x0) as usize;
|
||
let base = (((y - tc.ty * TILE_SIZE) as usize) * TILE_SIZE as usize + (tr.x0 - tc.tx * TILE_SIZE) as usize) * n;
|
||
let dst = &mut out[o..o + span];
|
||
row.resize(dst.len() * n, 0.0);
|
||
read_samples(t, fmt.sample, base, &mut row);
|
||
let a = |s: &[f32], i: usize| if fmt.alpha { s[i] } else { 1.0 };
|
||
let pixels = dst.iter_mut().zip(row.chunks_exact(n));
|
||
match fmt.mode {
|
||
ColorMode::Rgb => pixels.for_each(|(d, s)| *d = [s[0], s[1], s[2], a(s, 3)]),
|
||
ColorMode::Grayscale | ColorMode::Bitmap | ColorMode::Duotone => pixels.for_each(|(d, s)| *d = [s[0], s[0], s[0], a(s, 1)]),
|
||
_ => pixels.for_each(|(d, s)| *d = to_rgba(fmt, s)),
|
||
}
|
||
}
|
||
}
|
||
None => {
|
||
for y in tr.y0..tr.y1 {
|
||
let o = ((y - r.y0) as usize) * w + (tr.x0 - r.x0) as usize;
|
||
out[o..o + span].fill(dp_rgba);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn u16_rgba_and_graya_row_decode_is_bit_identical_over_full_range() {
|
||
// 256 x 256 = 65,536 pixels: exercises every 16-bit integer value `0u16..=65535u16` on every
|
||
// channel via odd-multiplier bijections modulo 2^16.
|
||
let tile_rect = Rect::new(0, 0, TILE_SIZE, TILE_SIZE);
|
||
let cross_rect = Rect::new(13, 7, TILE_SIZE + 14, TILE_SIZE + 4);
|
||
|
||
// 1. RGBA16 fast path: all 4 channels independently cover all 65,536 `u16` values.
|
||
let mut rgba16_bytes = Vec::with_capacity(65_536 * 8);
|
||
for i in 0u32..=65_535 {
|
||
let r = i as u16;
|
||
let g = (65_535 - i) as u16;
|
||
let b = (i.wrapping_mul(40_503)) as u16;
|
||
let a = (i.wrapping_mul(257).wrapping_add(17)) as u16;
|
||
for ch in [r, g, b, a] {
|
||
rgba16_bytes.extend_from_slice(&ch.to_ne_bytes());
|
||
}
|
||
}
|
||
let mut s_rgba16 = Surface::with_default(PixelFormat::RGBA16, &[0.125, 0.375, 0.625, 0.875]);
|
||
s_rgba16.try_write_interleaved(tile_rect, &rgba16_bytes).unwrap();
|
||
|
||
for rect in [tile_rect, cross_rect] {
|
||
let len = (rect.width() * rect.height()) as usize;
|
||
let mut got = vec![[0.0f32; 4]; len];
|
||
let mut want = vec![[0.0f32; 4]; len];
|
||
s_rgba16.read_rgba_into(rect, &mut got);
|
||
legacy_read_rgba_into(&s_rgba16, rect, &mut want);
|
||
for (idx, (g, w)) in got.iter().zip(&want).enumerate() {
|
||
assert_eq!(g.map(f32::to_bits), w.map(f32::to_bits), "RGBA16 {rect:?} idx {idx}: {g:?} vs {w:?}");
|
||
let x = rect.x0 + (idx as u32 % rect.width()) as i32;
|
||
let y = rect.y0 + (idx as u32 / rect.width()) as i32;
|
||
assert_eq!(g.map(f32::to_bits), s_rgba16.rgba(x, y).map(f32::to_bits), "RGBA16 scalar ({x},{y})");
|
||
}
|
||
}
|
||
|
||
// 2. GRAYA16 fast path: gray and alpha channels both cover all 65,536 `u16` values.
|
||
let graya16 = PixelFormat::new(ColorMode::Grayscale, SampleType::U16, true);
|
||
let mut graya16_bytes = Vec::with_capacity(65_536 * 4);
|
||
for i in 0u32..=65_535 {
|
||
let g = i as u16;
|
||
let a = (65_535 - i) as u16;
|
||
graya16_bytes.extend_from_slice(&g.to_ne_bytes());
|
||
graya16_bytes.extend_from_slice(&a.to_ne_bytes());
|
||
}
|
||
let mut s_graya16 = Surface::with_default(graya16, &[0.25, 0.75]);
|
||
s_graya16.try_write_interleaved(tile_rect, &graya16_bytes).unwrap();
|
||
|
||
for rect in [tile_rect, cross_rect] {
|
||
let len = (rect.width() * rect.height()) as usize;
|
||
let mut got = vec![[0.0f32; 4]; len];
|
||
let mut want = vec![[0.0f32; 4]; len];
|
||
s_graya16.read_rgba_into(rect, &mut got);
|
||
legacy_read_rgba_into(&s_graya16, rect, &mut want);
|
||
for (idx, (g, w)) in got.iter().zip(&want).enumerate() {
|
||
assert_eq!(g.map(f32::to_bits), w.map(f32::to_bits), "GRAYA16 {rect:?} idx {idx}: {g:?} vs {w:?}");
|
||
let x = rect.x0 + (idx as u32 % rect.width()) as i32;
|
||
let y = rect.y0 + (idx as u32 / rect.width()) as i32;
|
||
assert_eq!(g.map(f32::to_bits), s_graya16.rgba(x, y).map(f32::to_bits), "GRAYA16 scalar ({x},{y})");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Manually compare 16-bit RGBA/GRAYA `read_rgba_into` (whole-region and scanline-per-call as in
|
||
/// `composite_pixel_layer`) against the previous `read_samples` + `to_rgba` fallback.
|
||
#[test]
|
||
#[ignore = "manual 24 MP release benchmark"]
|
||
fn benchmark_u16_rgba_reads() {
|
||
use std::{hint::black_box, time::Instant};
|
||
type ReadRgbaFn = fn(&Surface, Rect, &mut [[f32; 4]]);
|
||
let (w, h) = (6000i32, 4000i32);
|
||
let rect = Rect::new(0, 0, w, h);
|
||
let graya16 = PixelFormat::new(ColorMode::Grayscale, SampleType::U16, true);
|
||
|
||
for (label, fmt) in [("RGBA16", PixelFormat::RGBA16), ("GRAYA16", graya16)] {
|
||
let mut s = Surface::new(fmt);
|
||
let fill: Vec<f32> = (0..fmt.channels()).map(|i| (i as f32 + 1.0) / (fmt.channels() as f32 + 1.0)).collect();
|
||
s.fill_rect(rect, &fill);
|
||
// Unshare a checkerboard of tiles so reads hit distinct tile allocations as well.
|
||
for ty in (0..h).step_by((TILE_SIZE * 2) as usize) {
|
||
for tx in (0..w).step_by((TILE_SIZE * 2) as usize) {
|
||
s.write_pixel(tx, ty, &fill);
|
||
}
|
||
}
|
||
|
||
let mut out_a = vec![[0.0f32; 4]; (w * h) as usize];
|
||
let mut out_b = vec![[0.0f32; 4]; (w * h) as usize];
|
||
legacy_read_rgba_into(&s, rect, &mut out_a);
|
||
s.read_rgba_into(rect, &mut out_b);
|
||
assert_eq!(out_a, out_b);
|
||
|
||
for trial in 0..7 {
|
||
let mut region_paths: [(&str, ReadRgbaFn); 2] = [("before", legacy_read_rgba_into), ("after", Surface::read_rgba_into)];
|
||
if trial % 2 == 1 {
|
||
region_paths.reverse();
|
||
}
|
||
for (name, path) in region_paths {
|
||
let t = Instant::now();
|
||
black_box(path)(&s, rect, black_box(&mut out_a));
|
||
let ms = t.elapsed().as_secs_f64() * 1000.0;
|
||
black_box(&out_a);
|
||
println!("region_24mp,{label},{trial},{name},{ms:.3}");
|
||
}
|
||
}
|
||
|
||
let mut row_buf = vec![[0.0f32; 4]; w as usize];
|
||
for trial in 0..7 {
|
||
let mut row_paths: [(&str, ReadRgbaFn); 2] = [("before", legacy_read_rgba_into), ("after", Surface::read_rgba_into)];
|
||
if trial % 2 == 1 {
|
||
row_paths.reverse();
|
||
}
|
||
for (name, path) in row_paths {
|
||
let t = Instant::now();
|
||
for y in 0..h {
|
||
black_box(path)(&s, Rect::new(0, y, w, y + 1), black_box(&mut row_buf));
|
||
}
|
||
let ms = t.elapsed().as_secs_f64() * 1000.0;
|
||
black_box(&row_buf);
|
||
println!("scanline_24mp,{label},{trial},{name},{ms:.3}");
|
||
}
|
||
}
|
||
}
|
||
}
|