diff --git a/node-graph/libraries/core-types/src/context.rs b/node-graph/libraries/core-types/src/context.rs index d818ba660b..589a010021 100644 --- a/node-graph/libraries/core-types/src/context.rs +++ b/node-graph/libraries/core-types/src/context.rs @@ -39,6 +39,18 @@ pub trait ExtractIndex { fn innermost_index(&self) -> u64 { self.try_index().and_then(|mut indices| indices.next()).unwrap_or(0) as u64 } + + /// The decompose half of decompose-and-promote: splits the flat innermost + /// index over the content's inner extent into the pushed level's copy and + /// the lane within it. Rectangular domains; an empty inner extent maps + /// everything to copy 0. + fn split_innermost(&self, inner: u64) -> (u64, u64) { + let flat = self.innermost_index(); + match inner { + 0 => (0, 0), + inner => (flat / inner, flat % inner), + } + } } pub trait ExtractVarArgs { // TODO: Consider returning a slice or something like that @@ -721,6 +733,7 @@ pub trait DeriveCtx { fn position_head(&self) -> Option<&PositionLink<'_>>; fn varargs_head(&self) -> Option<&VarArgLink<'_>>; fn promoted<'s>(&'s self, spilled_head: &'s IndexLink<'s>, inner_index: u64) -> Derived<'s, Self>; + fn push_level<'s>(&'s self, frame: &'s mut IndexLink<'s>, copy: u64, inner: u64) -> Derived<'s, Self>; fn with_footprint<'s>(&'s self, footprint: &'s Footprint) -> Derived<'s, Self>; fn with_varargs<'s>(&'s self, varargs: &'s VarArgLink<'s>) -> Derived<'s, Self>; fn with_position<'s>(&'s self, position: &'s PositionLink<'s>) -> Derived<'s, Self>; @@ -1111,6 +1124,10 @@ impl<'a> DeriveCtx for ContextImpl<'a> { ContextImpl::promoted(self, spilled_head, inner_index) } + fn push_level<'s>(&'s self, frame: &'s mut IndexLink<'s>, copy: u64, inner: u64) -> ContextImpl<'s> { + ContextImpl::push_level(self, frame, copy, inner) + } + fn with_footprint<'s>(&'s self, footprint: &'s Footprint) -> ContextImpl<'s> { ContextImpl::with_footprint(self, footprint) } diff --git a/node-graph/libraries/core-types/src/record.rs b/node-graph/libraries/core-types/src/record.rs index da6ea42297..47ce402feb 100644 --- a/node-graph/libraries/core-types/src/record.rs +++ b/node-graph/libraries/core-types/src/record.rs @@ -729,15 +729,17 @@ pub struct RecordLazyInput<'a, 'e, N> { node: &'a N, cell: &'a crate::node::StatusCell, input_index: usize, + inner_levels: u8, _lifetime: std::marker::PhantomData RecordValue<'e>>, } impl<'a, 'e, N> RecordLazyInput<'a, 'e, N> { - pub fn new(node: &'a N, cell: &'a crate::node::StatusCell, input_index: usize) -> Self { + pub fn new(node: &'a N, cell: &'a crate::node::StatusCell, input_index: usize, inner_levels: u8) -> Self { Self { node, cell, input_index, + inner_levels, _lifetime: std::marker::PhantomData, } } @@ -748,6 +750,36 @@ impl<'a, 'e, N> RecordLazyInput<'a, 'e, N> { { Ok(self.node.eval_derived(self.cell, self.input_index, ctx)?.rebind()) } + + /// The flat lane count of one copy: the product of the edge's inner-level + /// extents, queried uniform across copies (at copy 0). The dividend of a + /// structure node's decompose-and-promote. + pub fn inner_extent(&self, ctx: &B) -> Result + where + B: crate::context::DeriveCtx, + N: for<'d> DerivedRecordEdge<'d, crate::context::Derived<'d, B>>, + { + inner_extent_of(self.node, ctx, self.inner_levels) + } +} + +/// See [`RecordLazyInput::inner_extent`]. +fn inner_extent_of(node: &N, ctx: &B, levels: u8) -> Result +where + B: crate::context::DeriveCtx, + N: for<'d> DerivedRecordEdge<'d, crate::context::Derived<'d, B>>, +{ + let head = ctx.index_head(); + let derived = ctx.promoted(&head, 0); + let mut inner: u64 = 1; + for level in 0..levels { + match node.extent_at_derived(&derived, level) { + GPoll::Final(crate::gpoll::Extent::Exactly(count)) => inner *= count as u64, + GPoll::Pending => return Err(crate::gpoll::Interrupt::Pending), + _ => return Err(crate::gpoll::GraphError::new("structure decomposition over a non-exact extent").into()), + } + } + Ok(inner) } /// The derive-routing carrier beside its declared attribute reads: evaluating @@ -759,6 +791,7 @@ pub struct DerivedLazyInput<'a, 'e, Out, N> { node: &'a N, cell: &'a crate::node::StatusCell, input_index: usize, + inner_levels: u8, reads: &'a [Option], read: unsafe fn(Rec, &[Option]) -> Out, _lifetime: std::marker::PhantomData RecordValue<'e>>, @@ -767,17 +800,27 @@ pub struct DerivedLazyInput<'a, 'e, Out, N> { impl<'a, 'e, Out, N> DerivedLazyInput<'a, 'e, Out, N> { /// `read` must be sound against the layout the offsets in `reads` were /// resolved from; the macro proves both at wiring. - pub fn new(node: &'a N, cell: &'a crate::node::StatusCell, input_index: usize, reads: &'a [Option], read: unsafe fn(Rec, &[Option]) -> Out) -> Self { + pub fn new(node: &'a N, cell: &'a crate::node::StatusCell, input_index: usize, inner_levels: u8, reads: &'a [Option], read: unsafe fn(Rec, &[Option]) -> Out) -> Self { Self { node, cell, input_index, + inner_levels, reads, read, _lifetime: std::marker::PhantomData, } } + /// The flat lane count of one copy; see [`RecordLazyInput::inner_extent`]. + pub fn inner_extent(&self, ctx: &B) -> Result + where + B: crate::context::DeriveCtx, + N: for<'d> DerivedRecordEdge<'d, crate::context::Derived<'d, B>>, + { + inner_extent_of(self.node, ctx, self.inner_levels) + } + pub fn eval<'d, C>(&self, ctx: &C) -> Result where N: DerivedRecordEdge<'d, C>, @@ -1399,6 +1442,10 @@ where } } + fn extent_at(&self, input: &C, level: u8) -> GPoll { + self.edge.extent_at(input, level) + } + fn layout(&self) -> &Layout { &self.union } diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index f254be457f..068cb36576 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -1161,20 +1161,20 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn // A raw poll edge is threaded straight through, so it does not bind here. (LazyBinding::Plain, true) => quote!(), (LazyBinding::DeriveRouting, _) => quote! { - let #name = #core_types::record::RecordLazyInput::new(&self.#name, &__cell, #index); + let #name = #core_types::record::RecordLazyInput::new(&self.#name, &__cell, #index, self.__layout.depth.saturating_sub(#pushed_levels)); }, (LazyBinding::DeriveCarrier, _) => { let reads = reads_of(index); let read_fn = format_ident!("__{}_read_{}", fn_name, index); match reads.is_empty() { true => quote! { - let #name = #core_types::record::DerivedLazyInput::new(&self.#name, &__cell, #index, &[], #core_types::record::token_only); + let #name = #core_types::record::DerivedLazyInput::new(&self.#name, &__cell, #index, self.__layout.depth.saturating_sub(#pushed_levels), &[], #core_types::record::token_only); }, false => { let slot_idents: Vec = reads.iter().map(|(slot, _)| format_ident!("__read_{slot}")).collect(); quote! { let __carrier_reads = [#(self.#slot_idents),*]; - let #name = #core_types::record::DerivedLazyInput::new(&self.#name, &__cell, #index, &__carrier_reads, self::#read_fn); + let #name = #core_types::record::DerivedLazyInput::new(&self.#name, &__cell, #index, self.__layout.depth.saturating_sub(#pushed_levels), &__carrier_reads, self::#read_fn); } } } diff --git a/node-graph/nodes/gcore/src/record.rs b/node-graph/nodes/gcore/src/record.rs index 9101fe2265..24a5c1a21b 100644 --- a/node-graph/nodes/gcore/src/record.rs +++ b/node-graph/nodes/gcore/src/record.rs @@ -6,7 +6,7 @@ //! wiring is by hand until the compiler pass constructs layouts. use core_types::attribute::{Attr, Opacity, RemoveAttr}; -use core_types::context::{DeriveCtx, ExtractArena, ExtractIndex, InjectIndex}; +use core_types::context::{DeriveCtx, ExtractArena, ExtractIndex, IndexLink, InjectIndex}; use core_types::extent::{ExtentIn, LevelIn, ValueIn}; use core_types::gpoll::{ErrorKind, Extent, GPoll, GraphError, Interrupt}; use core_types::{Context, Ctx}; @@ -82,13 +82,15 @@ fn repeat( count: u32, reverse: bool, ) -> Result, Interrupt> { - let spilled = ctx.index_head(); - let copy = ctx.innermost_index() % count as u64; + let inner = content.inner_extent(ctx)?; + let (copy, rest) = ctx.split_innermost(inner); + let copy = copy % count.max(1) as u64; let copy = match reverse { true => count as u64 - 1 - copy, false => copy, }; - content.eval(&ctx.promoted(&spilled, copy)) + let mut frame = IndexLink { index: 0, outer: None }; + content.eval(&ctx.push_level(&mut frame, copy, rest)) } /// The pushed level's extent is the copy count; inner levels forward to the @@ -108,9 +110,11 @@ fn repeat_faded( content: impl Node, Output = (T, Attr)>, count: u32, ) -> Result)>, Interrupt> { - let spilled = ctx.index_head(); - let copy = ctx.innermost_index() % count as u64; - let (element, opacity) = content.eval(&ctx.promoted(&spilled, copy))?; + let inner = content.inner_extent(ctx)?; + let (copy, rest) = ctx.split_innermost(inner); + let copy = copy % count.max(1) as u64; + let mut frame = IndexLink { index: 0, outer: None }; + let (element, opacity) = content.eval(&ctx.push_level(&mut frame, copy, rest))?; Ok(emit(element, Attr(*opacity * (copy + 1) as f64))) } @@ -263,7 +267,9 @@ mod tests { type Output = RecordValue<'e>; fn eval(&self, input: &ContextImpl<'e>) -> GPoll> { - let element = input.innermost_index() as f64; + // Depth-0 content varying per copy: the enclosing (pushed) level's + // index sits one link above the content's own innermost lane. + let element = input.try_index().and_then(|mut indices| indices.nth(1)).unwrap_or(0) as f64; let mut value = RecordValue::zeroed(); let dst = match self.layout.frame_bytes() { 0 => value.as_mut_ptr(), @@ -495,6 +501,55 @@ mod tests { } } + #[test] + fn repeat_decomposes_the_flat_index_over_depth_one_content() { + let arena = Arena::new(1024).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let base = f64_layout(&[]); + let leveled_content = repeat_opacity_layout(&base); + let (count_edge, count_layout) = lifted_value(2u32); + let (reverse_edge, reverse_layout) = lifted_value(false); + reserve_for(&[&base, &leveled_content, &count_layout, &reverse_layout]); + + let content = install(RepeatOpacityNode::new(bare_source(&base, 7.), ValueNode(3u32), &base), repeat_opacity_layout_meta(), &[Some(&base)]); + let meta = core_types::record::LayoutMeta { + sources: vec![0], + reads: vec![], + element: core_types::record::ElementSpec::Carried, + writes: vec![], + removes: vec![], + level_delta: 1, + }; + let repeat = install( + RepeatNode::new(RecordSource::new(content, &leveled_content, &leveled_content), count_edge, reverse_edge, &leveled_content, &count_layout, &reverse_layout), + meta, + &[Some(&leveled_content)], + ); + let two_level = Node::::layout(&repeat).clone(); + assert_eq!(two_level.depth, 2, "the pushed level sits above the content's own level"); + assert_eq!(repeat.extent_at(&ctx, 1), GPoll::Final(Extent::Exactly(2)), "the pushed level's extent is the copy count"); + assert_eq!(repeat.extent_at(&ctx, 0), GPoll::Final(Extent::Exactly(3)), "the content's level forwards"); + + let head = ctx.index_head(); + for flat in 0..6u64 { + let mark = stack::sp(); + let lane = ctx.promoted(&head, flat); + let GPoll::Final(value) = repeat.eval(&lane) else { + panic!("expected a final record"); + }; + let rec = two_level.rec(&value); + assert_eq!(unsafe { rec.element::() }, 7.); + // The flat index decomposes: the content sees the remainder as its + // own innermost lane, so its per-lane opacity is `flat % 3`. + assert_eq!(unsafe { rec.read::(two_level.offset_of(Opacity::NAME, 0).unwrap()) }, (flat % 3) as f64); + // SAFETY: the element and attr were read out above, so no borrow into this lane's frames remains. + unsafe { stack::rewind(mark) }; + } + } + #[test] fn lazy_carrier_reads_and_rewrites_the_attr_per_copy() { let arena = Arena::new(1024).unwrap(); diff --git a/node-graph/nodes/repeat/src/leveled.rs b/node-graph/nodes/repeat/src/leveled.rs index 9c137519c8..6df60db012 100644 --- a/node-graph/nodes/repeat/src/leveled.rs +++ b/node-graph/nodes/repeat/src/leveled.rs @@ -4,7 +4,7 @@ use core::f64::consts::TAU; use core_types::attribute::{Attr, Transform}; -use core_types::context::ExtractIndex; +use core_types::context::{ExtractIndex, IndexLink}; use core_types::extent::{ExtentIn, LevelIn, ValueIn}; use core_types::gpoll::{Extent, GPoll, Interrupt}; use core_types::registry::types::{Angle, PixelSize}; @@ -23,9 +23,11 @@ fn repeat_array( #[hard(1..)] count: u32, ) -> Result)>, Interrupt> { - let spilled = ctx.index_head(); - let copy = ctx.innermost_index() % count as u64; - let (element, local) = content.eval(&ctx.promoted(&spilled, copy))?; + let inner = content.inner_extent(ctx)?; + let (copy, rest) = ctx.split_innermost(inner); + let copy = copy % count.max(1) as u64; + let mut frame = IndexLink { index: 0, outer: None }; + let (element, local) = content.eval(&ctx.push_level(&mut frame, copy, rest))?; // A single copy has no steps between copies, so the denominator stays 1. let total = (count - 1).max(1) as f64; @@ -58,9 +60,11 @@ fn repeat_radial( #[hard(1..)] count: u32, ) -> Result)>, Interrupt> { - let spilled = ctx.index_head(); - let copy = ctx.innermost_index() % count as u64; - let (element, local) = content.eval(&ctx.promoted(&spilled, copy))?; + let inner = content.inner_extent(ctx)?; + let (copy, rest) = ctx.split_innermost(inner); + let copy = copy % count.max(1) as u64; + let mut frame = IndexLink { index: 0, outer: None }; + let (element, local) = content.eval(&ctx.push_level(&mut frame, copy, rest))?; let angle = DAffine2::from_angle((TAU / count as f64) * copy as f64 + start_angle.to_radians()); let translation = DAffine2::from_translation(radius * DVec2::Y);