fix(gpu): refresh patterns when defaults or formats change (#5013)

Co-authored-by: haranishi <223786623+haranishi@users.noreply.github.com>
This commit is contained in:
Ikki Nishihara 2026-10-14 01:33:27 +09:00 committed by GitHub
parent 752f611264
commit 66e07cbf79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 87 additions and 5 deletions

View File

@ -178,6 +178,13 @@ type ResKey = (LayerId, Role, Cell);
/// larger than the texture limit.
type FxKey = (LayerId, Option<Cell>);
struct CachedPattern {
fingerprint: u64,
format: PixelFormat,
default_bits: Vec<u32>,
texture: Tex,
}
struct Resident {
texture: wgpu::Texture,
view: wgpu::TextureView,
@ -412,7 +419,7 @@ pub struct Compositor {
prefix: prefix::Prefix,
/// Effect temporaries (R32F, region sized) and the frame they were last used.
temps: Vec<(Tex, u64)>,
patterns: HashMap<String, (u64, Tex)>,
patterns: HashMap<String, CachedPattern>,
/// The colour mode of the document being encoded: patterns paint in it
/// (`photocraft_compose::pattern::pattern_rgba`).
pattern_mode: photocraft_color::ColorMode,
@ -1538,10 +1545,15 @@ impl Compositor {
});
let mode = self.pattern_mode;
let key = format!("{}\u{0}{}\u{0}{mode:?}", pat.id, pat.name);
if let Some((f, t)) = self.patterns.get(&key)
&& *f == fp
let format = pat.surface.format();
// Unstored tiles read the default pixel, interpreted in the source format.
let default_bits: Vec<u32> = pat.surface.default_pixel().iter().map(|v| v.to_bits()).collect();
if let Some(cached) = self.patterns.get(&key)
&& cached.fingerprint == fp
&& cached.format == format
&& cached.default_bits == default_bits
{
return t.view.clone();
return cached.texture.view.clone();
}
let (w, h) = (pat.width, pat.height);
let px = photocraft_compose::pattern::pattern_rgba(pat, mode);
@ -1554,7 +1566,7 @@ impl Compositor {
wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
);
let v = t.view.clone();
self.patterns.insert(key, (fp, t));
self.patterns.insert(key, CachedPattern { fingerprint: fp, format, default_bits, texture: t });
v
}

View File

@ -2109,6 +2109,76 @@ fn artboards() {
}
}
/// Sparse patterns may have no stored tiles: their defaults still determine every pixel.
#[test]
fn pattern_cache_tracks_default_pixels() {
let Some(mut g) = gpu() else { return };
for depth in [SampleType::U8, SampleType::U16, SampleType::F32] {
let fmt = PixelFormat::RGBA8.with_sample(depth);
let mut d = Document::new("pattern defaults", Size::new(8, 6), ColorMode::Rgb, depth);
let mut pat = Pattern::new("cache identity", photocraft_raster::Surface::with_default(fmt, &[1.0, 0.0, 0.0, 1.0]), 2, 2);
assert_eq!(pat.surface.tiles().count(), 0);
d.layers.push(Layer::new(
"pattern",
LayerContent::Fill(Fill::Pattern { name: pat.name.clone(), id: pat.id.clone(), scale: 1.0, angle: 0.0, link: true, phase: (0.0, 0.0) }),
));
d.patterns.push(pat.clone());
check(&mut g, &d, &format!("red default {depth:?}"));
pat.surface = photocraft_raster::Surface::with_default(fmt, &[0.0, 0.0, 1.0, 1.0]);
assert_eq!(pat.surface.tiles().count(), 0);
d.patterns[0] = pat;
let cpu = photocraft_compose::flatten(&d);
let mut fresh = Compositor::new(&g.device);
let out = render_to_vec(&mut fresh, &g.device, &g.queue, &d, d.bounds()).unwrap();
assert!(worst_diff(&cpu.px, &out).0 <= TOL, "fresh compositor matches blue default");
check(&mut g, &d, &format!("blue default {depth:?}"));
}
}
#[test]
fn pattern_cache_tracks_source_format_and_cow_tiles() {
let Some(mut g) = gpu() else { return };
let defaults = [0.5, 0.25, 0.75, 1.0];
let mut pat = Pattern::new("format identity", photocraft_raster::Surface::with_default(PixelFormat::RGBA32F, &defaults), 2, 2);
let mut d = Document::new("pattern formats", Size::new(8, 6), ColorMode::Rgb, SampleType::F32);
d.layers.push(Layer::new(
"pattern",
LayerContent::Fill(Fill::Pattern { name: pat.name.clone(), id: pat.id.clone(), scale: 1.0, angle: 0.0, link: true, phase: (0.0, 0.0) }),
));
let mut previous = None;
for fmt in [PixelFormat::RGBA32F, PixelFormat::new(ColorMode::Lab, SampleType::F32, true), PixelFormat::RGBA16, PixelFormat::RGBA8] {
pat.surface = photocraft_raster::Surface::with_default(fmt, &defaults);
if fmt.sample == SampleType::F32 {
assert_eq!(pat.surface.default_pixel(), defaults);
}
d.patterns = vec![pat.clone()];
let cpu = photocraft_compose::flatten(&d);
if fmt.mode == ColorMode::Lab {
let old: Vec<[f32; 4]> = previous.take().unwrap();
assert!(worst_diff(&old, &cpu.px).0 > 0.1, "same default bits have different source-format colours");
}
previous = Some(cpu.px.clone());
let mut fresh = Compositor::new(&g.device);
let out = render_to_vec(&mut fresh, &g.device, &g.queue, &d, d.bounds()).unwrap();
assert!(worst_diff(&cpu.px, &out).0 <= TOL, "fresh compositor matches {fmt:?}");
check(&mut g, &d, &format!("source format {fmt:?}"));
check(&mut g, &d, &format!("unchanged source format {fmt:?}"));
}
pat.surface.fill_rect(pat.rect(), &[1.0, 0.0, 0.0, 1.0]);
d.patterns = vec![pat.clone()];
check(&mut g, &d, "stored red tile");
// Keep the old tile alive: this exercises COW invalidation, not allocator address reuse.
let old = pat.clone();
pat.surface.fill_rect(Rect::new(0, 0, 1, 1), &[0.0, 0.0, 1.0, 1.0]);
assert!(!std::sync::Arc::ptr_eq(old.surface.tiles().next().unwrap().1, pat.surface.tiles().next().unwrap().1));
d.patterns = vec![pat];
check(&mut g, &d, "COW blue tile edit");
let mut fresh = Compositor::new(&g.device);
let out = render_to_vec(&mut fresh, &g.device, &g.queue, &d, d.bounds()).unwrap();
assert!(worst_diff(&photocraft_compose::flatten(&d).px, &out).0 <= TOL, "fresh compositor matches COW edit");
assert_eq!(old.surface.tile_count(), 1);
}
#[test]
fn pattern_fill_layers() {
let Some(mut g) = gpu() else { return };