diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index 7d329e3c04..8842aab74b 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -523,10 +523,13 @@ impl ProtoNetwork { }; // Compute the dependencies for each branch and combine all of them - for &node in &inputs { + let pushed_levels = self.nodes[node_index].1.context_features.pushed_levels.clone(); + for (input, &node) in inputs.iter().enumerate() { let branch = self.find_context_dependencies(node); - combined_deps |= &branch.0; + let mut lifted = branch.0.clone(); + lifted.index_levels = lifted.index_levels.popped(pushed_levels.get(input).copied().unwrap_or(0)); + combined_deps |= &lifted; branch_dependencies.push(branch); } let mut new_deps = combined_deps.clone(); diff --git a/node-graph/libraries/core-types/src/context.rs b/node-graph/libraries/core-types/src/context.rs index 7d1fd3d041..db1b3b3a44 100644 --- a/node-graph/libraries/core-types/src/context.rs +++ b/node-graph/libraries/core-types/src/context.rs @@ -57,10 +57,9 @@ pub trait ExtractIndices { } } -/// `LEVEL` is the level of the index chain a node reads, counted outwards from -/// the innermost. Declaring it narrows the cache key to that level, and reading -/// through `index` keeps the declaration and the read from drifting apart. -/// Repeat the bound to declare several, then spell the level at each read. +/// `LEVEL` is the index-chain level a node reads, counted outwards from the +/// innermost; only declared levels survive in the cache key. Repeat the bound +/// to declare several, spelling the level at each `index` call. pub trait ExtractIndex: ExtractIndices { fn index(&self) -> u64 { self.try_index().and_then(|mut levels| levels.nth(LEVEL as usize)).unwrap_or(0) as u64 @@ -245,6 +244,17 @@ impl IndexLevels { self.0 == u32::MAX } + /// The same requirement seen from outside `levels` pushed levels. An + /// all-levels mask stays saturated, since its reader's level is not known at + /// compile time. + pub const fn popped(self, levels: u8) -> Self { + match self.0 { + u32::MAX => self, + _ if levels as u32 >= u32::BITS => Self(0), + mask => Self(mask >> levels), + } + } + /// Levels at or past `u32::BITS` are only readable through the all-levels /// sentinel, so they count as declared whenever the mask is saturated. pub const fn contains_level(self, level: usize) -> bool { @@ -319,6 +329,9 @@ pub struct ContextDependencies { /// written before the field existed default to the whole chain. #[cfg_attr(feature = "serde", serde(default = "IndexLevels::innermost"))] pub index_levels: IndexLevels, + /// Index levels pushed per input, in input order; a missing entry is 0. + #[cfg_attr(feature = "serde", serde(default))] + pub pushed_levels: Vec, #[cfg_attr(feature = "serde", serde(default, deserialize_with = "deserialize_sorted_sources"))] sources: Vec, } @@ -333,6 +346,7 @@ impl ContextDependencies { extract, inject, index_levels, + pushed_levels: Vec::new(), sources: Vec::new(), } } @@ -342,6 +356,11 @@ impl ContextDependencies { self } + pub fn with_pushed_levels(mut self, pushed_levels: Vec) -> Self { + self.pushed_levels = pushed_levels; + self + } + pub fn sources(&self) -> &[SourceId] { &self.sources } @@ -496,6 +515,7 @@ impl From<&[ContextFeature]> for ContextDependencies { extract, inject, index_levels, + pushed_levels: Vec::new(), sources: Vec::new(), } } diff --git a/node-graph/libraries/core-types/src/registry.rs b/node-graph/libraries/core-types/src/registry.rs index c78e0c278d..d78b1c7adb 100644 --- a/node-graph/libraries/core-types/src/registry.rs +++ b/node-graph/libraries/core-types/src/registry.rs @@ -29,6 +29,8 @@ pub struct NodeMetadata { pub struct FieldMetadata { pub name: &'static str, pub description: &'static str, + /// Index levels the node pushes when evaluating this input. + pub pushed_levels: u8, pub hidden: bool, pub exposed: bool, pub widget_override: RegistryWidgetOverride, diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 2d43e9b937..d7a55b1beb 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -178,6 +178,17 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let input_descriptions: Vec<_> = regular_fields.iter().map(|f| &f.description).collect(); + let subject_depth = node.inputs.iter().find(|input| input.subject).map_or(0, |input| input.shape.depth); + let pushed_levels = (node.output.shape.depth as i8 - subject_depth as i8).max(0) as u8; + let field_pushed_levels: Vec = regular_fields + .iter() + .map(|field| match &field.ty { + ParsedFieldType::Regular(RegularParsedField { list_levels, .. }) if *list_levels > 0 => *list_levels as u8, + ParsedFieldType::Node(_) => pushed_levels, + _ => 0, + }) + .collect(); + // Generate struct fields: data fields (concrete types) + regular fields (generic types) let data_field_defs = data_fields.iter().map(|field| { let name = &field.pat_ident.ident; @@ -633,6 +644,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn name: #input_names, widget_override: #widget_override, description: #input_descriptions, + pushed_levels: #field_pushed_levels, hidden: #input_hidden, exposed: #exposed, value_source: #value_sources, diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index ce03d04db2..8cf19473c7 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -98,7 +98,7 @@ fn boolean_core<'e>( /// Combines the geometric forms of one or more closed paths into a new vector path that results from cutting or joining the paths by the chosen method. #[node_macro::node(category("Vector: Modifier"), memoize)] fn boolean_operation<'e>( - ctx: impl Ctx + ExtractArena<'e> + core_types::ExtractIndex + core_types::InjectIndex + Copy, + ctx: impl Ctx + ExtractArena<'e> + core_types::InjectIndex + Copy, /// The wire of vector paths to perform the boolean operation on. Nested groups are automatically flattened. content: IList, /// Which boolean operation to perform on the paths. @@ -134,7 +134,7 @@ fn boolean_operation<'e>( /// The boolean operation over a plain vector level, as [`boolean_operation`]. #[node_macro::node(category(""))] fn boolean_operation_vector<'e>( - ctx: impl Ctx + ExtractArena<'e> + core_types::ExtractIndex + core_types::InjectIndex + Copy, + ctx: impl Ctx + ExtractArena<'e> + core_types::InjectIndex + Copy, content: IList, operation: BooleanOperation, ) -> Result< diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 77dc316e0a..2cd89b0f18 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -237,9 +237,8 @@ fn assign_colors_graphic<'e>( Ok((element, transform, layer_path)) } -/// Where each lane's colors start in the level's flattened vector run, so the -/// level is counted once per evaluation rather than once per lane. `offsets` -/// holds one entry per lane plus the total. +/// Where each lane's colors start in the level's flattened vector run, valid +/// for one key and generation. `offsets` holds one entry per lane plus the total. #[derive(Debug)] pub struct LaneOffsets { key: u64, diff --git a/node-graph/preprocessor/src/lib.rs b/node-graph/preprocessor/src/lib.rs index 200839afe8..8fa7447271 100644 --- a/node-graph/preprocessor/src/lib.rs +++ b/node-graph/preprocessor/src/lib.rs @@ -205,7 +205,7 @@ impl Preprocessor { implementation: DocumentNodeImplementation::ProtoNode(id.clone()), visible: true, skip_deduplication: false, - context_features: ContextDependencies::from(metadata.context_features.as_slice()), + context_features: ContextDependencies::from(metadata.context_features.as_slice()).with_pushed_levels(metadata.fields.iter().map(|field| field.pushed_levels).collect()), ..Default::default() }; @@ -256,7 +256,7 @@ impl Preprocessor { call_argument: node_io.call_argument.clone(), implementation: DocumentNodeImplementation::ProtoNode(id.clone()), visible: true, - context_features: ContextDependencies::from(metadata.context_features.as_slice()), + context_features: ContextDependencies::from(metadata.context_features.as_slice()).with_pushed_levels(metadata.fields.iter().map(|field| field.pushed_levels).collect()), ..Default::default() }; inject_scopes.insert(id.clone(), (template, node_io.return_value.clone()));