Derive pushed index levels per input and pop them when lifting requirements

This commit is contained in:
Dennis Kobert
2026-08-24 17:05:14 +00:00
parent 9cdaf06ca9
commit 2c84bb478f
7 changed files with 49 additions and 13 deletions

View File

@@ -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();

View File

@@ -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<const LEVEL: u8 = 0>: 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<u8>,
#[cfg_attr(feature = "serde", serde(default, deserialize_with = "deserialize_sorted_sources"))]
sources: Vec<SourceId>,
}
@@ -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<u8>) -> 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(),
}
}

View File

@@ -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,

View File

@@ -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<u8> = 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,

View File

@@ -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<Graphic>,
/// 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<Vector>,
operation: BooleanOperation,
) -> Result<

View File

@@ -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,

View File

@@ -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()));