diff --git a/Cargo.lock b/Cargo.lock index 324b645471..ae220f141d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -350,6 +350,7 @@ dependencies = [ "core-types", "dyn-any", "glam", + "graphene-hash", "node-macro", "raster-nodes", "raster-types", @@ -873,6 +874,7 @@ dependencies = [ "ctor", "dyn-any", "glam", + "graphene-hash", "image", "kurbo", "log", @@ -1916,6 +1918,7 @@ dependencies = [ "graph-craft", "graphene-application-io", "graphene-core", + "graphene-hash", "graphic-types", "iai-callgrind", "js-sys", @@ -1996,6 +1999,7 @@ dependencies = [ "core-types", "dyn-any", "glam", + "graphene-hash", "graphic-types", "log", "node-macro", @@ -2005,6 +2009,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "graphene-hash" +version = "0.0.0" +dependencies = [ + "glam", + "graphene-hash-derive", +] + +[[package]] +name = "graphene-hash-derive" +version = "0.0.0" +dependencies = [ + "graphene-hash", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "graphene-std" version = "0.1.0" @@ -2065,6 +2087,7 @@ dependencies = [ "core-types", "dyn-any", "glam", + "graphene-hash", "node-macro", "raster-types", "serde", @@ -2182,6 +2205,7 @@ dependencies = [ "futures", "glam", "graph-craft", + "graphene-hash", "graphene-std", "graphite-proc-macros", "image", @@ -3296,6 +3320,7 @@ dependencies = [ "core-types", "dyn-any", "glam", + "graphene-hash", "half", "log", "node-macro", @@ -4316,6 +4341,7 @@ dependencies = [ "fastnoise-lite", "futures", "glam", + "graphene-hash", "image", "kurbo", "ndarray", @@ -4361,6 +4387,7 @@ dependencies = [ "core-types", "dyn-any", "glam", + "graphene-hash", "image", "node-macro", "serde", @@ -4501,6 +4528,7 @@ dependencies = [ "core-types", "dyn-any", "glam", + "graphene-hash", "graphic-types", "kurbo", "log", @@ -5513,6 +5541,7 @@ dependencies = [ "dyn-any", "fancy-regex", "glam", + "graphene-hash", "log", "node-macro", "parley", @@ -6157,6 +6186,7 @@ dependencies = [ "futures", "glam", "graphene-core", + "graphene-hash", "graphic-types", "kurbo", "log", @@ -6182,6 +6212,7 @@ dependencies = [ "dyn-any", "fixedbitset", "glam", + "graphene-hash", "kurbo", "log", "lyon_geom", diff --git a/Cargo.toml b/Cargo.toml index 802a5acce1..c6c6d60924 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "frontend/wrapper", "libraries/dyn-any", "libraries/math-parser", + "node-graph/libraries/graphene-hash", "node-graph/libraries/*", "node-graph/nodes/*", "node-graph/nodes/raster/shaders", @@ -63,6 +64,7 @@ dyn-any = { path = "libraries/dyn-any", features = [ "log-bad-types", "rc", ] } +graphene-hash = { path = "node-graph/libraries/graphene-hash", features = ["derive"] } preprocessor = { path = "node-graph/preprocessor" } math-parser = { path = "libraries/math-parser" } graphene-application-io = { path = "node-graph/libraries/application-io" } diff --git a/editor/Cargo.toml b/editor/Cargo.toml index c7ee2c3ebb..ac7bf6d7bf 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -19,6 +19,7 @@ gpu = ["interpreted-executor/gpu", "dep:wgpu-executor"] # Local dependencies graphite-proc-macros = { workspace = true } graph-craft = { workspace = true } +graphene-hash = { workspace = true } interpreted-executor = { workspace = true } graphene-std = { workspace = true } # NOTE: `core-types` should not be added here because `graphene-std` re-exports its contents preprocessor = { workspace = true } diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/memo_network.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/memo_network.rs index e6f7b126fd..71b0288591 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/memo_network.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/memo_network.rs @@ -1,6 +1,5 @@ use graph_craft::document::NodeNetwork; use std::cell::Cell; -use std::hash::{Hash, Hasher}; #[derive(Debug, Default, Clone, PartialEq)] pub struct MemoNetwork { @@ -26,9 +25,9 @@ impl serde::Serialize for MemoNetwork { } } -impl Hash for MemoNetwork { - fn hash(&self, state: &mut H) { - self.current_hash().hash(state); +impl graphene_hash::CacheHash for MemoNetwork { + fn cache_hash(&self, state: &mut H) { + self.current_hash().cache_hash(state); } } diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index f6b8f49be9..3859505e36 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -26,7 +26,7 @@ pub struct GradientOptions { #[impl_message(Message, ToolMessage, Gradient)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(PartialEq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize)] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum GradientToolMessage { // Standard messages Abort, diff --git a/frontend/wrapper/src/editor_wrapper.rs b/frontend/wrapper/src/editor_wrapper.rs index 74a76d7c3e..e9dd0edf6d 100644 --- a/frontend/wrapper/src/editor_wrapper.rs +++ b/frontend/wrapper/src/editor_wrapper.rs @@ -22,6 +22,7 @@ use editor::messages::portfolio::utility_types::{DockingSplitDirection, FontCata use editor::messages::prelude::*; use editor::messages::tool::tool_messages::tool_prelude::WidgetId; use graph_craft::document::NodeId; +use graphene_std::graphene_hash::CacheHashWrapper; use graphene_std::raster::color::Color; use graphene_std::vector::GradientStops; use serde::Serialize; @@ -131,7 +132,7 @@ impl EditorWrapper { // Sends a FrontendMessage to JavaScript pub(crate) fn send_frontend_message_to_js(&self, message: FrontendMessage) { if let FrontendMessage::UpdateImageData { ref image_data } = message { - let new_hash = calculate_hash(image_data); + let new_hash = calculate_hash(&CacheHashWrapper(image_data)); let prev_hash = IMAGE_DATA_HASH.load(Ordering::Relaxed); if new_hash != prev_hash { diff --git a/node-graph/graph-craft/Cargo.toml b/node-graph/graph-craft/Cargo.toml index db2fdd5756..93b0b64f9f 100644 --- a/node-graph/graph-craft/Cargo.toml +++ b/node-graph/graph-craft/Cargo.toml @@ -22,6 +22,7 @@ wasm = [ [dependencies] # Local dependencies dyn-any = { workspace = true } +graphene-hash = { workspace = true } core-types = { workspace = true } brush-nodes = { workspace = true } graphene-core = { workspace = true } diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index 0cb33bf3ff..08bc7c44d1 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -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(&self, state: &mut H) { - self.exports.hash(state); +impl core_types::CacheHash for NodeNetwork { + fn cache_hash(&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(&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)], diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index d89e8f7996..de3e90f0ef 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -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) } - // 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(&self, state: &mut H) { + impl CacheHash for TaggedValue { + fn cache_hash(&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 pub fn to_dynany(self) -> DAny<'a> { @@ -495,96 +494,33 @@ pub enum RenderOutputType { }, } -impl Hash for RenderOutputType { - fn hash(&self, state: &mut H) { +impl CacheHash for RenderOutputType { + fn cache_hash(&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(&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(&self, state: &mut H); -} -mod fake_hash { - use super::*; - impl FakeHash for f64 { - fn hash(&self, state: &mut H) { - self.to_bits().hash(state) - } - } - impl FakeHash for f32 { - fn hash(&self, state: &mut H) { - self.to_bits().hash(state) - } - } - impl FakeHash for DVec2 { - fn hash(&self, state: &mut H) { - self.to_array().iter().for_each(|x| x.to_bits().hash(state)) - } - } - impl FakeHash for Vec2 { - fn hash(&self, state: &mut H) { - self.to_array().iter().for_each(|x| x.to_bits().hash(state)) - } - } - impl FakeHash for DAffine2 { - fn hash(&self, state: &mut H) { - self.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)) - } - } - impl FakeHash for Affine2 { - fn hash(&self, state: &mut H) { - self.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)) - } - } - impl FakeHash for Option { - fn hash(&self, state: &mut H) { - if let Some(x) = self { - 1.hash(state); - x.hash(state); - } else { - 0.hash(state); - } - } - } - impl FakeHash for Vec { - fn hash(&self, state: &mut H) { - self.len().hash(state); - self.iter().for_each(|x| x.hash(state)) - } - } - impl FakeHash for [T; N] { - fn hash(&self, state: &mut H) { - self.iter().for_each(|x| x.hash(state)) - } - } - impl FakeHash for (f64, Color) { - fn hash(&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(&self, state: &mut H) { + self.data.cache_hash(state); } } diff --git a/node-graph/graphene-cli/src/export.rs b/node-graph/graphene-cli/src/export.rs index 558d6b5206..d41e13485f 100644 --- a/node-graph/graphene-cli/src/export.rs +++ b/node-graph/graphene-cli/src/export.rs @@ -69,7 +69,7 @@ pub async fn export_document( } RenderOutputType::Texture(image_texture) => { // Convert GPU texture to CPU buffer - let gpu_raster = Raster::::new_gpu(image_texture.texture.as_ref().clone()); + let gpu_raster = Raster::::new_gpu(image_texture.as_ref().clone()); let cpu_raster: Raster = gpu_raster.convert(Footprint::BOUNDLESS, wgpu_executor).await; let (data, width, height) = cpu_raster.to_flat_u8(); // Explicitly drop texture to make sure it lives long enough @@ -202,7 +202,7 @@ pub async fn export_gif( let (data, img_width, img_height) = match result { TaggedValue::RenderOutput(output) => match output.data { RenderOutputType::Texture(image_texture) => { - let gpu_raster = Raster::::new_gpu(image_texture.texture.as_ref().clone()); + let gpu_raster = Raster::::new_gpu(image_texture.as_ref().clone()); let cpu_raster: Raster = gpu_raster.convert(Footprint::BOUNDLESS, wgpu_executor).await; // Explicitly drop texture to make sure it lives long enough std::mem::drop(image_texture); diff --git a/node-graph/graphene-cli/src/main.rs b/node-graph/graphene-cli/src/main.rs index 5cf12ac04d..a14188de3c 100644 --- a/node-graph/graphene-cli/src/main.rs +++ b/node-graph/graphene-cli/src/main.rs @@ -4,12 +4,12 @@ use clap::{Args, Parser, Subcommand}; use fern::colors::{Color, ColoredLevelConfig}; use futures::executor::block_on; use graph_craft::application_io::EditorPreferences; +use graph_craft::application_io::{PlatformApplicationIo, PlatformEditorApi}; use graph_craft::document::*; use graph_craft::graphene_compiler::Compiler; use graph_craft::proto::ProtoNetwork; use graph_craft::util::load_network; use graphene_std::application_io::{ApplicationIo, NodeGraphUpdateMessage, NodeGraphUpdateSender}; -use graphene_std::application_io::{PlatformEditorApi, WasmApplicationIo}; use graphene_std::text::FontCache; use interpreted_executor::dynamic_executor::DynamicExecutor; use interpreted_executor::util::wrap_network_in_scope; @@ -121,7 +121,7 @@ async fn main() -> Result<(), Box> { let document_string = std::fs::read_to_string(document_path).expect("Failed to read document"); log::info!("Creating GPU context"); - let mut application_io = block_on(WasmApplicationIo::new()); + let mut application_io = block_on(PlatformApplicationIo::new()); if let Command::Export { image: Some(ref image_path), .. } = app.command { application_io.resources.insert("null".to_string(), Arc::from(std::fs::read(image_path).expect("Failed to read image"))); diff --git a/node-graph/libraries/application-io/src/lib.rs b/node-graph/libraries/application-io/src/lib.rs index c28a1baa7b..aec1fba5ee 100644 --- a/node-graph/libraries/application-io/src/lib.rs +++ b/node-graph/libraries/application-io/src/lib.rs @@ -164,6 +164,12 @@ impl Hash for EditorApi { } } +impl core_types::graphene_hash::CacheHash for EditorApi { + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(self, state); + } +} + impl PartialEq for EditorApi { fn eq(&self, other: &Self) -> bool { self.font_cache == other.font_cache diff --git a/node-graph/libraries/core-types/Cargo.toml b/node-graph/libraries/core-types/Cargo.toml index d6bc01c4db..e3259fd579 100644 --- a/node-graph/libraries/core-types/Cargo.toml +++ b/node-graph/libraries/core-types/Cargo.toml @@ -16,6 +16,7 @@ wasm = ["tsify", "wasm-bindgen", "no-std-types/wasm"] [dependencies] # Local dependencies no-std-types = { workspace = true, features = ["std"] } +graphene-hash = { workspace = true, features = ["derive"] } # Workspace dependencies bitflags = { workspace = true } diff --git a/node-graph/libraries/core-types/src/context.rs b/node-graph/libraries/core-types/src/context.rs index 443d2f8b2f..bba3e7f876 100644 --- a/node-graph/libraries/core-types/src/context.rs +++ b/node-graph/libraries/core-types/src/context.rs @@ -163,6 +163,12 @@ bitflags! { } } +impl graphene_hash::CacheHash for ContextFeatures { + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(self, state); + } +} + impl ContextFeatures { pub fn name(&self) -> &'static str { match *self { @@ -182,7 +188,7 @@ impl ContextFeatures { // CONTEXT DEPENDENCIES // ==================== -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, graphene_hash::CacheHash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, Default)] pub struct ContextDependencies { pub extract: ContextFeatures, pub inject: ContextFeatures, @@ -536,14 +542,14 @@ impl Default for OwnedContextImpl { } } -impl Hash for OwnedContextImpl { - fn hash(&self, state: &mut H) { - self.footprint.hash(state); - self.real_time.map(|x| x.to_bits()).hash(state); - self.animation_time.map(|x| x.to_bits()).hash(state); - self.pointer_position.map(|v| (v.x.to_bits(), v.y.to_bits())).hash(state); - self.position.iter().flat_map(|x| x.iter()).map(|v| (v.x.to_bits(), v.y.to_bits())).for_each(|v| v.hash(state)); - self.index.hash(state); +impl graphene_hash::CacheHash for OwnedContextImpl { + fn cache_hash(&self, state: &mut H) { + self.footprint.cache_hash(state); + self.real_time.cache_hash(state); + self.animation_time.cache_hash(state); + self.pointer_position.cache_hash(state); + self.position.cache_hash(state); + self.index.cache_hash(state); self.hash_varargs(state); } } @@ -600,9 +606,9 @@ pub trait DynHash { fn dyn_hash(&self, state: &mut dyn Hasher); } -impl DynHash for H { +impl DynHash for H { fn dyn_hash(&self, mut state: &mut dyn Hasher) { - self.hash(&mut state); + graphene_hash::CacheHash::cache_hash(self, &mut state); } } diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index 1d2f540a40..f28758405f 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -21,6 +21,8 @@ pub use color::Color; pub use context::*; pub use ctor; pub use dyn_any::{StaticTypeSized, WasmNotSend, WasmNotSync}; +pub use graphene_hash; +pub use graphene_hash::CacheHash; pub use memo::MemoHash; pub use no_std_types::AsU32; pub use no_std_types::blending; diff --git a/node-graph/libraries/core-types/src/memo.rs b/node-graph/libraries/core-types/src/memo.rs index 42d2d4b608..17a618c9a6 100644 --- a/node-graph/libraries/core-types/src/memo.rs +++ b/node-graph/libraries/core-types/src/memo.rs @@ -1,3 +1,4 @@ +use graphene_hash::CacheHash; use std::hash::DefaultHasher; use std::hash::{Hash, Hasher}; use std::ops::Deref; @@ -11,12 +12,12 @@ pub struct IORecord { } #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] -pub struct MemoHash { +pub struct MemoHash { hash: u64, value: Arc, } -impl<'de, T: serde::Deserialize<'de> + Hash> serde::Deserialize<'de> for MemoHash { +impl<'de, T: serde::Deserialize<'de> + CacheHash> serde::Deserialize<'de> for MemoHash { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -25,7 +26,7 @@ impl<'de, T: serde::Deserialize<'de> + Hash> serde::Deserialize<'de> for MemoHas } } -impl serde::Serialize for MemoHash { +impl serde::Serialize for MemoHash { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, @@ -34,7 +35,7 @@ impl serde::Serialize for MemoHash { } } -impl MemoHash { +impl MemoHash { pub fn new(value: T) -> Self { let hash = Self::calc_hash(&value); Self { hash, value: value.into() } @@ -45,7 +46,7 @@ impl MemoHash { fn calc_hash(data: &T) -> u64 { let mut hasher = DefaultHasher::new(); - data.hash(&mut hasher); + data.cache_hash(&mut hasher); hasher.finish() } @@ -59,19 +60,26 @@ impl MemoHash { self.hash } } -impl From for MemoHash { + +impl From for MemoHash { fn from(value: T) -> Self { Self::new(value) } } -impl Hash for MemoHash { +impl Hash for MemoHash { fn hash(&self, state: &mut H) { self.hash.hash(state) } } -impl Deref for MemoHash { +impl CacheHash for MemoHash { + fn cache_hash(&self, state: &mut H) { + self.hash.hash(state); + } +} + +impl Deref for MemoHash { type Target = T; fn deref(&self) -> &Self::Target { @@ -79,18 +87,18 @@ impl Deref for MemoHash { } } -pub struct MemoHashGuard<'a, T: Hash> { +pub struct MemoHashGuard<'a, T: CacheHash> { inner: &'a mut MemoHash, } -impl Drop for MemoHashGuard<'_, T> { +impl Drop for MemoHashGuard<'_, T> { fn drop(&mut self) { let hash = MemoHash::::calc_hash(&self.inner.value); self.inner.hash = hash; } } -impl Deref for MemoHashGuard<'_, T> { +impl Deref for MemoHashGuard<'_, T> { type Target = T; fn deref(&self) -> &Self::Target { @@ -98,7 +106,7 @@ impl Deref for MemoHashGuard<'_, T> { } } -impl std::ops::DerefMut for MemoHashGuard<'_, T> { +impl std::ops::DerefMut for MemoHashGuard<'_, T> { fn deref_mut(&mut self) -> &mut Self::Target { Arc::make_mut(&mut self.inner.value) } diff --git a/node-graph/libraries/core-types/src/table.rs b/node-graph/libraries/core-types/src/table.rs index 4a814aaf58..648e6e0909 100644 --- a/node-graph/libraries/core-types/src/table.rs +++ b/node-graph/libraries/core-types/src/table.rs @@ -4,7 +4,6 @@ use crate::uuid::NodeId; use crate::{AlphaBlending, math::quad::Quad}; use dyn_any::{StaticType, StaticTypeSized}; use glam::DAffine2; -use std::hash::Hash; #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct Table { @@ -198,16 +197,16 @@ impl Default for Table { } } -impl Hash for Table { - fn hash(&self, state: &mut H) { +impl graphene_hash::CacheHash for Table { + fn cache_hash(&self, state: &mut H) { for element in &self.element { - element.hash(state); + element.cache_hash(state); } for transform in &self.transform { - transform.to_cols_array().map(|x| x.to_bits()).hash(state); + graphene_hash::CacheHash::cache_hash(transform, state); } for alpha_blending in &self.alpha_blending { - alpha_blending.hash(state); + alpha_blending.cache_hash(state); } } } diff --git a/node-graph/libraries/core-types/src/transform.rs b/node-graph/libraries/core-types/src/transform.rs index f92965ab97..c657b2958f 100644 --- a/node-graph/libraries/core-types/src/transform.rs +++ b/node-graph/libraries/core-types/src/transform.rs @@ -6,7 +6,7 @@ use glam::{DAffine2, DMat2, DVec2, UVec2}; /// Controls whether the Decompose Scale node returns axis-length magnitudes or pure scale factors. #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum ScaleType { /// The visual length of each axis (always positive, includes any skew contribution). @@ -141,7 +141,7 @@ impl TransformMut for Footprint { } } -#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize)] pub enum RenderQuality { /// Low quality, fast rendering Preview, @@ -154,7 +154,7 @@ pub enum RenderQuality { /// Render at full quality Full, } -#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, dyn_any::DynAny, PartialEq, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize)] pub struct Footprint { /// Inverse of the transform which will be applied to the node output during the rendering process pub transform: DAffine2, @@ -214,13 +214,6 @@ impl From<()> for Footprint { } } -impl std::hash::Hash for Footprint { - fn hash(&self, state: &mut H) { - self.transform.to_cols_array().iter().for_each(|x| x.to_le_bytes().hash(state)); - self.resolution.hash(state) - } -} - pub trait ApplyTransform { fn apply_transform(&mut self, modification: &DAffine2); fn left_apply_transform(&mut self, modification: &DAffine2); diff --git a/node-graph/libraries/core-types/src/types.rs b/node-graph/libraries/core-types/src/types.rs index ac0c5a6687..d3a395f6e6 100644 --- a/node-graph/libraries/core-types/src/types.rs +++ b/node-graph/libraries/core-types/src/types.rs @@ -77,7 +77,7 @@ macro_rules! fn_type_fut { } // TODO: Rename to NodeSignatureMonomorphization -#[derive(Clone, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, graphene_hash::CacheHash, Default, serde::Serialize, serde::Deserialize)] pub struct NodeIOTypes { pub call_argument: Type, pub return_value: Type, @@ -126,7 +126,7 @@ impl std::fmt::Debug for NodeIOTypes { } #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize)] pub struct ProtoNodeIdentifier { name: Cow<'static, str>, } @@ -200,6 +200,12 @@ impl std::hash::Hash for TypeDescriptor { } } +impl graphene_hash::CacheHash for TypeDescriptor { + fn cache_hash(&self, state: &mut H) { + graphene_hash::CacheHash::cache_hash(&self.name, state); + } +} + impl std::fmt::Display for TypeDescriptor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let text = make_type_user_readable(&simplify_identifier_name(&self.name)); @@ -222,7 +228,7 @@ impl PartialEq for TypeDescriptor { /// Graph runtime type information used for type inference. #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, Eq, Hash, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize)] pub enum Type { /// A wrapper for some type variable used within the inference system. Resolved at inference time and replaced with a concrete type. Generic(Cow<'static, str>), diff --git a/node-graph/libraries/core-types/src/uuid.rs b/node-graph/libraries/core-types/src/uuid.rs index 9ddab56d4c..6847c2c384 100644 --- a/node-graph/libraries/core-types/src/uuid.rs +++ b/node-graph/libraries/core-types/src/uuid.rs @@ -68,7 +68,7 @@ mod uuid_generation { #[repr(transparent)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, DynAny)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, graphene_hash::CacheHash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, DynAny)] pub struct NodeId(pub u64); impl NodeId { diff --git a/node-graph/libraries/graphene-hash/Cargo.toml b/node-graph/libraries/graphene-hash/Cargo.toml new file mode 100644 index 0000000000..f1827e57f2 --- /dev/null +++ b/node-graph/libraries/graphene-hash/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "graphene-hash" +version = "0.0.0" +edition = "2024" +authors = ["Graphite Authors "] +description = "CacheHash trait and derive macro for cache invalidation hashing in Graphite" +license = "MIT OR Apache-2.0" +publish = false + +[features] +default = ["std"] +std = [] +derive = ["graphene-hash-derive"] + +[dependencies] +graphene-hash-derive = { path = "derive", optional = true } +glam = { workspace = true } diff --git a/node-graph/libraries/graphene-hash/derive/Cargo.toml b/node-graph/libraries/graphene-hash/derive/Cargo.toml new file mode 100644 index 0000000000..e96fd50016 --- /dev/null +++ b/node-graph/libraries/graphene-hash/derive/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "graphene-hash-derive" +version = "0.0.0" +edition = "2024" +authors = ["Graphite Authors "] +description = "#[derive(CacheHash)]" +license = "MIT OR Apache-2.0" +publish = false + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true } + +[dev-dependencies] +graphene-hash = { path = "..", features = ["derive"] } diff --git a/node-graph/libraries/graphene-hash/derive/src/lib.rs b/node-graph/libraries/graphene-hash/derive/src/lib.rs new file mode 100644 index 0000000000..de66c93d72 --- /dev/null +++ b/node-graph/libraries/graphene-hash/derive/src/lib.rs @@ -0,0 +1,129 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{Data, DeriveInput, Fields, parse_macro_input}; + +/// Derives `CacheHash` for a struct or enum. +/// +/// All fields must implement `CacheHash`. Fields annotated with `#[cache_hash(skip)]` +/// are excluded from hashing. +/// +/// # Example +/// +/// ``` +/// # use graphene_hash::CacheHash; +/// #[derive(CacheHash)] +/// pub struct MyNode { +/// pub value: f64, +/// pub count: u32, +/// #[cache_hash(skip)] +/// pub debug_label: String, +/// } +/// ``` +#[proc_macro_derive(CacheHash, attributes(cache_hash))] +pub fn derive_cache_hash(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let name = &ast.ident; + let mut generics = ast.generics.clone(); + for param in &mut generics.params { + if let syn::GenericParam::Type(type_param) = param { + type_param.bounds.push(syn::parse_quote!(graphene_hash::CacheHash)); + } + } + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let body = match &ast.data { + Data::Struct(s) => hash_fields(&s.fields, quote! { self }), + Data::Enum(e) => { + let arms = e.variants.iter().map(|variant| { + let variant_name = &variant.ident; + let (pattern, hash_body) = match &variant.fields { + Fields::Unit => (quote! {}, quote! {}), + Fields::Unnamed(fields) => { + let bindings: Vec<_> = (0..fields.unnamed.len()) + .map(|i| { + let ident = proc_macro2::Ident::new(&format!("f{i}"), proc_macro2::Span::call_site()); + quote! { #ident } + }) + .collect(); + let hash_stmts = fields.unnamed.iter().enumerate().filter_map(|(i, field)| { + if has_skip_attr(&field.attrs) { + return None; + } + let ident = proc_macro2::Ident::new(&format!("f{i}"), proc_macro2::Span::call_site()); + Some(quote! { graphene_hash::CacheHash::cache_hash(#ident, state); }) + }); + (quote! { (#(#bindings,)*) }, quote! { #(#hash_stmts)* }) + } + Fields::Named(fields) => { + let names: Vec<_> = fields.named.iter().map(|f| f.ident.as_ref().unwrap()).collect(); + let hash_stmts = fields.named.iter().filter_map(|field| { + if has_skip_attr(&field.attrs) { + return None; + } + let ident = field.ident.as_ref().unwrap(); + Some(quote! { graphene_hash::CacheHash::cache_hash(#ident, state); }) + }); + (quote! { { #(#names,)* } }, quote! { #(#hash_stmts)* }) + } + }; + quote! { + Self::#variant_name #pattern => { #hash_body } + } + }); + quote! { + ::core::hash::Hash::hash(&::core::mem::discriminant(self), state); + match self { + #(#arms)* + } + } + } + Data::Union(_) => return syn::Error::new(ast.ident.span(), "CacheHash cannot be derived for unions").to_compile_error().into(), + }; + + quote! { + impl #impl_generics graphene_hash::CacheHash for #name #ty_generics #where_clause { + fn cache_hash(&self, state: &mut H) { + #body + } + } + } + .into() +} + +fn hash_fields(fields: &Fields, self_expr: TokenStream2) -> TokenStream2 { + match fields { + Fields::Unit => quote! {}, + Fields::Unnamed(fields) => { + let stmts = fields.unnamed.iter().enumerate().filter_map(|(i, field)| { + if has_skip_attr(&field.attrs) { + return None; + } + let index = syn::Index::from(i); + Some(quote! { graphene_hash::CacheHash::cache_hash(&#self_expr.#index, state); }) + }); + quote! { #(#stmts)* } + } + Fields::Named(fields) => { + let stmts = fields.named.iter().filter_map(|field| { + if has_skip_attr(&field.attrs) { + return None; + } + let ident = field.ident.as_ref().unwrap(); + Some(quote! { graphene_hash::CacheHash::cache_hash(&#self_expr.#ident, state); }) + }); + quote! { #(#stmts)* } + } + } +} + +fn has_skip_attr(attrs: &[syn::Attribute]) -> bool { + attrs.iter().any(|attr| { + if !attr.path().is_ident("cache_hash") { + return false; + } + attr.parse_args::().map(|id| id == "skip").unwrap_or(false) + }) +} diff --git a/node-graph/libraries/graphene-hash/src/lib.rs b/node-graph/libraries/graphene-hash/src/lib.rs new file mode 100644 index 0000000000..8b2c6ae6b5 --- /dev/null +++ b/node-graph/libraries/graphene-hash/src/lib.rs @@ -0,0 +1,237 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(feature = "std")] +extern crate std; + +#[cfg(feature = "derive")] +pub use graphene_hash_derive::CacheHash; + +pub trait CacheHash { + fn cache_hash(&self, state: &mut H); +} + +/// Wrapper that implements `std::hash::Hash` by delegating to `CacheHash`. +/// +/// Use this to store `CacheHash` types in `HashMap`/`HashSet` keys, +/// making it explicit that float fields are hashed via bit patterns. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CacheHashWrapper(pub T); + +impl core::hash::Hash for CacheHashWrapper { + fn hash(&self, state: &mut H) { + self.0.cache_hash(state); + } +} + +impl CacheHash for core::ops::RangeInclusive { + #[inline] + fn cache_hash(&self, state: &mut H) { + self.start().cache_hash(state); + self.end().cache_hash(state); + } +} + +impl core::ops::Deref for CacheHashWrapper { + type Target = T; + fn deref(&self) -> &T { + &self.0 + } +} + +// Bulk impl for types that already implement std::hash::Hash — delegates directly. +#[macro_export] +macro_rules! impl_via_hash { + ($($t:ty),* $(,)?) => { + $( + impl $crate::CacheHash for $t { + #[inline] + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(self, state); + } + } + )* + }; +} + +impl_via_hash! { + bool, char, + u8, u16, u32, u64, u128, usize, + i8, i16, i32, i64, i128, isize, + // glam integer vector types have Hash + glam::UVec2, glam::UVec3, glam::UVec4, + glam::IVec2, glam::IVec3, glam::IVec4, + glam::I64Vec2, glam::I64Vec3, glam::I64Vec4, + glam::U64Vec2, glam::U64Vec3, glam::U64Vec4, + glam::BVec2, glam::BVec3, glam::BVec4, +} + +#[cfg(feature = "std")] +impl_via_hash! { + String, +} + +impl<'a> CacheHash for std::borrow::Cow<'a, str> { + #[inline] + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(self, state); + } +} + +impl CacheHash for str { + #[inline] + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(self, state); + } +} + +impl CacheHash for () { + #[inline] + fn cache_hash(&self, _state: &mut H) {} +} + +// f32 and f64: hash via bit pattern so NaN is handled deterministically. +impl CacheHash for f32 { + #[inline] + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(&self.to_bits(), state); + } +} + +impl CacheHash for f64 { + #[inline] + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(&self.to_bits(), state); + } +} + +// glam float vector/matrix types: hash each component via to_bits(). +macro_rules! impl_glam_array { + ($($t:ty),* $(,)?) => { + $( + impl CacheHash for $t { + #[inline] + fn cache_hash(&self, state: &mut H) { + for v in self.to_array() { + CacheHash::cache_hash(&v, state); + } + } + } + )* + }; +} + +macro_rules! impl_glam_cols { + ($($t:ty),* $(,)?) => { + $( + impl CacheHash for $t { + #[inline] + fn cache_hash(&self, state: &mut H) { + for v in self.to_cols_array() { + CacheHash::cache_hash(&v, state); + } + } + } + )* + }; +} + +impl_glam_array! { + glam::Vec2, glam::Vec3, glam::Vec3A, glam::Vec4, + glam::DVec2, glam::DVec3, glam::DVec4, +} + +impl_glam_cols! { + glam::Mat2, glam::Mat3, glam::Mat3A, glam::Mat4, + glam::DMat2, glam::DMat3, glam::DMat4, + glam::Affine2, glam::Affine3A, + glam::DAffine2, glam::DAffine3, +} + +// Quat / DQuat — to_array gives [x, y, z, w] as floats +impl_glam_array! { + glam::Quat, glam::DQuat, +} + +// Generic container impls. +impl CacheHash for Option { + #[inline] + fn cache_hash(&self, state: &mut H) { + match self { + None => core::hash::Hash::hash(&0u8, state), + Some(v) => { + core::hash::Hash::hash(&1u8, state); + v.cache_hash(state); + } + } + } +} + +impl CacheHash for [T] { + #[inline] + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(&self.len(), state); + for item in self { + item.cache_hash(state); + } + } +} + +impl CacheHash for [T; N] { + #[inline] + fn cache_hash(&self, state: &mut H) { + for item in self { + item.cache_hash(state); + } + } +} + +#[cfg(feature = "std")] +impl CacheHash for Vec { + #[inline] + fn cache_hash(&self, state: &mut H) { + self.as_slice().cache_hash(state); + } +} + +#[cfg(feature = "std")] +impl CacheHash for Box { + #[inline] + fn cache_hash(&self, state: &mut H) { + (**self).cache_hash(state); + } +} + +#[cfg(feature = "std")] +impl CacheHash for std::sync::Arc { + #[inline] + fn cache_hash(&self, state: &mut H) { + (**self).cache_hash(state); + } +} + +impl CacheHash for &T { + #[inline] + fn cache_hash(&self, state: &mut H) { + (**self).cache_hash(state); + } +} + +// Tuple impls. +macro_rules! impl_tuple { + ($($T:ident),+) => { + impl<$($T: CacheHash),+> CacheHash for ($($T,)+) { + #[inline] + #[allow(non_snake_case)] + fn cache_hash(&self, state: &mut H) { + let ($($T,)+) = self; + $($T.cache_hash(state);)+ + } + } + }; +} + +impl_tuple!(A, B); +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); diff --git a/node-graph/libraries/graphic-types/Cargo.toml b/node-graph/libraries/graphic-types/Cargo.toml index 1dac611f07..6140e239f3 100644 --- a/node-graph/libraries/graphic-types/Cargo.toml +++ b/node-graph/libraries/graphic-types/Cargo.toml @@ -18,6 +18,7 @@ wasm = [ [dependencies] # Local dependencies core-types = { workspace = true } +graphene-hash = { workspace = true } raster-types = { workspace = true, features = ["wgpu"] } vector-types = { workspace = true } node-macro = { workspace = true } diff --git a/node-graph/libraries/graphic-types/src/artboard.rs b/node-graph/libraries/graphic-types/src/artboard.rs index 7595f2cd52..d42c71842c 100644 --- a/node-graph/libraries/graphic-types/src/artboard.rs +++ b/node-graph/libraries/graphic-types/src/artboard.rs @@ -9,10 +9,10 @@ use core_types::transform::Transform; use core_types::uuid::NodeId; use dyn_any::DynAny; use glam::{DAffine2, DVec2, IVec2}; -use std::hash::Hash; +use graphene_hash::CacheHash; /// Some [`ArtboardData`] with some optional clipping bounds that can be exported. -#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, CacheHash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] pub struct Artboard { pub content: Table, pub label: String, @@ -76,7 +76,7 @@ impl Transform for Artboard { pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result, D::Error> { use serde::Deserialize; - #[derive(Clone, Default, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Default, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] pub struct ArtboardGroup { pub artboards: Vec<(Artboard, Option)>, } diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 878d702fdf..776d1ce316 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -1,6 +1,7 @@ use core_types::Color; use core_types::blending::AlphaBlending; use core_types::bounds::{BoundingBox, RenderBoundingBox}; +use core_types::graphene_hash::CacheHash; use core_types::ops::TableConvert; use core_types::render_complexity::RenderComplexity; use core_types::table::{Table, TableRow}; @@ -8,14 +9,13 @@ use core_types::uuid::NodeId; use dyn_any::DynAny; use glam::DAffine2; use raster_types::{CPU, GPU, Raster}; -use std::hash::Hash; use vector_types::GradientStops; // use vector_types::Vector; pub type Vector = vector_types::Vector>>; /// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax. -#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, CacheHash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] pub enum Graphic { Graphic(Table), Vector(Table), diff --git a/node-graph/libraries/no-std-types/Cargo.toml b/node-graph/libraries/no-std-types/Cargo.toml index 10d8bf67e1..3258cc0328 100644 --- a/node-graph/libraries/no-std-types/Cargo.toml +++ b/node-graph/libraries/no-std-types/Cargo.toml @@ -14,6 +14,7 @@ license = "MIT OR Apache-2.0" # should be in this list instead of `[workspace.dependency]` std = [ "dep:dyn-any", + "dep:graphene-hash", "dep:serde", "dep:log", "glam/debug-glam-assert", @@ -32,6 +33,7 @@ node-macro = { workspace = true } # Local std dependencies dyn-any = { workspace = true, optional = true } +graphene-hash = { workspace = true, optional = true } # Workspace dependencies bytemuck = { workspace = true } diff --git a/node-graph/libraries/no-std-types/src/blending.rs b/node-graph/libraries/no-std-types/src/blending.rs index f6bb2965af..6033a7909f 100644 --- a/node-graph/libraries/no-std-types/src/blending.rs +++ b/node-graph/libraries/no-std-types/src/blending.rs @@ -1,12 +1,11 @@ use core::fmt::Display; -use core::hash::{Hash, Hasher}; use node_macro::BufferStruct; use num_enum::{FromPrimitive, IntoPrimitive}; #[cfg(not(feature = "std"))] use num_traits::float::Float; #[derive(Debug, Clone, Copy, PartialEq, BufferStruct)] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize, graphene_hash::CacheHash))] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[cfg_attr(feature = "std", serde(default))] pub struct AlphaBlending { @@ -20,14 +19,6 @@ impl Default for AlphaBlending { Self::new() } } -impl Hash for AlphaBlending { - fn hash(&self, state: &mut H) { - self.opacity.to_bits().hash(state); - self.fill.to_bits().hash(state); - self.blend_mode.hash(state); - self.clip.hash(state); - } -} impl Display for AlphaBlending { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let round = |x: f32| (x * 1e3).round() / 1e3; @@ -71,6 +62,7 @@ impl AlphaBlending { #[repr(i32)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "std", derive(graphene_hash::CacheHash))] #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, BufferStruct, FromPrimitive, IntoPrimitive)] pub enum BlendMode { // Basic group diff --git a/node-graph/libraries/no-std-types/src/color/color_types.rs b/node-graph/libraries/no-std-types/src/color/color_types.rs index abcb1e9e86..86a4d50f58 100644 --- a/node-graph/libraries/no-std-types/src/color/color_types.rs +++ b/node-graph/libraries/no-std-types/src/color/color_types.rs @@ -2,7 +2,6 @@ use super::color_traits::{Alpha, AlphaMut, AssociatedAlpha, Luminance, Luminance use super::discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float}; use bytemuck::{Pod, Zeroable}; use core::fmt::Debug; -use core::hash::Hash; use glam::Vec4; use half::f16; use node_macro::BufferStruct; @@ -220,6 +219,7 @@ impl Pixel for Luma {} #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "std", derive(graphene_hash::CacheHash))] #[derive(Debug, Default, Clone, Copy, Pod, Zeroable, BufferStruct)] pub struct Color { red: f32, @@ -236,16 +236,6 @@ impl PartialEq for Color { impl Eq for Color {} -#[allow(clippy::derived_hash_with_manual_eq)] -impl Hash for Color { - fn hash(&self, state: &mut H) { - self.red.to_bits().hash(state); - self.green.to_bits().hash(state); - self.blue.to_bits().hash(state); - self.alpha.to_bits().hash(state); - } -} - impl RGB for Color { type ColorChannel = f32; #[inline(always)] diff --git a/node-graph/libraries/raster-types/Cargo.toml b/node-graph/libraries/raster-types/Cargo.toml index 0039481667..c7377beb6e 100644 --- a/node-graph/libraries/raster-types/Cargo.toml +++ b/node-graph/libraries/raster-types/Cargo.toml @@ -14,6 +14,7 @@ wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] # Local dependencies core-types = { workspace = true } +graphene-hash = { workspace = true } node-macro = { workspace = true } # Workspace dependencies diff --git a/node-graph/libraries/raster-types/src/image.rs b/node-graph/libraries/raster-types/src/image.rs index 420b2439af..85b57bc064 100644 --- a/node-graph/libraries/raster-types/src/image.rs +++ b/node-graph/libraries/raster-types/src/image.rs @@ -5,7 +5,6 @@ use core_types::Color; use core_types::color::float_to_srgb_u8; use core_types::table::{Table, TableRow}; // use crate::vector::Vector; // TODO: Check if Vector is actually used, if so handle differently -use core::hash::{Hash, Hasher}; use core_types::color::*; use dyn_any::{DynAny, StaticType}; use glam::{DAffine2, DVec2}; @@ -64,8 +63,10 @@ impl PartialEq for Image

{ #[derive(Debug, Clone, dyn_any::DynAny, Default, PartialEq, serde::Serialize, serde::Deserialize)] pub struct TransformImage(pub DAffine2); -impl Hash for TransformImage { - fn hash(&self, _: &mut H) {} +impl core_types::CacheHash for TransformImage { + fn cache_hash(&self, state: &mut H) { + core_types::CacheHash::cache_hash(&self.0, state); + } } impl std::fmt::Debug for Image

{ @@ -109,11 +110,11 @@ impl BitmapMut for Image

{ } } -impl Hash for Image

{ - fn hash(&self, state: &mut H) { - self.width.hash(state); - self.height.hash(state); - self.data.hash(state); +impl core_types::CacheHash for Image

{ + fn cache_hash(&self, state: &mut H) { + core_types::CacheHash::cache_hash(&self.width, state); + core_types::CacheHash::cache_hash(&self.height, state); + core_types::CacheHash::cache_hash(&self.data, state); } } @@ -220,7 +221,7 @@ impl IntoIterator for Image

{ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result>, D::Error> { use serde::Deserialize; - #[derive(Clone, Debug, Hash, PartialEq, DynAny)] + #[derive(Clone, Debug, core_types::CacheHash, PartialEq, DynAny)] enum RasterFrame { ImageFrame(Table>), } @@ -237,7 +238,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> } } - #[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Debug, core_types::CacheHash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] pub enum GraphicElement { GraphicGroup(Table), RasterFrame(RasterFrame), @@ -372,7 +373,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result>, D::Error> { use serde::Deserialize; - #[derive(Clone, Debug, Hash, PartialEq, DynAny)] + #[derive(Clone, Debug, PartialEq, DynAny)] enum RasterFrame { /// A CPU-based bitmap image with a finite position and extent, equivalent to the SVG tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image ImageFrame(Table>), @@ -390,7 +391,7 @@ pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D } } - #[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] pub enum GraphicElement { /// Equivalent to the SVG tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g GraphicGroup(Table), diff --git a/node-graph/libraries/raster-types/src/raster_types.rs b/node-graph/libraries/raster-types/src/raster_types.rs index 918df0cec8..7372b3b76f 100644 --- a/node-graph/libraries/raster-types/src/raster_types.rs +++ b/node-graph/libraries/raster-types/src/raster_types.rs @@ -16,7 +16,7 @@ pub trait Storage: __private::Sealed + Clone + Debug + 'static { fn is_empty(&self) -> bool; } -#[derive(Clone, Debug, PartialEq, Hash, Default)] +#[derive(Clone, Debug, PartialEq, Default)] pub struct Raster where Raster: Storage, @@ -60,13 +60,23 @@ where } } +impl core_types::CacheHash for Raster +where + Raster: Storage, + T: core_types::CacheHash, +{ + fn cache_hash(&self, state: &mut H) { + core_types::CacheHash::cache_hash(&self.storage, state); + } +} + pub use cpu::CPU; mod cpu { use super::*; use crate::raster_types::__private::Sealed; - #[derive(Clone, Debug, Default, PartialEq, Hash, DynAny)] + #[derive(Clone, Debug, Default, PartialEq, core_types::CacheHash, DynAny)] pub struct CPU(Image); impl Sealed for Raster {} @@ -140,6 +150,13 @@ mod gpu { pub texture: wgpu::Texture, } + impl core_types::CacheHash for GPU { + fn cache_hash(&self, state: &mut H) { + use ::core::hash::Hash; + self.texture.hash(state); + } + } + impl Sealed for Raster {} impl Storage for Raster { @@ -164,7 +181,7 @@ mod gpu { use super::*; use crate::raster_types::__private::Sealed; - #[derive(Clone, Debug, PartialEq, Hash)] + #[derive(Clone, Debug, PartialEq, Hash, core_types::CacheHash)] pub struct GPU; impl Sealed for Raster {} diff --git a/node-graph/libraries/rendering/Cargo.toml b/node-graph/libraries/rendering/Cargo.toml index e33ce052fd..1926c39f0d 100644 --- a/node-graph/libraries/rendering/Cargo.toml +++ b/node-graph/libraries/rendering/Cargo.toml @@ -10,6 +10,7 @@ license = "MIT OR Apache-2.0" # Local dependencies dyn-any = { workspace = true } core-types = { workspace = true } +graphene-hash = { workspace = true } # Workspace dependencies glam = { workspace = true } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 6f1690ad37..4dc9d658c6 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -1,5 +1,6 @@ use crate::render_ext::RenderExt; use crate::to_peniko::BlendModeExt; +use core_types::CacheHash; use core_types::blending::BlendMode; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::color::{Alpha, Color}; @@ -10,6 +11,7 @@ use core_types::transform::{Footprint, Transform}; use core_types::uuid::{NodeId, generate_uuid}; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; +use graphene_hash::CacheHashWrapper; use graphic_types::Vector; use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster}; use graphic_types::vector_types::gradient::{GradientStops, GradientType}; @@ -21,7 +23,6 @@ use kurbo::{Affine, Cap, Join, Shape}; use num_traits::Zero; use std::collections::{HashMap, HashSet}; use std::fmt::Write; -use std::hash::{Hash, Hasher}; use std::ops::Deref; use std::sync::{Arc, LazyLock}; use vector_types::gradient::GradientSpreadMethod; @@ -95,7 +96,7 @@ pub struct SvgRender { pub svg: Vec, pub svg_defs: String, pub transform: DAffine2, - pub image_data: HashMap, u64>, + pub image_data: HashMap>, u64>, indent: usize, } @@ -191,7 +192,7 @@ pub struct RenderContext { pub resource_overrides: Vec<(peniko::ImageBrush, wgpu::Texture)>, } -#[derive(Default, Clone, Copy, Hash)] +#[derive(Default, Clone, Copy, Hash, graphene_hash::CacheHash)] pub enum RenderOutputType { #[default] Svg, @@ -199,12 +200,13 @@ pub enum RenderOutputType { } /// Static state used whilst rendering -#[derive(Default, Clone)] +#[derive(Default, Clone, CacheHash)] pub struct RenderParams { pub render_mode: RenderMode, pub footprint: Footprint, /// Ratio of physical pixels to logical pixels. `scale := physical_pixels / logical_pixels` /// Ignored when rendering to SVG. + #[cache_hash(skip)] pub scale: f64, pub render_output_type: RenderOutputType, pub thumbnail: bool, @@ -223,25 +225,6 @@ pub struct RenderParams { pub viewport_zoom: f64, } -impl Hash for RenderParams { - fn hash(&self, state: &mut H) { - self.render_mode.hash(state); - self.footprint.hash(state); - self.render_output_type.hash(state); - self.thumbnail.hash(state); - self.hide_artboards.hash(state); - self.for_export.hash(state); - self.for_mask.hash(state); - if let Some(x) = self.alignment_parent_transform { - x.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)) - } - self.aligned_strokes.hash(state); - self.override_paint_order.hash(state); - self.artboard_background.hash(state); - self.viewport_zoom.to_bits().hash(state); - } -} - impl RenderParams { pub fn for_clipper(&self) -> Self { Self { for_mask: true, ..*self } @@ -1426,7 +1409,7 @@ impl Render for Table> { if render_params.to_canvas() { let mut image_copy = image.clone(); image_copy.data_mut().map_pixels(|p| p.to_unassociated_alpha()); - let id = *render.image_data.entry(image_copy.into_data()).or_insert_with(generate_uuid); + let id = *render.image_data.entry(CacheHashWrapper(image_copy.into_data())).or_insert_with(generate_uuid); render.parent_tag( "foreignObject", diff --git a/node-graph/libraries/vector-types/Cargo.toml b/node-graph/libraries/vector-types/Cargo.toml index 5212fb2386..7b64a9c039 100644 --- a/node-graph/libraries/vector-types/Cargo.toml +++ b/node-graph/libraries/vector-types/Cargo.toml @@ -13,6 +13,7 @@ wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] # Local dependencies core-types = { workspace = true } +graphene-hash = { workspace = true } node-macro = { workspace = true } # Workspace dependencies diff --git a/node-graph/libraries/vector-types/src/gradient.rs b/node-graph/libraries/vector-types/src/gradient.rs index f5c241c2f0..39e8d7701e 100644 --- a/node-graph/libraries/vector-types/src/gradient.rs +++ b/node-graph/libraries/vector-types/src/gradient.rs @@ -3,7 +3,7 @@ use dyn_any::DynAny; use glam::{DAffine2, DVec2}; #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, node_macro::ChoiceType)] +#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum GradientType { #[default] @@ -15,7 +15,7 @@ pub enum GradientType { // TODO: Use linear not gamma colors /// A list of colors associated with positions (in the range 0 to 1) along a gradient. #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, PartialEq, serde::Serialize, DynAny)] +#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, serde::Serialize, DynAny)] pub struct GradientStops { /// The position of this stop, a factor from 0-1 along the length of the full gradient. pub position: Vec, @@ -60,17 +60,6 @@ impl<'de> serde::Deserialize<'de> for GradientStops { } } -impl std::hash::Hash for GradientStops { - fn hash(&self, state: &mut H) { - self.position.len().hash(state); - for i in 0..self.position.len() { - self.position[i].to_bits().hash(state); - self.midpoint[i].to_bits().hash(state); - self.color[i].hash(state); - } - } -} - impl Default for GradientStops { fn default() -> Self { Self { @@ -336,7 +325,7 @@ impl GradientStops { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, node_macro::ChoiceType)] +#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum GradientSpreadMethod { #[default] @@ -360,7 +349,7 @@ impl GradientSpreadMethod { /// Contains the start and end points, along with the colors at varying points along the length. #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny)] +#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize, DynAny)] pub struct Gradient { pub stops: GradientStops, pub gradient_type: GradientType, @@ -382,21 +371,6 @@ impl Default for Gradient { } } -impl std::hash::Hash for Gradient { - fn hash(&self, state: &mut H) { - self.stops.len().hash(state); - [].iter() - .chain(self.start.to_array().iter()) - .chain(self.end.to_array().iter()) - .chain(self.stops.position.iter()) - .chain(self.stops.midpoint.iter()) - .for_each(|x| x.to_bits().hash(state)); - self.stops.color.iter().for_each(|color| color.hash(state)); - self.gradient_type.hash(state); - self.spread_method.hash(state); - } -} - impl std::fmt::Display for Gradient { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let round = |x: f64| (x * 1e3).round() / 1e3; diff --git a/node-graph/libraries/vector-types/src/subpath/mod.rs b/node-graph/libraries/vector-types/src/subpath/mod.rs index 1e50e32f40..80ea7cd241 100644 --- a/node-graph/libraries/vector-types/src/subpath/mod.rs +++ b/node-graph/libraries/vector-types/src/subpath/mod.rs @@ -13,7 +13,7 @@ use std::ops::{Index, IndexMut}; pub use structs::*; /// Structure used to represent a path composed of [Bezier] curves. -#[derive(Clone, PartialEq, Hash)] +#[derive(Clone, PartialEq, graphene_hash::CacheHash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Subpath { manipulator_groups: Vec>, diff --git a/node-graph/libraries/vector-types/src/subpath/structs.rs b/node-graph/libraries/vector-types/src/subpath/structs.rs index 1498402292..11d72b0e2b 100644 --- a/node-graph/libraries/vector-types/src/subpath/structs.rs +++ b/node-graph/libraries/vector-types/src/subpath/structs.rs @@ -6,12 +6,12 @@ use std::fmt::{Debug, Formatter, Result}; use std::hash::Hash; /// An id type used for each [ManipulatorGroup]. -pub trait Identifier: Sized + Clone + PartialEq + Hash + 'static { +pub trait Identifier: Sized + Clone + PartialEq + Hash + graphene_hash::CacheHash + 'static { fn new() -> Self; } /// Structure used to represent a single anchor with up to two optional associated handles along a `Subpath` -#[derive(Copy, Clone, PartialEq)] +#[derive(Copy, Clone, PartialEq, graphene_hash::CacheHash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ManipulatorGroup { pub anchor: DVec2, @@ -20,22 +20,6 @@ pub struct ManipulatorGroup { pub id: PointId, } -// TODO: Remove once we no longer need to hash floats in Graphite -impl Hash for ManipulatorGroup { - fn hash(&self, state: &mut H) { - self.anchor.to_array().iter().for_each(|x| x.to_bits().hash(state)); - self.in_handle.is_some().hash(state); - if let Some(in_handle) = self.in_handle { - in_handle.to_array().iter().for_each(|x| x.to_bits().hash(state)); - } - self.out_handle.is_some().hash(state); - if let Some(out_handle) = self.out_handle { - out_handle.to_array().iter().for_each(|x| x.to_bits().hash(state)); - } - self.id.hash(state); - } -} - impl Debug for ManipulatorGroup { fn fmt(&self, f: &mut Formatter<'_>) -> Result { f.debug_struct("ManipulatorGroup") @@ -119,7 +103,7 @@ pub enum AppendType { SmoothJoin(f64), } -#[derive(Copy, Clone, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, graphene_hash::CacheHash)] pub enum ArcType { Open, Closed, @@ -127,7 +111,7 @@ pub enum ArcType { } /// Representation of the handle point(s) in a bezier segment. -#[derive(Copy, Clone, PartialEq, Debug)] +#[derive(Copy, Clone, PartialEq, Debug, graphene_hash::CacheHash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum BezierHandles { Linear, @@ -145,17 +129,6 @@ pub enum BezierHandles { }, } -impl std::hash::Hash for BezierHandles { - fn hash(&self, state: &mut H) { - std::mem::discriminant(self).hash(state); - match self { - BezierHandles::Linear => {} - BezierHandles::Quadratic { handle } => handle.to_array().map(|v| v.to_bits()).hash(state), - BezierHandles::Cubic { handle_start, handle_end } => [handle_start, handle_end].map(|handle| handle.to_array().map(|v| v.to_bits())).hash(state), - } - } -} - impl BezierHandles { pub fn is_cubic(&self) -> bool { matches!(self, Self::Cubic { .. }) diff --git a/node-graph/libraries/vector-types/src/vector/misc.rs b/node-graph/libraries/vector-types/src/vector/misc.rs index 7f5711da75..23cc4babd2 100644 --- a/node-graph/libraries/vector-types/src/vector/misc.rs +++ b/node-graph/libraries/vector-types/src/vector/misc.rs @@ -368,7 +368,7 @@ impl Tangent for kurbo::PathSeg { } /// A selectable part of a curve, either an anchor (start or end of a bézier) or a handle (doesn't necessarily go through the bézier but influences curvature). -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, graphene_hash::CacheHash, Debug, DynAny, serde::Serialize, serde::Deserialize)] pub enum ManipulatorPointId { /// A control anchor - the start or end point of a bézier. Anchor(PointId), @@ -479,7 +479,7 @@ impl ManipulatorPointId { } /// The type of handle found on a bézier curve. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, graphene_hash::CacheHash, Debug, DynAny, serde::Serialize, serde::Deserialize)] pub enum HandleType { /// The first handle on a cubic bézier or the only handle on a quadratic bézier. Primary, @@ -488,7 +488,7 @@ pub enum HandleType { } /// Represents a primary or end handle found in a particular segment. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, graphene_hash::CacheHash, Debug, DynAny, serde::Serialize, serde::Deserialize)] pub struct HandleId { pub ty: HandleType, pub segment: SegmentId, @@ -572,3 +572,16 @@ pub enum InterpolationDistribution { /// All slants (changes in skew angle) between objects are covered at a constant rate, meaning more time is spent skewing through larger changes in slant. Slants, } + +graphene_hash::impl_via_hash!( + BooleanOperation, + CentroidType, + RowsOrColumns, + GridType, + ArcType, + MergeByDistanceAlgorithm, + ExtrudeJoiningAlgorithm, + PointSpacingType, + SpiralType, + InterpolationDistribution +); diff --git a/node-graph/libraries/vector-types/src/vector/reference_point.rs b/node-graph/libraries/vector-types/src/vector/reference_point.rs index 094155918c..728070a75a 100644 --- a/node-graph/libraries/vector-types/src/vector/reference_point.rs +++ b/node-graph/libraries/vector-types/src/vector/reference_point.rs @@ -2,7 +2,7 @@ use core_types::math::bbox::AxisAlignedBbox; use glam::DVec2; #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Copy, Debug, Default, Hash, graphene_hash::CacheHash, Eq, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] pub enum ReferencePoint { #[default] None, diff --git a/node-graph/libraries/vector-types/src/vector/style.rs b/node-graph/libraries/vector-types/src/vector/style.rs index 0828c4e6f2..820a2b1414 100644 --- a/node-graph/libraries/vector-types/src/vector/style.rs +++ b/node-graph/libraries/vector-types/src/vector/style.rs @@ -16,7 +16,7 @@ use std::f64::consts::{PI, TAU}; /// In the future we'll probably also add a pattern fill. This will probably be named "Paint" in the future. #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash)] +#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize, DynAny)] pub enum Fill { #[default] None, @@ -161,7 +161,7 @@ impl From for Fill { /// In the future we'll probably also add a pattern fill. #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash)] +#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize, DynAny)] pub enum FillChoice { #[default] None, @@ -209,7 +209,7 @@ impl From for FillChoice { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, graphene_hash::CacheHash, node_macro::ChoiceType)] #[widget(Radio)] pub enum FillType { #[default] @@ -220,7 +220,7 @@ pub enum FillType { /// The stroke (outline) style of an SVG element. #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum StrokeCap { #[default] @@ -241,7 +241,7 @@ impl StrokeCap { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum StrokeJoin { #[default] @@ -262,7 +262,7 @@ impl StrokeJoin { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum StrokeAlign { #[default] @@ -279,7 +279,7 @@ impl StrokeAlign { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum PaintOrder { #[default] @@ -299,7 +299,7 @@ fn daffine2_identity() -> DAffine2 { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny)] +#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize, DynAny)] #[serde(default)] pub struct Stroke { /// Stroke color @@ -322,24 +322,6 @@ pub struct Stroke { pub paint_order: PaintOrder, } -impl std::hash::Hash for Stroke { - fn hash(&self, state: &mut H) { - self.color.hash(state); - self.weight.to_bits().hash(state); - { - self.dash_lengths.len().hash(state); - self.dash_lengths.iter().for_each(|length| length.to_bits().hash(state)); - } - self.dash_offset.to_bits().hash(state); - self.cap.hash(state); - self.join.hash(state); - self.join_miter_limit.to_bits().hash(state); - self.align.hash(state); - self.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)); - self.paint_order.hash(state); - } -} - impl Stroke { pub const fn new(color: Option, weight: f64) -> Self { Self { @@ -512,19 +494,12 @@ impl Default for Stroke { #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize, DynAny)] +#[derive(Debug, Clone, PartialEq, Default, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize, DynAny)] pub struct PathStyle { pub stroke: Option, pub fill: Fill, } -impl std::hash::Hash for PathStyle { - fn hash(&self, state: &mut H) { - self.stroke.hash(state); - self.fill.hash(state); - } -} - impl std::fmt::Display for PathStyle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let fill = &self.fill; @@ -680,7 +655,7 @@ impl PathStyle { /// Ways the user can choose to view the artwork in the viewport. #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, graphene_hash::CacheHash, DynAny)] pub enum RenderMode { /// Render with normal coloration at the current viewport resolution #[default] diff --git a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs index a2f8edd188..063b88f767 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs @@ -13,7 +13,7 @@ use std::iter::zip; macro_rules! create_ids { ($($id:ident),*) => { $( - #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq, Hash, DynAny)] + #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq, Hash, graphene_hash::CacheHash, DynAny)] #[derive(serde::Serialize, serde::Deserialize)] /// A strongly typed ID pub struct $id(u64); @@ -79,7 +79,7 @@ impl std::hash::BuildHasher for NoHashBuilder { } } -#[derive(Clone, Debug, Default, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, graphene_hash::CacheHash, DynAny, serde::Serialize, serde::Deserialize)] /// Stores data which is per-point. Each point is merely a position and can be used in a point cloud or to for a bézier path. In future this will be extendable at runtime with custom attributes. pub struct PointDomain { id: Vec, @@ -87,13 +87,6 @@ pub struct PointDomain { pub(crate) position: Vec, } -impl Hash for PointDomain { - fn hash(&self, state: &mut H) { - self.id.hash(state); - self.position.iter().for_each(|pos| pos.to_array().map(|v| v.to_bits()).hash(state)); - } -} - impl PointDomain { pub const fn new() -> Self { Self { id: Vec::new(), position: Vec::new() } @@ -212,7 +205,7 @@ impl PointDomain { } } -#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, graphene_hash::CacheHash, DynAny, serde::Serialize, serde::Deserialize)] /// Stores data which is per-segment. A segment is a bézier curve between two end points with a stroke. In future this will be extendable at runtime with custom attributes. pub struct SegmentDomain { #[serde(alias = "ids")] @@ -594,7 +587,7 @@ impl SegmentDomain { } } -#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, Hash, graphene_hash::CacheHash, DynAny, serde::Serialize, serde::Deserialize)] /// Stores data which is per-region. A region is an enclosed area composed of a range of segments from the /// [`SegmentDomain`] that can be given a fill. In future this will be extendable at runtime with custom attributes. pub struct RegionDomain { @@ -849,7 +842,7 @@ struct Faces { face_start: Vec, } -#[derive(Debug, Clone, PartialEq, Hash)] +#[derive(Debug, Clone, PartialEq)] pub struct FaceIterator<'a, Upstream> { vector: &'a Vector, faces: Faces, diff --git a/node-graph/libraries/vector-types/src/vector/vector_modification.rs b/node-graph/libraries/vector-types/src/vector/vector_modification.rs index f9d094223f..735e120c25 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_modification.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_modification.rs @@ -17,12 +17,6 @@ pub struct PointModification { delta: HashMap, } -impl Hash for PointModification { - fn hash(&self, state: &mut H) { - generate_uuid().hash(state) - } -} - impl PointModification { /// Apply this modification to the specified [`PointDomain`]. pub fn apply(&self, point_domain: &mut PointDomain, segment_domain: &mut SegmentDomain) { @@ -511,9 +505,13 @@ impl VectorModification { } } -impl Hash for VectorModification { - fn hash(&self, state: &mut H) { - generate_uuid().hash(state) +// Intentionally non-deterministic: fields contain HashMaps with non-deterministic iteration order, +// so we use a UUID to always bust the cache and force re-evaluation when any modification is present. +// This will not actually lead to a cache invalidation in most cases due to the +// graph inputs being wrapped in a `MemoHash` wrapper. +impl graphene_hash::CacheHash for VectorModification { + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(&generate_uuid(), state); } } diff --git a/node-graph/libraries/vector-types/src/vector/vector_types.rs b/node-graph/libraries/vector-types/src/vector/vector_types.rs index e9b8bbb670..e85d707657 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_types.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_types.rs @@ -54,13 +54,13 @@ impl Default for Vector { } } -impl std::hash::Hash for Vector { - fn hash(&self, state: &mut H) { - self.point_domain.hash(state); - self.segment_domain.hash(state); - self.region_domain.hash(state); - self.style.hash(state); - self.colinear_manipulators.hash(state); +impl graphene_hash::CacheHash for Vector { + fn cache_hash(&self, state: &mut H) { + self.point_domain.cache_hash(state); + self.segment_domain.cache_hash(state); + self.region_domain.cache_hash(state); + self.style.cache_hash(state); + self.colinear_manipulators.cache_hash(state); // We don't hash the upstream_data intentionally } } diff --git a/node-graph/nodes/brush/Cargo.toml b/node-graph/nodes/brush/Cargo.toml index 59e0852567..372edf0daa 100644 --- a/node-graph/nodes/brush/Cargo.toml +++ b/node-graph/nodes/brush/Cargo.toml @@ -14,6 +14,7 @@ serde = ["dep:serde"] # Local dependencies dyn-any = { workspace = true } core-types = { workspace = true } +graphene-hash = { workspace = true } raster-types = { workspace = true } raster-nodes = { workspace = true } node-macro = { workspace = true } diff --git a/node-graph/nodes/brush/src/brush_cache.rs b/node-graph/nodes/brush/src/brush_cache.rs index f90618830b..d05fa5c10d 100644 --- a/node-graph/nodes/brush/src/brush_cache.rs +++ b/node-graph/nodes/brush/src/brush_cache.rs @@ -1,5 +1,6 @@ use crate::brush_stroke::BrushStroke; use crate::brush_stroke::BrushStyle; +use core_types::graphene_hash::CacheHashWrapper; use core_types::table::TableRow; use dyn_any::DynAny; use raster_types::CPU; @@ -31,7 +32,7 @@ struct BrushCacheImpl { // A cache for brush textures. #[serde(skip)] - brush_texture_cache: HashMap>, + brush_texture_cache: HashMap, Raster>, } impl BrushCacheImpl { @@ -165,6 +166,12 @@ impl Hash for BrushCache { } } +impl graphene_hash::CacheHash for BrushCache { + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(&self.0.lock().unwrap().unique_id, state); + } +} + impl BrushCache { pub fn compute_brush_plan(&self, background: TableRow>, input: &[BrushStroke]) -> BrushPlan { let mut inner = self.0.lock().unwrap(); @@ -178,11 +185,11 @@ impl BrushCache { pub fn get_cached_brush(&self, style: &BrushStyle) -> Option> { let inner = self.0.lock().unwrap(); - inner.brush_texture_cache.get(style).cloned() + inner.brush_texture_cache.get(&CacheHashWrapper(style.clone())).cloned() } pub fn store_brush(&self, style: BrushStyle, brush: Raster) { let mut inner = self.0.lock().unwrap(); - inner.brush_texture_cache.insert(style, brush); + inner.brush_texture_cache.insert(CacheHashWrapper(style), brush); } } diff --git a/node-graph/nodes/brush/src/brush_stroke.rs b/node-graph/nodes/brush/src/brush_stroke.rs index c69360148d..9ad19dbb33 100644 --- a/node-graph/nodes/brush/src/brush_stroke.rs +++ b/node-graph/nodes/brush/src/brush_stroke.rs @@ -1,12 +1,11 @@ +use core_types::CacheHash; use core_types::blending::BlendMode; use core_types::color::Color; use core_types::math::bbox::AxisAlignedBbox; use dyn_any::DynAny; use glam::DVec2; -use std::hash::{Hash, Hasher}; - /// The style of a brush. -#[derive(Clone, Debug, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, CacheHash, DynAny, serde::Serialize, serde::Deserialize)] pub struct BrushStyle { pub color: Color, pub diameter: f64, @@ -29,17 +28,6 @@ impl Default for BrushStyle { } } -impl Hash for BrushStyle { - fn hash(&self, state: &mut H) { - self.color.hash(state); - self.diameter.to_bits().hash(state); - self.hardness.to_bits().hash(state); - self.flow.to_bits().hash(state); - self.spacing.to_bits().hash(state); - self.blend_mode.hash(state); - } -} - impl Eq for BrushStyle {} impl PartialEq for BrushStyle { @@ -54,23 +42,13 @@ impl PartialEq for BrushStyle { } /// A single sample of brush parameters across the brush stroke. -#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)] pub struct BrushInputSample { - // The position of the sample in layer space, in pixels. - // The origin of layer space is not specified. pub position: DVec2, - // Future work: pressure, stylus angle, etc. -} - -impl Hash for BrushInputSample { - fn hash(&self, state: &mut H) { - self.position.x.to_bits().hash(state); - self.position.y.to_bits().hash(state); - } } /// The parameters for a single stroke brush. -#[derive(Clone, Debug, PartialEq, Hash, Default, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, core_types::CacheHash, Default, DynAny, serde::Serialize, serde::Deserialize)] pub struct BrushStroke { pub style: BrushStyle, pub trace: Vec, diff --git a/node-graph/nodes/gcore/Cargo.toml b/node-graph/nodes/gcore/Cargo.toml index 740a021b59..31afd2f065 100644 --- a/node-graph/nodes/gcore/Cargo.toml +++ b/node-graph/nodes/gcore/Cargo.toml @@ -19,6 +19,7 @@ wasm = [ [dependencies] # Local dependencies core-types = { workspace = true } +graphene-hash = { workspace = true } raster-types = { workspace = true } graphic-types = { workspace = true } node-macro = { workspace = true } diff --git a/node-graph/nodes/gcore/src/animation.rs b/node-graph/nodes/gcore/src/animation.rs index 50c682d0b3..ffc9ed00ed 100644 --- a/node-graph/nodes/gcore/src/animation.rs +++ b/node-graph/nodes/gcore/src/animation.rs @@ -1,7 +1,7 @@ use core_types::table::Table; use core_types::transform::Footprint; use core_types::uuid::NodeId; -use core_types::{CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl}; +use core_types::{CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl}; use glam::{DAffine2, DVec2}; use graphic_types::vector_types::GradientStops; use graphic_types::{Artboard, Graphic, Vector}; @@ -9,7 +9,7 @@ use raster_types::{CPU, GPU, Raster}; const DAY: f64 = 1000. * 3600. * 24.; -#[derive(Debug, Clone, Copy, PartialEq, Eq, dyn_any::DynAny, Default, Hash, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, dyn_any::DynAny, Default, Hash, CacheHash, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)] pub enum RealTimeMode { #[label("UTC")] Utc, diff --git a/node-graph/nodes/gcore/src/context_modification.rs b/node-graph/nodes/gcore/src/context_modification.rs index b99ac69335..9d9c52c793 100644 --- a/node-graph/nodes/gcore/src/context_modification.rs +++ b/node-graph/nodes/gcore/src/context_modification.rs @@ -51,17 +51,17 @@ async fn context_modification( #[cfg(test)] mod tests { use super::*; + use core_types::graphene_hash::CacheHash; use core_types::transform::Footprint; use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; + use std::hash::Hasher; - /// Test that the hash of a nullified context remains stable even when nullified inputs change + /// Verifies that nullified context fields don't affect the cache hash — only the kept features matter. #[test] fn test_nullified_context_hash_stability() { use core_types::Context; use std::sync::Arc; - // Create original contexts using the Context type (Option>) let original_ctx: Context = Some(Arc::new( OwnedContextImpl::empty() .with_footprint(Footprint::default()) @@ -71,53 +71,48 @@ mod tests { .with_animation_time(20.25), )); - // Test nullifying different features - hash should remain stable for each nullification - let features_to_keep = ContextFeatures::empty(); // Nullify everything - - // Create nullified context - this should only keep features specified in features_to_keep - let nullified_ctx = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), features_to_keep); - - // Calculate hash of nullified context - let mut hasher1 = DefaultHasher::new(); - nullified_ctx.hash(&mut hasher1); - let hash1 = hasher1.finish(); - - // Create a different original context with changed values + // A second context with different values for the nullified fields let changed_ctx: Context = Some(Arc::new( OwnedContextImpl::empty() - .with_footprint(Footprint::default()) // Same footprint + .with_footprint(Footprint::default()) .with_index(2) - .with_real_time(999.9) // Different real time + .with_real_time(999.9) .with_vararg(Box::new("test")) - .with_animation_time(888.8), // Different animation time + .with_animation_time(888.8), )); - // Create nullified context from the changed original - should have same hash since everything is nullified - let nullified_changed_ctx = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), features_to_keep); + // Nullify everything — both should hash the same regardless of their field values + let features_to_keep = ContextFeatures::empty(); + let nullified1 = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), features_to_keep); + let nullified2 = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), features_to_keep); + + let mut hasher1 = DefaultHasher::new(); + nullified1.cache_hash(&mut hasher1); let mut hasher2 = DefaultHasher::new(); - nullified_changed_ctx.hash(&mut hasher2); - let hash2 = hasher2.finish(); + nullified2.cache_hash(&mut hasher2); - // Hash should be the same because all features were nullified - assert_eq!(hash1, hash2, "Hash of nullified context should remain stable regardless of input changes when features are nullified"); + assert_eq!( + hasher1.finish(), + hasher2.finish(), + "Hash of nullified context should remain stable regardless of input changes when features are nullified" + ); - // Test partial nullification - keep only footprint + // Keep only footprint and varargs — both have the same footprint and vararg, so hash should still match let partial_features = ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS; - - let partial_nullified1 = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), partial_features); - let partial_nullified2 = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), partial_features); + let partial1 = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), partial_features); + let partial2 = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), partial_features); let mut hasher3 = DefaultHasher::new(); - partial_nullified1.hash(&mut hasher3); - let hash3 = hasher3.finish(); + partial1.cache_hash(&mut hasher3); let mut hasher4 = DefaultHasher::new(); - partial_nullified2.hash(&mut hasher4); - let hash4 = hasher4.finish(); + partial2.cache_hash(&mut hasher4); - // These should be the same because both have the same footprint (Footprint::default()) and varargs - // and other features are nullified - assert_eq!(hash3, hash4, "Hash should be stable when keeping only footprint and footprint values are the same"); + assert_eq!( + hasher3.finish(), + hasher4.finish(), + "Hash should be stable when keeping only footprint and varargs and their values are the same" + ); } } diff --git a/node-graph/nodes/gcore/src/extract_xy.rs b/node-graph/nodes/gcore/src/extract_xy.rs index ffe13e26f2..6f686dd512 100644 --- a/node-graph/nodes/gcore/src/extract_xy.rs +++ b/node-graph/nodes/gcore/src/extract_xy.rs @@ -1,4 +1,4 @@ -use core_types::Ctx; +use core_types::{CacheHash, Ctx}; use dyn_any::DynAny; use glam::{DVec2, IVec2, UVec2}; @@ -15,7 +15,7 @@ fn extract_xy>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2 /// The X or Y component of a vec2. #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, CacheHash, DynAny, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)] #[widget(Radio)] pub enum XY { #[default] diff --git a/node-graph/nodes/gcore/src/memo.rs b/node-graph/nodes/gcore/src/memo.rs index e57d7755b9..eaacfc0ebd 100644 --- a/node-graph/nodes/gcore/src/memo.rs +++ b/node-graph/nodes/gcore/src/memo.rs @@ -1,7 +1,8 @@ use core_types::WasmNotSend; +use core_types::graphene_hash::CacheHash; use core_types::memo::*; use std::hash::DefaultHasher; -use std::hash::{Hash, Hasher}; +use std::hash::Hasher; use std::sync::Arc; use std::sync::Mutex; @@ -13,9 +14,9 @@ use std::sync::Mutex; /// /// Currently, only one input-output pair is cached. Subsequent calls with different inputs will overwrite the previous cache. #[node_macro::node(category(""), path(graphene_core::memo), skip_impl)] -async fn memo(input: I, #[data] cache: Arc>>, node: impl Node) -> T { +async fn memo(input: I, #[data] cache: Arc>>, node: impl Node) -> T { let mut hasher = DefaultHasher::new(); - input.hash(&mut hasher); + input.cache_hash(&mut hasher); let hash = hasher.finish(); if let Some(data) = cache.lock().as_ref().unwrap().as_ref().and_then(|data| (data.0 == hash).then_some(data.1.clone())) { diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index c370b25a5f..8a6c72e8f9 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -80,7 +80,7 @@ pub fn omit_element( } #[node_macro::node(category("General"))] -async fn map( +async fn map( ctx: impl Ctx + CloneVarArgs + ExtractAll, #[implementations( Table, diff --git a/node-graph/nodes/gstd/src/platform_application_io.rs b/node-graph/nodes/gstd/src/platform_application_io.rs index c72828a9d7..2b4f4c5652 100644 --- a/node-graph/nodes/gstd/src/platform_application_io.rs +++ b/node-graph/nodes/gstd/src/platform_application_io.rs @@ -120,7 +120,7 @@ fn string_to_bytes(_: impl Ctx, string: String) -> Vec { #[node_macro::node(category("Web Request"), name("Image to Bytes"))] fn image_to_bytes(_: impl Ctx, image: Table>) -> Vec { let Some(image) = image.iter().next() else { return vec![] }; - image.element.data.iter().flat_map(|color| color.to_rgb8_srgb().into_iter()).collect::>() + image.element.data.iter().flat_map(|color| color.to_rgba8_srgb().into_iter()).collect::>() } /// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue. diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index 734420732c..7d8bcd9387 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -21,7 +21,7 @@ use wgpu_executor::RenderContext; pub use crate::render_cache::render_output_cache; /// List of (canvas id, image data) pairs for embedding images as canvases in the final SVG string. -type ImageData = HashMap, u64>; +type ImageData = HashMap>, u64>; #[derive(Clone, dyn_any::DynAny)] pub enum RenderIntermediateType { @@ -191,7 +191,7 @@ async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, edito rendering.wrap_with_transform(footprint.transform, Some(logical_resolution)); RenderOutputType::Svg { svg: rendering.svg.to_svg_string(), - image_data: rendering.image_data.into_iter().map(|(image, id)| (id, image)).collect(), + image_data: rendering.image_data.into_iter().map(|(image, id)| (id, image.0)).collect(), } } (RenderOutputTypeRequest::Vello, RenderIntermediateType::Vello(vello_data)) => { diff --git a/node-graph/nodes/raster/Cargo.toml b/node-graph/nodes/raster/Cargo.toml index 55c7e06a03..781131ee3b 100644 --- a/node-graph/nodes/raster/Cargo.toml +++ b/node-graph/nodes/raster/Cargo.toml @@ -15,6 +15,7 @@ shader-nodes = ["std", "dep:raster-nodes-shaders", "dep:wgpu-executor"] std = [ "dep:core-types", "dep:dyn-any", + "dep:graphene-hash", "dep:raster-types", "dep:vector-types", "dep:image", @@ -41,6 +42,7 @@ node-macro = { workspace = true } # Local std dependencies dyn-any = { workspace = true, optional = true } core-types = { workspace = true, optional = true } +graphene-hash = { workspace = true, optional = true } raster-types = { workspace = true, optional = true } vector-types = { workspace = true, optional = true } wgpu-executor = { workspace = true, optional = true } diff --git a/node-graph/nodes/raster/src/adjustments.rs b/node-graph/nodes/raster/src/adjustments.rs index 2dd182c06a..f6a9666950 100644 --- a/node-graph/nodes/raster/src/adjustments.rs +++ b/node-graph/nodes/raster/src/adjustments.rs @@ -1015,3 +1015,20 @@ fn exposure>( }); input } + +#[cfg(feature = "std")] +mod _graphene_hash_impls { + use super::{CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute, SelectiveColorChoice}; + graphene_hash::impl_via_hash!( + LuminanceCalculation, + RedGreenBlue, + RedGreenBlueAlpha, + NoiseType, + FractalType, + CellularDistanceFunction, + CellularReturnType, + DomainWarpType, + RelativeAbsolute, + SelectiveColorChoice + ); +} diff --git a/node-graph/nodes/raster/src/curve.rs b/node-graph/nodes/raster/src/curve.rs index 2ba1d84cbf..6e9ffac4cf 100644 --- a/node-graph/nodes/raster/src/curve.rs +++ b/node-graph/nodes/raster/src/curve.rs @@ -1,11 +1,10 @@ use core_types::Node; use core_types::color::{Channel, Linear, LuminanceMut}; use dyn_any::{DynAny, StaticType, StaticTypeSized}; -use std::hash::{Hash, Hasher}; use std::ops::{Add, Mul, Sub}; #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, PartialEq, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)] pub struct Curve { #[serde(rename = "manipulatorGroups")] pub manipulator_groups: Vec, @@ -25,28 +24,13 @@ impl Default for Curve { } } -impl Hash for Curve { - fn hash(&self, state: &mut H) { - self.manipulator_groups.hash(state); - [self.first_handle, self.last_handle].iter().flatten().for_each(|f| f.to_bits().hash(state)); - } -} - #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)] pub struct CurveManipulatorGroup { pub anchor: [f32; 2], pub handles: [[f32; 2]; 2], } -impl Hash for CurveManipulatorGroup { - fn hash(&self, state: &mut H) { - for c in self.handles.iter().chain([&self.anchor]).flatten() { - c.to_bits().hash(state); - } - } -} - pub struct ValueMapperNode { lut: Vec, } diff --git a/node-graph/nodes/text/Cargo.toml b/node-graph/nodes/text/Cargo.toml index e5558c741d..adf8a467c8 100644 --- a/node-graph/nodes/text/Cargo.toml +++ b/node-graph/nodes/text/Cargo.toml @@ -13,6 +13,7 @@ wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] # Local dependencies core-types = { workspace = true } +graphene-hash = { workspace = true } raster-types = { workspace = true } vector-types = { workspace = true } node-macro = { workspace = true } diff --git a/node-graph/nodes/text/src/font_cache.rs b/node-graph/nodes/text/src/font_cache.rs index 58111bda21..9a07e3c599 100644 --- a/node-graph/nodes/text/src/font_cache.rs +++ b/node-graph/nodes/text/src/font_cache.rs @@ -1,3 +1,4 @@ +use core_types::graphene_hash::CacheHash; use dyn_any::DynAny; use parley::fontique::Blob; use std::collections::HashMap; @@ -23,6 +24,14 @@ impl std::hash::Hash for Font { } } +impl CacheHash for Font { + fn cache_hash(&self, state: &mut H) { + self.font_family.cache_hash(state); + self.font_style.cache_hash(state); + // Don't consider `font_style_to_restore` in the HashMaps + } +} + impl PartialEq for Font { fn eq(&self, other: &Self) -> bool { // Don't consider `font_style_to_restore` in the HashMaps diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index e877c1b89b..55640e1cf7 100644 --- a/node-graph/nodes/text/src/lib.rs +++ b/node-graph/nodes/text/src/lib.rs @@ -7,6 +7,7 @@ mod to_path; use convert_case::{Boundary, Converter, pattern}; use core_types::Color; +use core_types::graphene_hash::CacheHash; use core_types::registry::types::{SignedInteger, TextArea}; use core_types::table::Table; use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractVarArgs, OwnedContextImpl}; @@ -25,7 +26,7 @@ pub use vector_types; /// Alignment of lines of type within a text block. #[repr(C)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, CacheHash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum TextAlign { #[default] @@ -116,7 +117,7 @@ fn escape_string(input: String) -> String { result } -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, dyn_any::DynAny, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, CacheHash, dyn_any::DynAny, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)] #[widget(Dropdown)] pub enum StringCapitalization { /// "on the origin of species" — Converts all letters to lower case. diff --git a/node-graph/nodes/vector/Cargo.toml b/node-graph/nodes/vector/Cargo.toml index bd976b84e7..3f5df26780 100644 --- a/node-graph/nodes/vector/Cargo.toml +++ b/node-graph/nodes/vector/Cargo.toml @@ -13,6 +13,7 @@ wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] # Local dependencies core-types = { workspace = true } +graphene-hash = { workspace = true } vector-types = { workspace = true } graphic-types = { workspace = true } node-macro = { workspace = true } diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index 66c8009e02..73564c75e3 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -1,6 +1,6 @@ -use core_types::Ctx; use core_types::registry::types::{Angle, PixelLength, PixelSize}; use core_types::table::Table; +use core_types::{CacheHash, Ctx}; use dyn_any::DynAny; use glam::DVec2; use graphic_types::Vector; @@ -188,7 +188,7 @@ fn star( } #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, CacheHash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum QRCodeErrorCorrectionLevel { /// Allows recovery from up to 7% data loss.