From 98be2b8c79dacf3e526fcbd0a61efe43e91a0d06 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Thu, 6 Aug 2026 08:01:19 +0000 Subject: [PATCH] Carry erased element and re-park glue on layouts and collapse memoize to a generic record row --- .../src/dynamic_executor.rs | 21 ++ .../interpreted-executor/src/node_registry.rs | 111 ++----- .../libraries/core-types/src/attribute.rs | 14 + node-graph/libraries/core-types/src/record.rs | 281 +++++++++++++++--- .../libraries/core-types/src/runtime.rs | 10 +- node-graph/node-macro/src/codegen.rs | 24 +- node-graph/nodes/gcore/src/record.rs | 99 +++++- 7 files changed, 422 insertions(+), 138 deletions(-) diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index 859624daf0..45bf917e2f 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -655,6 +655,27 @@ mod test { assert!(fields.is_empty(), "an element-only record has no attribute fields"); } + #[test] + fn a_memoize_row_wires_generically_and_replays_over_record_wires() { + let network = ProtoNetwork { + inputs: vec![], + output: NodeId(2), + nodes: vec![ + (NodeId(0), ProtoNode::value(ConstructionArgs::Value(TaggedValue::String(String::from("cached")).into()), vec![])), + (NodeId(1), proto_node("graphene_core::memo::MemoizeNode", vec![NodeId(0)])), + (NodeId(2), proto_node("core_types::record::RecordExtractNode", vec![NodeId(1)])), + ], + }; + + let executor = DynamicExecutor::new(network).unwrap(); + assert_eq!((&executor).execute(()).unwrap(), GPoll::Final(TaggedValue::String(String::from("cached")))); + assert_eq!( + (&executor).execute(()).unwrap(), + GPoll::Final(TaggedValue::String(String::from("cached"))), + "the second execution replays the deep copy against a reset arena" + ); + } + fn modification_value() -> ProtoNode { let modification = core_types::ContextModification::from_sources(core_types::context::ContextFeatures::all(), &[]); ProtoNode::value(ConstructionArgs::Value(TaggedValue::ContextModification(modification).into()), vec![]) diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 66ce9270af..358102875a 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -189,93 +189,30 @@ fn node_registry() -> HashMap> { // ========== // MEMO NODES // ========== - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => ()]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => RuntimeHandle]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => SourceId]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => bool]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List>]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Image]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => AttributeDyn]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => AttributeValueDyn]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => ListDyn]), - #[cfg(target_family = "wasm")] - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => CanvasHandle]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => f64]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => f32]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => u32]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => u64]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => DVec2]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => String]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => DAffine2]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Footprint]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => RenderOutput]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => std::sync::Arc]), - #[cfg(feature = "gpu")] - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List>]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Graphic]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => glam::f32::Vec2]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => glam::f32::Affine2]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Stroke]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::text::Font]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => DocumentNode]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::ContextModification]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::Footprint]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Box]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::blending::BlendMode]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::LuminanceCalculation]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::QRCodeErrorCorrectionLevel]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::extract_xy::XY]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::text_nodes::StringCapitalization]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::RedGreenBlue]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::RedGreenBlueAlpha]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::animation::RealTimeMode]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::NoiseType]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::FractalType]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::CellularDistanceFunction]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::CellularReturnType]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::DomainWarpType]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::RelativeAbsolute]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::SelectiveColorChoice]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::GridType]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ArcType]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::RowsOrColumns]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::MergeByDistanceAlgorithm]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ExtrudeJoiningAlgorithm]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeCap]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeJoin]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeAlign]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::PaintOrder]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientType]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientSpreadMethod]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::BooleanOperation]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::text::TextAlign]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::ScaleType]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::InterpolationDistribution]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => RenderIntermediate]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuExecutorHandle]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option]), - async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuPipelineCache]), + ( + ProtoNodeIdentifier::new("graphene_core::memo::MemoizeNode"), + RegistryEntry { + io: NodeIOTypes::new( + concrete!(Context), + core_types::Type::Record(Box::new(core_types::Type::Generic(std::borrow::Cow::Borrowed("T")))), + vec![core_types::registry::generic_record_edge_type("T")], + ), + constructor: |inputs| { + if inputs.len() != 1 { + return Err(ConstructionError::Arity { expected: 1, got: inputs.len() }); + } + let mut inputs = inputs.into_iter(); + let handle = inputs.next().unwrap(); + let ty = handle.ty().clone(); + let Some(layout) = handle.layout().cloned() else { + return Err(ConstructionError::MissingLayout); + }; + let edge = handle.downcast_erased::(ty.clone())?; + let node = core_types::record::RecordMemo::new(edge, &layout); + Ok(EdgeHandle::new_erased(std::sync::Arc::new(node) as std::sync::Arc, ty)) + }, + }, + ), // ============ // REF ADAPTERS // ============ diff --git a/node-graph/libraries/core-types/src/attribute.rs b/node-graph/libraries/core-types/src/attribute.rs index 0eeb9d7562..2e7533d745 100644 --- a/node-graph/libraries/core-types/src/attribute.rs +++ b/node-graph/libraries/core-types/src/attribute.rs @@ -37,6 +37,10 @@ pub trait Attribute: 'static { /// # Safety /// `ptr` must point at a live field of this marker's value type. unsafe fn read_erased(ptr: *const u8) -> Box; + + /// Re-parks the owned clone [`Self::read_erased`] produced into fresh + /// field storage; `None` for plain values, which ride the byte copy. + const REPARK: Option Option<()>> = None; } /// A kernel-facing attribute value. A parameter `Attr` is a read of `A` @@ -162,6 +166,16 @@ macro_rules! attribute { unsafe fn read_erased(ptr: *const u8) -> ::std::boxed::Box { ::std::boxed::Box::new(unsafe { ptr.cast::<&$value>().read() }.to_owned()) } + + const REPARK: ::core::option::Option ::core::option::Option<()>> = { + unsafe fn repark(value: &dyn $crate::list::AnyAttributeValue, dst: *mut u8, arena: &$crate::arena::Arena) -> ::core::option::Option<()> { + let owned: &<$value as ::std::borrow::ToOwned>::Owned = value.as_any().downcast_ref().expect("a reference attribute replays its owned clone"); + let (parked, _) = arena.alloc(<$value as ::std::borrow::ToOwned>::to_owned(::std::borrow::Borrow::borrow(owned)))?; + unsafe { dst.cast::<&$value>().write(::std::borrow::Borrow::borrow(parked)) }; + ::core::option::Option::Some(()) + } + ::core::option::Option::Some(repark) + }; } $crate::attribute!(@register $marker); diff --git a/node-graph/libraries/core-types/src/record.rs b/node-graph/libraries/core-types/src/record.rs index a762a8a09a..ed3761fb1c 100644 --- a/node-graph/libraries/core-types/src/record.rs +++ b/node-graph/libraries/core-types/src/record.rs @@ -20,6 +20,7 @@ pub struct FieldWrite { pub size: usize, pub align: usize, pub read_erased: unsafe fn(*const u8) -> Box, + pub repark: Option Option<()>>, } impl FieldWrite { @@ -30,6 +31,7 @@ impl FieldWrite { size: size_of::>(), align: align_of::>(), read_erased: A::read_erased, + repark: A::REPARK, } } } @@ -47,6 +49,7 @@ pub struct FieldDesc { pub size: usize, pub align: usize, pub read_erased: unsafe fn(*const u8) -> Box, + pub repark: Option Option<()>>, } impl PartialEq for FieldDesc { @@ -57,14 +60,52 @@ impl PartialEq for FieldDesc { impl Eq for FieldDesc {} +/// The element slot of a layout: its dimensions plus erased glue bound where +/// the element type is statically known, so generic consumers read or +/// deep-copy the element without it. Equality is structural: glue pointers +/// are excluded for the same reason as [`FieldDesc`]'s. +#[derive(Clone, Copy, Debug)] +pub struct ElementWrite { + pub size: usize, + pub align: usize, + pub parked: bool, + pub clone_out: unsafe fn(*const u8) -> Box, + pub repark: unsafe fn(&(dyn std::any::Any + Send + Sync), *mut u8, &crate::arena::Arena) -> Option<()>, +} + +impl PartialEq for ElementWrite { + fn eq(&self, other: &Self) -> bool { + (self.size, self.align, self.parked) == (other.size, other.align, other.parked) + } +} + +impl Eq for ElementWrite {} + +impl Default for ElementWrite { + fn default() -> Self { + unsafe fn clone_out(_ptr: *const u8) -> Box { + Box::new(()) + } + unsafe fn repark(_value: &(dyn std::any::Any + Send + Sync), _dst: *mut u8, _arena: &crate::arena::Arena) -> Option<()> { + Some(()) + } + Self { + size: 0, + align: 0, + parked: false, + clone_out, + repark, + } + } +} + /// A record layout: the element at offset 0, then the written attributes in /// canonical order (descending alignment, then size, then name, then level). /// Layouts are derived data, a pure function of the upstream write set. #[derive(Clone, Debug, Default, PartialEq)] pub struct Layout { pub depth: u8, - pub element_size: usize, - pub element_align: usize, + pub element: ElementWrite, pub fields: Vec, pub size: usize, pub align: usize, @@ -92,11 +133,11 @@ impl Layout { } } - /// The union of this layout's fields and `writes` over an element of - /// (size, align) at `depth`, in canonical order. A (name, level) written - /// at a different size is a type conflict and panics; the census keeps - /// declared names to one type, so this only fires on wiring bugs. - pub fn with_writes(&self, depth: u8, element: (usize, usize), writes: &[FieldWrite]) -> Layout { + /// The union of this layout's fields and `writes` over `element` at + /// `depth`, in canonical order. A (name, level) written at a different + /// size is a type conflict and panics; the census keeps declared names to + /// one type, so this only fires on wiring bugs. + pub fn with_writes(&self, depth: u8, element: ElementWrite, writes: &[FieldWrite]) -> Layout { let mut merged: Vec = self .fields .iter() @@ -106,6 +147,7 @@ impl Layout { size: field.size, align: field.align, read_erased: field.read_erased, + repark: field.repark, }) .collect(); for &write in writes { @@ -115,9 +157,8 @@ impl Layout { } } merged.sort_by(|a, b| b.align.cmp(&a.align).then(b.size.cmp(&a.size)).then(a.name.cmp(b.name)).then(a.level.cmp(&b.level))); - let (element_size, element_align) = element; - let mut offset = element_size; - let mut align = element_align.max(1); + let mut offset = element.size; + let mut align = element.align.max(1); let fields = merged .into_iter() .map(|write| { @@ -130,6 +171,7 @@ impl Layout { size: write.size, align: write.align, read_erased: write.read_erased, + repark: write.repark, }; offset += write.size; desc @@ -137,8 +179,7 @@ impl Layout { .collect(); Layout { depth, - element_size, - element_align, + element, fields, size: offset, align, @@ -148,10 +189,9 @@ impl Layout { /// The union of several layouts over the same element and depth. pub fn union(layouts: &[&Layout]) -> Layout { let first = layouts.first().expect("a union needs at least one layout"); - let mut union = Layout::default().with_writes(first.depth, (first.element_size, first.element_align), &[]); + let mut union = Layout::default().with_writes(first.depth, first.element, &[]); for layout in layouts { - assert_eq!(union.element_size, layout.element_size, "union layouts must share the element size"); - assert_eq!(union.element_align, layout.element_align, "union layouts must share the element alignment"); + assert_eq!(union.element, layout.element, "union layouts must share the element"); assert_eq!(union.depth, layout.depth, "union layouts must share the depth"); let writes: Vec = layout .fields @@ -162,9 +202,10 @@ impl Layout { size: field.size, align: field.align, read_erased: field.read_erased, + repark: field.repark, }) .collect(); - union = union.with_writes(union.depth, (union.element_size, union.element_align), &writes); + union = union.with_writes(union.depth, union.element, &writes); } union } @@ -500,9 +541,9 @@ pub mod stack { pub fn copy_plan(from: &Layout, to: &Layout, carry_element: bool) -> Vec<(usize, usize, usize)> { let mut plan = Vec::new(); if carry_element { - assert_eq!(from.element_size, to.element_size, "a carried element must keep its size"); - if from.element_size > 0 { - plan.push((0, 0, from.element_size)); + assert_eq!(from.element.size, to.element.size, "a carried element must keep its size"); + if from.element.size > 0 { + plan.push((0, 0, from.element.size)); } } for field in &from.fields { @@ -534,6 +575,26 @@ pub fn element_dims() -> (usize, usize) { } } +/// The element slot a record wire of `T` carries, its erased glue bound at +/// the statically-known type. +pub fn element_write() -> ElementWrite { + unsafe fn clone_out(ptr: *const u8) -> Box { + Box::new(unsafe { read_element::(Rec::new(ptr)) }) + } + unsafe fn repark(value: &(dyn std::any::Any + Send + Sync), dst: *mut u8, arena: &crate::arena::Arena) -> Option<()> { + let value = value.downcast_ref::().expect("an element replays at its own type"); + unsafe { write_element(dst, value.clone(), arena) } + } + let (size, align) = element_dims::(); + ElementWrite { + size, + align, + parked: element_parked::(), + clone_out: clone_out::, + repark: repark::, + } +} + /// # Safety /// The record's element must be a `T` in the form [`element_parked`] picks, /// and the borrow is only valid while the record is. @@ -728,6 +789,128 @@ where } } +/// A record deep-copied out of its evaluation: the packed bytes plus owned +/// clones of every parked payload, replayable into a later evaluation's +/// storage through the layout's erased glue. The layout stays with the +/// holder, which proved it at wiring. +pub struct OwnedRecord { + bytes: Box<[u8]>, + element: Option>, + fields: Vec<(usize, Box)>, +} + +impl OwnedRecord { + /// # Safety + /// `rec` must be a live record of `layout`. + pub unsafe fn copy_out(layout: &Layout, rec: Rec) -> OwnedRecord { + let bytes: Box<[u8]> = unsafe { std::slice::from_raw_parts(rec.ptr(), layout.size) }.into(); + let element = layout.element.parked.then(|| unsafe { (layout.element.clone_out)(rec.ptr()) }); + let fields = layout + .fields + .iter() + .enumerate() + .filter(|(_, field)| field.repark.is_some()) + .map(|(index, field)| (index, unsafe { (field.read_erased)(rec.ptr().add(field.offset)) })) + .collect(); + OwnedRecord { bytes, element, fields } + } + + /// Replays the copy into fresh storage of `layout`, the layout it was + /// copied out under, re-parking droppable payloads against `arena`; + /// `None` reports arena exhaustion. + pub fn replay<'e>(&self, layout: &Layout, arena: &'e crate::arena::Arena) -> Option> { + let mut value = RecordValue::zeroed(); + let dst = match layout.frame_bytes() { + 0 => value.as_mut_ptr(), + bytes => stack::push(bytes), + }; + let written = self.write_into(layout, dst, arena); + if layout.frame_bytes() != 0 { + stack::pop(dst); + value = RecordValue::spilled(unsafe { Rec::new(dst.cast_const()) }); + } + written.map(|()| value) + } + + fn write_into(&self, layout: &Layout, dst: *mut u8, arena: &crate::arena::Arena) -> Option<()> { + unsafe { std::ptr::copy_nonoverlapping(self.bytes.as_ptr(), dst, self.bytes.len()) }; + if let Some(element) = &self.element { + unsafe { (layout.element.repark)(&**element, dst, arena) }?; + } + for (index, value) in &self.fields { + let field = &layout.fields[*index]; + let repark = field.repark.expect("copied fields carry re-park glue"); + unsafe { repark(&**value, dst.add(field.offset), arena) }?; + } + Some(()) + } +} + +// TODO: Convert to a `#[node_macro::node]` node with the monitor, once the +// macro grows a capture capability. +/// Memoizes a record wire: a hit replays the deep copy into the current +/// evaluation, a miss evaluates the edge and copies the record out. +pub struct RecordMemo { + edge: N, + layout: Layout, + cache: std::sync::Mutex>, +} + +impl RecordMemo { + pub fn new(edge: N, layout: &Layout) -> Self { + Self { + edge, + layout: layout.clone(), + cache: std::sync::Mutex::new(None), + } + } +} + +impl<'e, C, N> Node for RecordMemo +where + C: crate::graphene_hash::CacheHash + crate::context::ExtractArena, + N: Node>, +{ + type Output = RecordValue<'e>; + + fn eval(&self, input: &C) -> GPoll> { + let key = crate::registry::cache_key(input); + { + let cache = self.cache.lock().unwrap_or_else(std::sync::PoisonError::into_inner); + if let Some((hash, copy, finality)) = cache.as_ref() + && *hash == key + { + return match copy.replay(&self.layout, input.arena()) { + Some(value) => match finality { + crate::gpoll::Finality::AllFinal => GPoll::Final(value), + crate::gpoll::Finality::Partial => GPoll::Partial(value), + }, + None => GPoll::arena_exhausted(), + }; + } + } + let result = self.edge.eval(input); + let publishable = match &result { + GPoll::Final(record) => Some((record, crate::gpoll::Finality::AllFinal)), + GPoll::Partial(record) => Some((record, crate::gpoll::Finality::Partial)), + _ => None, + }; + if let Some((record, finality)) = publishable { + let copy = unsafe { OwnedRecord::copy_out(&self.layout, self.layout.rec(record)) }; + *self.cache.lock().unwrap_or_else(std::sync::PoisonError::into_inner) = Some((key, copy, finality)); + } + result + } + + fn extent(&self, input: &C) -> GPoll { + self.edge.extent(input) + } + + fn layout(&self) -> Option<&Layout> { + Some(&self.layout) + } +} + /// Lifts a plain producer onto a record wire: the element lands at offset 0 /// of a fresh element-only record, parked when it carries drop glue. pub struct RecordLift { @@ -736,11 +919,11 @@ pub struct RecordLift { _marker: std::marker::PhantomData El>, } -impl RecordLift { +impl RecordLift { pub fn new(edge: N) -> Self { Self { edge, - layout: Layout::default().with_writes(0, element_dims::(), &[]), + layout: Layout::default().with_writes(0, element_write::(), &[]), _marker: std::marker::PhantomData, } } @@ -771,11 +954,11 @@ pub struct RecordLiftLend { _marker: std::marker::PhantomData El>, } -impl RecordLiftLend { +impl RecordLiftLend { pub fn new(edge: N) -> Self { Self { edge, - layout: Layout::default().with_writes(0, element_dims::(), &[]), + layout: Layout::default().with_writes(0, element_write::(), &[]), _marker: std::marker::PhantomData, } } @@ -930,7 +1113,14 @@ mod tests { } fn sized_field(name: &'static str, size: usize, align: usize) -> FieldWrite { - FieldWrite { name, level: 0, size, align, read_erased: unread } + FieldWrite { + name, + level: 0, + size, + align, + read_erased: unread, + repark: None, + } } fn f64_field(name: &'static str) -> FieldWrite { @@ -939,7 +1129,7 @@ mod tests { #[test] fn canonical_order_and_offsets() { - let layout = Layout::default().with_writes(0, (8, 8), &[sized_field("tint", 4, 4), f64_field("opacity"), sized_field("flag", 1, 1)]); + let layout = Layout::default().with_writes(0, element_write::(), &[sized_field("tint", 4, 4), f64_field("opacity"), sized_field("flag", 1, 1)]); assert_eq!(layout.offset_of("opacity", 0), Some(8)); assert_eq!(layout.offset_of("tint", 0), Some(16)); assert_eq!(layout.offset_of("flag", 0), Some(20)); @@ -950,22 +1140,22 @@ mod tests { #[test] #[should_panic(expected = "two different sizes")] fn size_conflicts_panic() { - let layout = Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")]); - layout.with_writes(0, (8, 8), &[sized_field("opacity", 4, 4)]); + let layout = Layout::default().with_writes(0, element_write::(), &[f64_field("opacity")]); + layout.with_writes(0, element_write::(), &[sized_field("opacity", 4, 4)]); } #[test] fn union_is_order_independent() { - let a = Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")]); - let b = Layout::default().with_writes(0, (8, 8), &[f64_field("length")]); + let a = Layout::default().with_writes(0, element_write::(), &[f64_field("opacity")]); + let b = Layout::default().with_writes(0, element_write::(), &[f64_field("length")]); assert_eq!(Layout::union(&[&a, &b]), Layout::union(&[&b, &a])); assert!(Layout::union(&[&a, &b]).offset_of("length", 0).is_some()); } #[test] fn translation_moves_fields_and_fills_census_defaults() { - let source = Layout::default().with_writes(0, (8, 8), &[f64_field("length")]); - let union = Layout::union(&[&source, &Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")])]); + let source = Layout::default().with_writes(0, element_write::(), &[f64_field("length")]); + let union = Layout::union(&[&source, &Layout::default().with_writes(0, element_write::(), &[f64_field("opacity")])]); let plan = SourcePlan::new(&source, &union).unwrap(); let record = [5f64, 7f64]; @@ -978,7 +1168,7 @@ mod tests { #[test] fn identity_layouts_forward() { - let layout = Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")]); + let layout = Layout::default().with_writes(0, element_write::(), &[f64_field("opacity")]); assert!(SourcePlan::new(&layout, &layout.clone()).is_none()); } @@ -1009,6 +1199,29 @@ mod tests { assert_eq!(unsafe { read_element::(rec) }, "moved once"); } + #[test] + fn owned_records_replay_re_parked_payloads_after_the_source_dies() { + let layout = Layout::default().with_writes(0, element_write::(), &[FieldWrite::of::(0)]); + let mut buffer = vec![0u64; layout.size.div_ceil(8)]; + let base: *mut u8 = buffer.as_mut_ptr().cast(); + + let copy = { + let arena = crate::arena::Arena::new(1024).unwrap(); + unsafe { write_element(base, String::from("element"), &arena) }.unwrap(); + let (name, _) = arena.alloc(String::from("field")).unwrap(); + unsafe { write_field::<&str>(base, layout.offset_of("name", 0).unwrap(), name.as_str()) }; + unsafe { OwnedRecord::copy_out(&layout, Rec::new(base)) } + }; + buffer.fill(u64::MAX); + + let replay_arena = crate::arena::Arena::new(1024).unwrap(); + stack::reserve(layout.frame_bytes()); + let value = copy.replay(&layout, &replay_arena).unwrap(); + let rec = layout.rec(&value); + assert_eq!(unsafe { read_element::(rec) }, "element"); + assert_eq!(unsafe { rec.read::<&str>(layout.offset_of("name", 0).unwrap()) }, "field"); + } + #[test] fn record_values_are_two_words() { assert_eq!(size_of::(), 16); @@ -1017,7 +1230,7 @@ mod tests { #[test] fn layouts_resolve_inline_and_spilled_values() { - let inline = Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")]); + let inline = Layout::default().with_writes(0, element_write::(), &[f64_field("opacity")]); assert!(inline.is_inline()); assert_eq!(inline.frame_bytes(), 0); let mut value = RecordValue::zeroed(); @@ -1029,7 +1242,7 @@ mod tests { assert_eq!(unsafe { rec.element::() }, 4.); assert_eq!(unsafe { rec.read::(inline.offset_of("opacity", 0).unwrap()) }, 0.5); - let spilled = Layout::default().with_writes(0, (8, 8), &[f64_field("opacity"), f64_field("length")]); + let spilled = Layout::default().with_writes(0, element_write::(), &[f64_field("opacity"), f64_field("length")]); assert!(!spilled.is_inline()); assert_eq!(spilled.frame_bytes(), 24); let record = [1f64, 2., 3.]; diff --git a/node-graph/libraries/core-types/src/runtime.rs b/node-graph/libraries/core-types/src/runtime.rs index 3b7adb7d9c..9b75b568eb 100644 --- a/node-graph/libraries/core-types/src/runtime.rs +++ b/node-graph/libraries/core-types/src/runtime.rs @@ -171,7 +171,7 @@ mod tests { use crate::context::{ContextImpl, Ctx, CtxSnapshot, EvalScope, ExtractFootprint, ExtractVarArgs, VarArgLink, VarArgSlots}; use crate::gpoll::GPoll; use crate::node::Node; - use crate::record::{Layout, RecordExtract, RecordLift, element_dims}; + use crate::record::{Layout, RecordExtract, RecordLift, element_write}; use crate::transform::Footprint; use std::sync::Mutex; use std::sync::atomic::{AtomicU32, Ordering}; @@ -258,15 +258,15 @@ mod tests { } } - fn element_layout() -> Layout { - Layout::default().with_writes(0, element_dims::(), &[]) + fn element_layout() -> Layout { + Layout::default().with_writes(0, element_write::(), &[]) } - fn lifted(value: T) -> RecordLift> { + fn lifted(value: T) -> RecordLift> { RecordLift::new(SourceNode(value)) } - fn extract(graph: N) -> RecordExtract { + fn extract(graph: N) -> RecordExtract { RecordExtract::new(graph, &element_layout::()) } diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index e70c8e523e..968f6c6505 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -354,7 +354,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let flip_prelude = flip .then(|| { quote! { - let __layout = gcore::record::Layout::default().with_writes(0, gcore::record::element_dims::<#slot_value_type>(), &[]); + let __layout = gcore::record::Layout::default().with_writes(0, gcore::record::element_write::<#slot_value_type>(), &[]); let __frame_bytes = __layout.frame_bytes(); } }) @@ -365,8 +365,14 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn false => quote!(__layout, __frame_bytes, __marker: ::core::marker::PhantomData,), }) .into_iter(); - let fn_where = &parsed.where_clause; - let new_where = flip.then(|| quote!(#fn_where)).into_iter(); + // The flip prelude's `element_write` instantiates the erased glue at the + // output type, so `new` carries the bounds the glue needs. + let new_where = flip + .then(|| { + let existing = parsed.where_clause.iter().flat_map(|clause| clause.predicates.iter()); + quote!(where #(#existing,)* #slot_value_type: ::core::clone::Clone + ::core::marker::Send + ::core::marker::Sync + 'static) + }) + .into_iter(); let new_impl = match record.is_none() { true => quote! { #[automatically_derived] @@ -1547,19 +1553,19 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn .iter() .map(|marker| quote!(#core_types::record::FieldWrite::of::<#marker>(0))) .collect(); - let element_dims = match &shape.element_write { - Some(ty) => quote!((::core::mem::size_of::<#ty>(), ::core::mem::align_of::<#ty>())), - None => quote!((__carrier.element_size, __carrier.element_align)), + let element = match &shape.element_write { + Some(ty) => quote!(#core_types::record::element_write::<#ty>()), + None => quote!(__carrier.element), }; let layout_def = match shape.skips_carrier() { true => quote! { #vis fn #layout_fn() -> #core_types::record::Layout { - #core_types::record::Layout::default().with_writes(0, #element_dims, &[#(#write_descs),*]) + #core_types::record::Layout::default().with_writes(0, #element, &[#(#write_descs),*]) } }, false => quote! { #vis fn #layout_fn(__carrier: &#core_types::record::Layout) -> #core_types::record::Layout { - __carrier.with_writes(__carrier.depth, #element_dims, &[#(#write_descs),*]) + __carrier.with_writes(__carrier.depth, #element, &[#(#write_descs),*]) } }, }; @@ -2302,7 +2308,7 @@ fn flip_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_field let output_alias = format_ident!("__{}_output", fn_name); let alias_def = match alias_param_tokens.is_empty() { true => quote!(#[allow(non_camel_case_types)] type #output_alias = #output;), - false => quote!(#[allow(non_camel_case_types)] type #output_alias<#(#alias_param_tokens,)*> = #output;), + false => quote!(#[allow(non_camel_case_types, type_alias_bounds)] type #output_alias<#(#alias_param_tokens,)*> = #output;), }; let entries = rows.iter().filter_map(|row| { diff --git a/node-graph/nodes/gcore/src/record.rs b/node-graph/nodes/gcore/src/record.rs index 3fdbd9cc55..577367ad95 100644 --- a/node-graph/nodes/gcore/src/record.rs +++ b/node-graph/nodes/gcore/src/record.rs @@ -149,9 +149,10 @@ mod tests { size: 8, align: 8, read_erased: ::read_erased, + repark: None, }) .collect(); - Layout::default().with_writes(0, (8, 8), &writes) + Layout::default().with_writes(0, core_types::record::element_write::(), &writes) } fn reserve_for(layouts: &[&Layout]) { @@ -270,7 +271,7 @@ mod tests { let f64_source = f64_layout(&[]); let f64_faded = fade_layout(&f64_source); - let u32_source = Layout::default().with_writes(0, (4, 4), &[]); + let u32_source = Layout::default().with_writes(0, core_types::record::element_write::(), &[]); let u32_faded = fade_layout(&u32_source); reserve_for(&[&f64_source, &f64_faded, &u32_source, &u32_faded]); @@ -436,7 +437,7 @@ mod tests { #[test] fn census_fills_reference_defaults_from_static_data() { let source = f64_layout(&[]); - let labeled = Layout::default().with_writes(0, (8, 8), &[core_types::record::FieldWrite::of::