Migrate usage of the Hash trait for cache invalidation to the dedicated CacheHash trait (#4051)

* WIP start migrating usages of hash for cache invalidadion to dedicated trait

* Finish migrating usages

* Code review

* Add comments clearifying the reasoning for using random ids in the VectorModification cach hash impl

* Fix some remaining hash violations

* Finish migration and fix compilation

* Fix import ordering

* Cleanup

* Fix code review stuff

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Dennis Kobert
2026-04-27 07:18:47 +02:00
committed by GitHub
parent 7bb01c9651
commit 3d84e63ef9
64 changed files with 828 additions and 448 deletions

View File

@@ -9,7 +9,7 @@ use core_types::{Context, ContextDependencies, Cow, MemoHash, ProtoNodeIdentifie
use dyn_any::DynAny;
use glam::IVec2;
use log::Metadata;
use rustc_hash::{FxBuildHasher, FxHashMap};
use rustc_hash::FxHashMap;
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
@@ -32,7 +32,7 @@ fn return_true() -> bool {
/// An instance of a [`DocumentNodeDefinition`] that has been instantiated in a [`NodeNetwork`].
/// Currently, when an instance is made, it lives all on its own without any lasting connection to the definition.
/// But we will want to change it in the future so it merely references its definition.
#[derive(Clone, Debug, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub struct DocumentNode {
/// The inputs to a node, which are either:
/// - From other nodes within this graph [`NodeInput::Node`],
@@ -172,7 +172,7 @@ impl DocumentNode {
}
/// Represents the possible inputs to a node.
#[derive(Debug, Clone, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, PartialEq, Hash, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
pub enum NodeInput {
/// A reference to another node in the same network from which this node can receive its input.
Node { node_id: NodeId, output_index: usize },
@@ -196,7 +196,7 @@ pub enum NodeInput {
Inline(InlineRust),
}
#[derive(Debug, Clone, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, PartialEq, Hash, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
pub struct InlineRust {
pub expr: String,
pub ty: Type,
@@ -208,7 +208,7 @@ impl InlineRust {
}
}
#[derive(Debug, Clone, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, PartialEq, Hash, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
pub enum DocumentNodeMetadata {
DocumentNodePath,
}
@@ -292,7 +292,7 @@ pub enum OldDocumentNodeImplementation {
Extract,
}
#[derive(Clone, Debug, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, PartialEq, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
/// Represents the implementation of a node, which can be a nested [`NodeNetwork`], a proto [`ProtoNodeIdentifier`], or `Extract`.
pub enum DocumentNodeImplementation {
/// This describes a (document) node built out of a subgraph of other (document) nodes.
@@ -546,29 +546,42 @@ pub struct NodeNetwork {
pub generated: bool,
}
impl Hash for NodeNetwork {
fn hash<H: Hasher>(&self, state: &mut H) {
self.exports.hash(state);
impl core_types::CacheHash for NodeNetwork {
fn cache_hash<H: ::core::hash::Hasher>(&self, state: &mut H) {
self.exports.cache_hash(state);
let mut nodes: Vec<_> = self.nodes.iter().collect();
nodes.sort_by_key(|(id, _)| *id);
for (id, node) in nodes {
id.hash(state);
node.hash(state);
id.cache_hash(state);
node.cache_hash(state);
}
let mut scope_injections: Vec<_> = self.scope_injections.iter().collect();
scope_injections.sort_by_key(|(key, _)| key.as_str());
for (key, (node_id, ty)) in scope_injections {
key.cache_hash(state);
node_id.cache_hash(state);
ty.cache_hash(state);
}
}
}
impl PartialEq for NodeNetwork {
fn eq(&self, other: &Self) -> bool {
self.exports == other.exports
self.exports == other.exports && self.nodes == other.nodes && self.scope_injections == other.scope_injections
}
}
/// Graph modification functions
impl NodeNetwork {
pub fn current_hash(&self) -> u64 {
use std::hash::BuildHasher;
FxBuildHasher.hash_one(self)
use core_types::graphene_hash::CacheHash;
use rustc_hash::FxHasher;
use std::hash::Hasher;
let mut hasher = FxHasher::default();
self.cache_hash(&mut hasher);
hasher.finish()
}
pub fn value_network(node: DocumentNode) -> Self {
@@ -1136,6 +1149,17 @@ fn migrate_call_argument<'de, D: serde::Deserializer<'de>>(deserializer: D) -> R
})
}
impl core_types::graphene_hash::CacheHash for DocumentNode {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.inputs.cache_hash(state);
self.call_argument.cache_hash(state);
self.implementation.cache_hash(state);
self.visible.cache_hash(state);
self.skip_deduplication.cache_hash(state);
self.context_features.cache_hash(state);
}
}
#[cfg(test)]
mod test {
use super::*;
@@ -1254,11 +1278,11 @@ mod test {
};
network.populate_dependants();
network.flatten_with_fns(NodeId(1), |self_id, inner_id| NodeId(self_id.0 * 10 + inner_id.0), gen_node_id);
let flat_network = flat_network();
println!("{flat_network:#?}");
let expected = flatten_add_expected();
println!("{expected:#?}");
println!("{network:#?}");
assert_eq!(flat_network, network);
assert_eq!(expected, network);
}
#[test]
@@ -1345,6 +1369,55 @@ mod test {
pretty_assertions::assert_eq!(resolved_network[0], construction_network);
}
fn flatten_add_expected() -> NodeNetwork {
NodeNetwork {
exports: vec![NodeInput::node(NodeId(11), 0)],
nodes: [
(
NodeId(10),
DocumentNode {
inputs: vec![NodeInput::import(concrete!(u32), 0), NodeInput::node(NodeId(14), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("core_types::structural::ConsNode")),
original_location: OriginalLocation {
inputs_source: [(Source { node: vec![], index: 0 }, 1)].into(),
dependants: vec![vec![NodeId(11)]],
..Default::default()
},
..Default::default()
},
),
(
NodeId(14),
DocumentNode {
inputs: vec![NodeInput::value(TaggedValue::U32(2), false)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("core_types::value::ClonedNode")),
original_location: OriginalLocation {
path: Some(vec![NodeId(4)]),
dependants: vec![vec![NodeId(1), NodeId(10)]],
..Default::default()
},
..Default::default()
},
),
(
NodeId(11),
DocumentNode {
inputs: vec![NodeInput::node(NodeId(10), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("core_types::ops::AddPairNode")),
original_location: OriginalLocation {
dependants: vec![vec![]],
..Default::default()
},
..Default::default()
},
),
]
.into_iter()
.collect(),
..Default::default()
}
}
fn flat_network() -> NodeNetwork {
NodeNetwork {
exports: vec![NodeInput::node(NodeId(11), 0)],

View File

@@ -5,7 +5,7 @@ use brush_nodes::brush_cache::BrushCache;
use brush_nodes::brush_stroke::BrushStroke;
use core_types::table::Table;
use core_types::uuid::NodeId;
use core_types::{Color, ContextFeatures, MemoHash, Node, Type};
use core_types::{CacheHash, Color, ContextFeatures, MemoHash, Node, Type};
use dyn_any::DynAny;
pub use dyn_any::StaticType;
use glam::{Affine2, Vec2};
@@ -43,19 +43,18 @@ macro_rules! tagged_value {
EditorApi(Arc<PlatformEditorApi>)
}
// We must manually implement hashing because some values are floats and so do not reproducibly hash (see FakeHash below)
#[allow(clippy::derived_hash_with_manual_eq)]
impl Hash for TaggedValue {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
impl CacheHash for TaggedValue {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::mem::discriminant(self).hash(state);
match self {
Self::None => {}
$( Self::$identifier(x) => {x.hash(state)}),*
Self::RenderOutput(x) => x.hash(state),
Self::EditorApi(x) => x.hash(state),
$( Self::$identifier(x) => { x.cache_hash(state) }),*
Self::RenderOutput(x) => x.cache_hash(state),
Self::EditorApi(x) => x.cache_hash(state),
}
}
}
impl<'a> TaggedValue {
/// Converts to a Box<dyn DynAny>
pub fn to_dynany(self) -> DAny<'a> {
@@ -495,96 +494,33 @@ pub enum RenderOutputType {
},
}
impl Hash for RenderOutputType {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
impl CacheHash for RenderOutputType {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::mem::discriminant(self).hash(state);
match self {
Self::Texture(texture) => {
texture.hash(state);
}
Self::Texture(texture) => texture.hash(state),
Self::Buffer { data, width, height } => {
data.hash(state);
width.hash(state);
height.hash(state);
data.cache_hash(state);
width.cache_hash(state);
height.cache_hash(state);
}
Self::Svg { svg, image_data } => {
svg.hash(state);
image_data.hash(state);
svg.cache_hash(state);
image_data.cache_hash(state);
}
#[cfg(target_family = "wasm")]
Self::CanvasFrame { canvas_id, resolution } => {
canvas_id.hash(state);
resolution.to_array().iter().for_each(|x| x.to_bits().hash(state));
canvas_id.cache_hash(state);
resolution.cache_hash(state);
}
}
}
}
impl Hash for RenderOutput {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.data.hash(state)
}
}
/// We hash the floats and so-forth despite it not being reproducible because all inputs to the node graph must be hashed otherwise the graph execution breaks (so sorry about this hack)
trait FakeHash {
fn hash<H: core::hash::Hasher>(&self, state: &mut H);
}
mod fake_hash {
use super::*;
impl FakeHash for f64 {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.to_bits().hash(state)
}
}
impl FakeHash for f32 {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.to_bits().hash(state)
}
}
impl FakeHash for DVec2 {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.to_array().iter().for_each(|x| x.to_bits().hash(state))
}
}
impl FakeHash for Vec2 {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.to_array().iter().for_each(|x| x.to_bits().hash(state))
}
}
impl FakeHash for DAffine2 {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.to_cols_array().iter().for_each(|x| x.to_bits().hash(state))
}
}
impl FakeHash for Affine2 {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.to_cols_array().iter().for_each(|x| x.to_bits().hash(state))
}
}
impl<T: FakeHash> FakeHash for Option<T> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
if let Some(x) = self {
1.hash(state);
x.hash(state);
} else {
0.hash(state);
}
}
}
impl<T: FakeHash> FakeHash for Vec<T> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.len().hash(state);
self.iter().for_each(|x| x.hash(state))
}
}
impl<T: FakeHash, const N: usize> FakeHash for [T; N] {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.iter().for_each(|x| x.hash(state))
}
}
impl FakeHash for (f64, Color) {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.0.to_bits().hash(state);
self.1.hash(state)
}
// Metadata is excluded because it's editor-side auxiliary data (click targets, transforms)
// that shouldn't affect render cache invalidation, and it contains HashMaps with non-deterministic iteration order
impl CacheHash for RenderOutput {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.data.cache_hash(state);
}
}