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) <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-13 06:32:45 -04:00
parent 10a7a4f2a4
commit ed181a809e
1 changed files with 17 additions and 6 deletions

View File

@ -66,11 +66,11 @@ fn with<R>(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};