From ed181a809ea448ffe87606fe297ad20bd08c8264 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 06:32:45 -0400 Subject: [PATCH] Render: pictures with opposite corrections no longer share decoded pixels The decoded-picture cache keyed adjustments with an xor-multiply mix in which brightness b, contrast c and -b, -c collide (the sign bits cancel), so of two copies of a picture, one +40% bright and contrast and one -40%, both showed whichever was decoded first. The key is now a real hash. Wild synthetic set: brightness/contrast grid 27.86 -> 15.93. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/render/src/images.rs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/crates/render/src/images.rs b/crates/render/src/images.rs index 4d4f96d..238821c 100644 --- a/crates/render/src/images.rs +++ b/crates/render/src/images.rs @@ -66,11 +66,11 @@ fn with(f: impl FnOnce(&mut Cache) -> R) -> R { } fn adjust_key(a: &PictureAdjust) -> u64 { - let mut h: u64 = 1469598103934665603; - let mut mix = |v: u64| { - h ^= v; - h = h.wrapping_mul(1099511628211); - }; + use std::hash::Hasher; + // A real hash: an FNV-style xor-multiply mix let (b, c) and (-b, -c) collide (the sign bits + // cancel), so a picture brightened and contrasted showed the darkened copy's pixels. + let mut hs = std::hash::DefaultHasher::new(); + let mut mix = |v: u64| hs.write_u64(v); mix(a.brightness.to_bits()); mix(a.contrast.to_bits()); mix(a.saturation.unwrap_or(1.0).to_bits()); @@ -84,7 +84,7 @@ fn adjust_key(a: &PictureAdjust) -> u64 { mix(((c.r as u64) << 24) | ((c.g as u64) << 16) | ((c.b as u64) << 8) | c.a as u64 | 1 << 41); } } - h + hs.finish() } /// The duotone's dark and light colours. The painter resolves them to plain RGB first (see @@ -368,6 +368,17 @@ mod tests { assert_ne!(adjust_key(&adj), adjust_key(&PictureAdjust::default())); } + #[test] + fn opposite_brightness_and_contrast_are_cached_apart() { + let a = PictureAdjust { brightness: 0.4, contrast: 0.4, ..Default::default() }; + let b = PictureAdjust { brightness: -0.4, contrast: -0.4, ..Default::default() }; + assert_ne!(adjust_key(&a), adjust_key(&b)); + let c = PictureAdjust { brightness: 0.4, contrast: -0.4, ..Default::default() }; + let d = PictureAdjust { brightness: -0.4, contrast: 0.4, ..Default::default() }; + assert_ne!(adjust_key(&c), adjust_key(&d)); + assert_eq!(adjust_key(&a), adjust_key(&a.clone())); + } + #[test] fn a_gif_of_many_tiny_frames_keeps_only_the_frame_cap() { use image::{Delay, Frame, Rgba, RgbaImage};