From a1c014db2c953a398c2e5ce2a43066fb6a26153a Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Fri, 31 Jul 2026 10:18:00 +0000 Subject: [PATCH] Pin cache key and source id hashing to u64 fx state --- node-graph/graph-craft/src/document.rs | 2 +- .../libraries/core-types/src/registry.rs | 2 +- node-graph/libraries/graphene-hash/src/lib.rs | 110 ++++++++++++++++++ 3 files changed, 112 insertions(+), 2 deletions(-) diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index e57665b691..f6fabb6e1c 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -981,7 +981,7 @@ impl NodeNetwork { } fn source_id_for_path(path: &[NodeId]) -> u64 { - let mut hasher = DefaultHasher::new(); + let mut hasher = graphene_hash::FxHasher64::new(); path.hash(&mut hasher); hasher.finish() } diff --git a/node-graph/libraries/core-types/src/registry.rs b/node-graph/libraries/core-types/src/registry.rs index 320386e696..5151737631 100644 --- a/node-graph/libraries/core-types/src/registry.rs +++ b/node-graph/libraries/core-types/src/registry.rs @@ -91,7 +91,7 @@ pub fn lend_edge_type() -> Type { } pub fn cache_key(ctx: &C) -> u64 { - let mut hasher = std::hash::DefaultHasher::new(); + let mut hasher = graphene_hash::FxHasher64::new(); ctx.cache_hash(&mut hasher); hasher.finish() } diff --git a/node-graph/libraries/graphene-hash/src/lib.rs b/node-graph/libraries/graphene-hash/src/lib.rs index 4bacd39f10..27a036da19 100644 --- a/node-graph/libraries/graphene-hash/src/lib.rs +++ b/node-graph/libraries/graphene-hash/src/lib.rs @@ -236,3 +236,113 @@ impl_tuple!(A, B, C); impl_tuple!(A, B, C, D); impl_tuple!(A, B, C, D, E); impl_tuple!(A, B, C, D, E, F); + +/// rustc-hash's polynomial hash with the state pinned to u64, so keys match across native and wasm targets. +#[derive(Clone, Default)] +pub struct FxHasher64 { + hash: u64, +} + +const K: u64 = 0xf1357aea2e62a9c5; +const SEED1: u64 = 0x243f6a8885a308d3; +const SEED2: u64 = 0x13198a2e03707344; +const PREVENT_TRIVIAL_ZERO_COLLAPSE: u64 = 0xa4093822299f31d0; + +impl FxHasher64 { + pub const fn new() -> Self { + Self { hash: 0 } + } + + #[inline] + fn add_to_hash(&mut self, i: u64) { + self.hash = self.hash.wrapping_add(i).wrapping_mul(K); + } +} + +impl core::hash::Hasher for FxHasher64 { + #[inline] + fn write(&mut self, bytes: &[u8]) { + self.add_to_hash(hash_bytes(bytes)); + } + + #[inline] + fn write_u8(&mut self, i: u8) { + self.add_to_hash(i as u64); + } + + #[inline] + fn write_u16(&mut self, i: u16) { + self.add_to_hash(i as u64); + } + + #[inline] + fn write_u32(&mut self, i: u32) { + self.add_to_hash(i as u64); + } + + #[inline] + fn write_u64(&mut self, i: u64) { + self.add_to_hash(i); + } + + #[inline] + fn write_u128(&mut self, i: u128) { + self.add_to_hash(i as u64); + self.add_to_hash((i >> 64) as u64); + } + + #[inline] + fn write_usize(&mut self, i: usize) { + self.add_to_hash(i as u64); + } + + #[inline] + fn finish(&self) -> u64 { + self.hash.rotate_left(26) + } +} + +#[inline] +fn multiply_mix(x: u64, y: u64) -> u64 { + let full = (x as u128) * (y as u128); + (full as u64) ^ ((full >> 64) as u64) +} + +#[inline] +fn hash_bytes(bytes: &[u8]) -> u64 { + let len = bytes.len(); + let mut s0 = SEED1; + let mut s1 = SEED2; + + if len <= 16 { + if len >= 8 { + s0 ^= u64::from_le_bytes(bytes[0..8].try_into().unwrap()); + s1 ^= u64::from_le_bytes(bytes[len - 8..].try_into().unwrap()); + } else if len >= 4 { + s0 ^= u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as u64; + s1 ^= u32::from_le_bytes(bytes[len - 4..].try_into().unwrap()) as u64; + } else if len > 0 { + let lo = bytes[0]; + let mid = bytes[len / 2]; + let hi = bytes[len - 1]; + s0 ^= lo as u64; + s1 ^= ((hi as u64) << 8) | mid as u64; + } + } else { + let mut off = 0; + while off < len - 16 { + let x = u64::from_le_bytes(bytes[off..off + 8].try_into().unwrap()); + let y = u64::from_le_bytes(bytes[off + 8..off + 16].try_into().unwrap()); + let t = multiply_mix(s0 ^ x, PREVENT_TRIVIAL_ZERO_COLLAPSE ^ y); + s0 = s1; + s1 = t; + off += 16; + } + + let suffix = &bytes[len - 16..]; + s0 ^= u64::from_le_bytes(suffix[0..8].try_into().unwrap()); + s1 ^= u64::from_le_bytes(suffix[8..16].try_into().unwrap()); + } + + multiply_mix(s0, s1) ^ (len as u64) +}