From 81371f4a535de01bdbb46bf8d86abf4d569dc8d7 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 11 Nov 2026 14:49:35 -0500 Subject: [PATCH] render: seed and mix the node-id cache hash, skip the cell allocation past the pattern limit (#2254) * render: seed and mix the node-id cache hash, skip the cell allocation past the pattern limit * render: use hash_one in the bucket test --- crates/render/src/bounds_memo_tests.rs | 8 +++++++ crates/render/src/draw.rs | 29 ++++++++++++++++++++++---- crates/render/src/pattern.rs | 4 ++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/crates/render/src/bounds_memo_tests.rs b/crates/render/src/bounds_memo_tests.rs index 44d7dc420..6f7ab1f18 100644 --- a/crates/render/src/bounds_memo_tests.rs +++ b/crates/render/src/bounds_memo_tests.rs @@ -91,3 +91,11 @@ fn kept_splat_answers_are_the_answers_worked_out_afresh() { } } } + +#[test] +fn ids_chosen_to_differ_only_in_high_bits_still_spread_over_buckets() { + use std::hash::BuildHasher; + let state = crate::draw::IdState::default(); + let buckets: std::collections::HashSet = (0..4096u64).map(|k| state.hash_one(NodeId(k << 40)) & 0xfff).collect(); + assert!(buckets.len() > 2000, "{}", buckets.len()); +} diff --git a/crates/render/src/draw.rs b/crates/render/src/draw.rs index ff3a06270..bf5e8953c 100644 --- a/crates/render/src/draw.rs +++ b/crates/render/src/draw.rs @@ -69,16 +69,37 @@ impl<'a> Bounds<'a> { } /// A hash map keyed by node ids, which need no SipHash: they are counters (or a peer's range of -/// them), and SipHash took a good part of a render. -type IdMap = HashMap>; +/// them), and SipHash took a good part of a render. A file can choose its ids, so each map is +/// seeded at random and the hash mixes its high bits down (ids differing only up high would +/// otherwise share a bucket). +type IdMap = HashMap; + +/// Builds [`IdHash`]es from a per-map random seed. +#[derive(Clone)] +pub(crate) struct IdState(u64); + +impl Default for IdState { + fn default() -> Self { + use std::hash::BuildHasher; + IdState(std::collections::hash_map::RandomState::new().hash_one(0u8)) + } +} + +impl std::hash::BuildHasher for IdState { + type Hasher = IdHash; + fn build_hasher(&self) -> IdHash { + IdHash(self.0) + } +} /// Mixes each word in with a multiply (Fibonacci hashing), so ids that differ in low bits spread. -#[derive(Default)] pub(crate) struct IdHash(u64); impl Hasher for IdHash { fn finish(&self) -> u64 { - self.0 + // The multiply's low bits depend only on the input's low bits: mix the high ones down. + let h = (self.0 ^ (self.0 >> 32)).wrapping_mul(0xFF51_AFD7_ED55_8CCD); + h ^ (h >> 29) } fn write(&mut self, bytes: &[u8]) { for b in bytes { diff --git a/crates/render/src/pattern.rs b/crates/render/src/pattern.rs index 55559d59f..f86f9e774 100644 --- a/crates/render/src/pattern.rs +++ b/crates/render/src/pattern.rs @@ -39,6 +39,10 @@ pub fn nested(f: impl FnOnce() -> Option) -> Option { /// the pixmap, the cell and the pixmap's pixels per unit on each axis (whole pixels per cell, so /// the repeat lines up exactly). pub fn cell_pixmap(doc: &Document, pt: &PatternPaint, w: f64, h: f64, scale: f64, time: f64) -> Option<(Pixmap, PatternCell, (f64, f64))> { + // Past the nesting limit nothing is drawn, so don't allocate a cell first. + if DEPTH.with(Cell::get) >= MAX_DEPTH { + return None; + } let src = doc.node(NodeId(pt.source)).filter(|n| !n.is_page())?; let cell = pt.cell(src.width, src.height, w, h)?; let scale = if scale.is_finite() && scale > 0.0 { scale } else { 1.0 };