From 7a1d1f3238ba40b24610bf310749e2567c4fa8cc Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Thu, 6 Aug 2026 09:57:21 +0000 Subject: [PATCH] Define the frame memo through the record-opaque class over same-frame byte copies --- .../interpreted-executor/src/node_registry.rs | 107 +----------------- node-graph/libraries/core-types/src/record.rs | 25 +++- node-graph/nodes/gcore/src/memo.rs | 64 ++++++----- 3 files changed, 62 insertions(+), 134 deletions(-) diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index a9759ce206..cc2e64bca6 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -19,7 +19,7 @@ use graphene_std::transform::Footprint; use graphene_std::uuid::NodeId; use graphene_std::vector::Vector; use graphene_std::{Artboard, Context, Graphic, ProtoNodeIdentifier, SourceId, concrete, fn_type}; -use node_registry_macros::{clone_node, convert_node, frame_memo_node, into_node, lend_node, record_extract_node, record_lift_node}; +use node_registry_macros::{clone_node, convert_node, into_node, lend_node, record_extract_node, record_lift_node}; use std::collections::HashMap; #[cfg(feature = "gpu")] use wgpu_executor::WgpuExecutorHandle; @@ -116,79 +116,55 @@ fn node_registry() -> HashMap> { // ============ lend_node!(()), clone_node!(()), - frame_memo_node!(()), lend_node!(RuntimeHandle), clone_node!(RuntimeHandle), - frame_memo_node!(RuntimeHandle), lend_node!(SourceId), clone_node!(SourceId), - frame_memo_node!(SourceId), lend_node!(bool), clone_node!(bool), - frame_memo_node!(bool), lend_node!(List), clone_node!(List), - frame_memo_node!(List), lend_node!(List), clone_node!(List), - frame_memo_node!(List), lend_node!(List), clone_node!(List), - frame_memo_node!(List), lend_node!(List>), clone_node!(List>), - frame_memo_node!(List>), lend_node!(List), clone_node!(List), - frame_memo_node!(List), lend_node!(Image), clone_node!(Image), - frame_memo_node!(Image), lend_node!(List), clone_node!(List), - frame_memo_node!(List), lend_node!(List), clone_node!(List), - frame_memo_node!(List), lend_node!(List), clone_node!(List), - frame_memo_node!(List), lend_node!(List), clone_node!(List), - frame_memo_node!(List), lend_node!(List), clone_node!(List), - frame_memo_node!(List), lend_node!(List), clone_node!(List), - frame_memo_node!(List), lend_node!(List), clone_node!(List), - frame_memo_node!(List), lend_node!(List), clone_node!(List), - frame_memo_node!(List), lend_node!(List), clone_node!(List), - frame_memo_node!(List), lend_node!(List), clone_node!(List), - frame_memo_node!(List), lend_node!(AttributeDyn), clone_node!(AttributeDyn), - frame_memo_node!(AttributeDyn), lend_node!(AttributeValueDyn), clone_node!(AttributeValueDyn), - frame_memo_node!(AttributeValueDyn), lend_node!(ListDyn), clone_node!(ListDyn), - frame_memo_node!(ListDyn), #[cfg(target_family = "wasm")] lend_node!(CanvasHandle), #[cfg(target_family = "wasm")] clone_node!(CanvasHandle), #[cfg(target_family = "wasm")] - frame_memo_node!(CanvasHandle), lend_node!(f64), record_lift_node!(f64), record_extract_node!(f64), @@ -276,190 +252,129 @@ fn node_registry() -> HashMap> { record_lift_node!(wgpu_executor::WgpuPipelineCache), #[cfg(feature = "gpu")] record_extract_node!(wgpu_executor::WgpuPipelineCache), - frame_memo_node!(f64), lend_node!(f32), clone_node!(f32), - frame_memo_node!(f32), lend_node!(u32), clone_node!(u32), - frame_memo_node!(u32), lend_node!(u64), clone_node!(u64), - frame_memo_node!(u64), lend_node!(DVec2), clone_node!(DVec2), - frame_memo_node!(DVec2), lend_node!(String), clone_node!(String), - frame_memo_node!(String), lend_node!(DAffine2), clone_node!(DAffine2), - frame_memo_node!(DAffine2), lend_node!(Footprint), clone_node!(Footprint), - frame_memo_node!(Footprint), lend_node!(RenderOutput), clone_node!(RenderOutput), - frame_memo_node!(RenderOutput), lend_node!(std::sync::Arc), clone_node!(std::sync::Arc), - frame_memo_node!(std::sync::Arc), #[cfg(feature = "gpu")] lend_node!(List>), #[cfg(feature = "gpu")] clone_node!(List>), #[cfg(feature = "gpu")] - frame_memo_node!(List>), lend_node!(Option), clone_node!(Option), - frame_memo_node!(Option), lend_node!(Option), clone_node!(Option), - frame_memo_node!(Option), lend_node!(Graphic), clone_node!(Graphic), - frame_memo_node!(Graphic), lend_node!(glam::f32::Vec2), clone_node!(glam::f32::Vec2), - frame_memo_node!(glam::f32::Vec2), lend_node!(glam::f32::Affine2), clone_node!(glam::f32::Affine2), - frame_memo_node!(glam::f32::Affine2), lend_node!(graphene_std::vector::style::Stroke), clone_node!(graphene_std::vector::style::Stroke), - frame_memo_node!(graphene_std::vector::style::Stroke), lend_node!(graphene_std::text::Font), clone_node!(graphene_std::text::Font), - frame_memo_node!(graphene_std::text::Font), lend_node!(List), clone_node!(List), - frame_memo_node!(List), lend_node!(DocumentNode), clone_node!(DocumentNode), - frame_memo_node!(DocumentNode), lend_node!(graphene_std::ContextModification), clone_node!(graphene_std::ContextModification), - frame_memo_node!(graphene_std::ContextModification), lend_node!(graphene_std::transform::Footprint), clone_node!(graphene_std::transform::Footprint), - frame_memo_node!(graphene_std::transform::Footprint), lend_node!(Box), clone_node!(Box), - frame_memo_node!(Box), lend_node!(graphene_std::blending::BlendMode), clone_node!(graphene_std::blending::BlendMode), - frame_memo_node!(graphene_std::blending::BlendMode), lend_node!(graphene_std::raster::LuminanceCalculation), clone_node!(graphene_std::raster::LuminanceCalculation), - frame_memo_node!(graphene_std::raster::LuminanceCalculation), lend_node!(graphene_std::vector::QRCodeErrorCorrectionLevel), clone_node!(graphene_std::vector::QRCodeErrorCorrectionLevel), - frame_memo_node!(graphene_std::vector::QRCodeErrorCorrectionLevel), lend_node!(graphene_std::extract_xy::XY), clone_node!(graphene_std::extract_xy::XY), - frame_memo_node!(graphene_std::extract_xy::XY), lend_node!(graphene_std::text_nodes::StringCapitalization), clone_node!(graphene_std::text_nodes::StringCapitalization), - frame_memo_node!(graphene_std::text_nodes::StringCapitalization), lend_node!(graphene_std::raster::RedGreenBlue), clone_node!(graphene_std::raster::RedGreenBlue), - frame_memo_node!(graphene_std::raster::RedGreenBlue), lend_node!(graphene_std::raster::RedGreenBlueAlpha), clone_node!(graphene_std::raster::RedGreenBlueAlpha), - frame_memo_node!(graphene_std::raster::RedGreenBlueAlpha), lend_node!(graphene_std::animation::RealTimeMode), clone_node!(graphene_std::animation::RealTimeMode), - frame_memo_node!(graphene_std::animation::RealTimeMode), lend_node!(graphene_std::raster::NoiseType), clone_node!(graphene_std::raster::NoiseType), - frame_memo_node!(graphene_std::raster::NoiseType), lend_node!(graphene_std::raster::FractalType), clone_node!(graphene_std::raster::FractalType), - frame_memo_node!(graphene_std::raster::FractalType), lend_node!(graphene_std::raster::CellularDistanceFunction), clone_node!(graphene_std::raster::CellularDistanceFunction), - frame_memo_node!(graphene_std::raster::CellularDistanceFunction), lend_node!(graphene_std::raster::CellularReturnType), clone_node!(graphene_std::raster::CellularReturnType), - frame_memo_node!(graphene_std::raster::CellularReturnType), lend_node!(graphene_std::raster::DomainWarpType), clone_node!(graphene_std::raster::DomainWarpType), - frame_memo_node!(graphene_std::raster::DomainWarpType), lend_node!(graphene_std::raster::RelativeAbsolute), clone_node!(graphene_std::raster::RelativeAbsolute), - frame_memo_node!(graphene_std::raster::RelativeAbsolute), lend_node!(graphene_std::raster::SelectiveColorChoice), clone_node!(graphene_std::raster::SelectiveColorChoice), - frame_memo_node!(graphene_std::raster::SelectiveColorChoice), lend_node!(graphene_std::vector::misc::GridType), clone_node!(graphene_std::vector::misc::GridType), - frame_memo_node!(graphene_std::vector::misc::GridType), lend_node!(graphene_std::vector::misc::ArcType), clone_node!(graphene_std::vector::misc::ArcType), - frame_memo_node!(graphene_std::vector::misc::ArcType), lend_node!(graphene_std::vector::misc::RowsOrColumns), clone_node!(graphene_std::vector::misc::RowsOrColumns), - frame_memo_node!(graphene_std::vector::misc::RowsOrColumns), lend_node!(graphene_std::vector::misc::MergeByDistanceAlgorithm), clone_node!(graphene_std::vector::misc::MergeByDistanceAlgorithm), - frame_memo_node!(graphene_std::vector::misc::MergeByDistanceAlgorithm), lend_node!(graphene_std::vector::misc::ExtrudeJoiningAlgorithm), clone_node!(graphene_std::vector::misc::ExtrudeJoiningAlgorithm), - frame_memo_node!(graphene_std::vector::misc::ExtrudeJoiningAlgorithm), lend_node!(graphene_std::vector::misc::PointSpacingType), clone_node!(graphene_std::vector::misc::PointSpacingType), - frame_memo_node!(graphene_std::vector::misc::PointSpacingType), lend_node!(graphene_std::vector::style::StrokeCap), clone_node!(graphene_std::vector::style::StrokeCap), - frame_memo_node!(graphene_std::vector::style::StrokeCap), lend_node!(graphene_std::vector::style::StrokeJoin), clone_node!(graphene_std::vector::style::StrokeJoin), - frame_memo_node!(graphene_std::vector::style::StrokeJoin), lend_node!(graphene_std::vector::style::StrokeAlign), clone_node!(graphene_std::vector::style::StrokeAlign), - frame_memo_node!(graphene_std::vector::style::StrokeAlign), lend_node!(graphene_std::vector::style::PaintOrder), clone_node!(graphene_std::vector::style::PaintOrder), - frame_memo_node!(graphene_std::vector::style::PaintOrder), lend_node!(graphene_std::vector::style::GradientType), clone_node!(graphene_std::vector::style::GradientType), - frame_memo_node!(graphene_std::vector::style::GradientType), lend_node!(graphene_std::vector::style::GradientSpreadMethod), clone_node!(graphene_std::vector::style::GradientSpreadMethod), - frame_memo_node!(graphene_std::vector::style::GradientSpreadMethod), lend_node!(Option), clone_node!(Option), - frame_memo_node!(Option), lend_node!(graphene_std::transform::ReferencePoint), clone_node!(graphene_std::transform::ReferencePoint), - frame_memo_node!(graphene_std::transform::ReferencePoint), lend_node!(graphene_std::vector::misc::CentroidType), clone_node!(graphene_std::vector::misc::CentroidType), - frame_memo_node!(graphene_std::vector::misc::CentroidType), lend_node!(graphene_std::vector::misc::BooleanOperation), clone_node!(graphene_std::vector::misc::BooleanOperation), - frame_memo_node!(graphene_std::vector::misc::BooleanOperation), lend_node!(graphene_std::text::TextAlign), clone_node!(graphene_std::text::TextAlign), - frame_memo_node!(graphene_std::text::TextAlign), lend_node!(graphene_std::transform::ScaleType), clone_node!(graphene_std::transform::ScaleType), - frame_memo_node!(graphene_std::transform::ScaleType), lend_node!(graphene_std::vector::misc::InterpolationDistribution), clone_node!(graphene_std::vector::misc::InterpolationDistribution), - frame_memo_node!(graphene_std::vector::misc::InterpolationDistribution), lend_node!(RenderIntermediate), clone_node!(RenderIntermediate), - frame_memo_node!(RenderIntermediate), lend_node!(wgpu_executor::WgpuExecutorHandle), clone_node!(wgpu_executor::WgpuExecutorHandle), - frame_memo_node!(wgpu_executor::WgpuExecutorHandle), lend_node!(Option), clone_node!(Option), - frame_memo_node!(Option), lend_node!(wgpu_executor::WgpuPipelineCache), clone_node!(wgpu_executor::WgpuPipelineCache), - frame_memo_node!(wgpu_executor::WgpuPipelineCache), ]; // ============= // CONVERT NODES @@ -708,28 +623,8 @@ mod node_registry_macros { }; } - macro_rules! frame_memo_node { - ($type:ty) => { - ( - ProtoNodeIdentifier::new("graphene_core::memo::FrameMemoNode"), - RegistryEntry { - io: NodeIOTypes::new(concrete!(Context), ref_type::<$type>(), vec![fn_type!(Context, $type)]), - constructor: |inputs| { - if inputs.len() != 1 { - return Err(ConstructionError::Arity { expected: 1, got: inputs.len() }); - } - let mut inputs = inputs.into_iter(); - let node = graphene_core::memo::FrameMemoNode::new(inputs.next().unwrap().downcast::<$type>()?); - Ok(EdgeHandle::new_ref(std::sync::Arc::new(node) as std::sync::Arc>)) - }, - }, - ) - }; - } - pub(crate) use clone_node; pub(crate) use convert_node; - pub(crate) use frame_memo_node; pub(crate) use into_node; pub(crate) use lend_node; pub(crate) use record_extract_node; diff --git a/node-graph/libraries/core-types/src/record.rs b/node-graph/libraries/core-types/src/record.rs index 0eb155ea02..1214bd4bc4 100644 --- a/node-graph/libraries/core-types/src/record.rs +++ b/node-graph/libraries/core-types/src/record.rs @@ -743,6 +743,29 @@ impl RecordSource { } } +/// # Safety +/// `rec` must be a live record of `layout`. +pub unsafe fn copy_record_bytes(layout: &Layout, rec: Rec) -> Box<[u8]> { + unsafe { std::slice::from_raw_parts(rec.ptr(), layout.size) }.into() +} + +/// Builds a record value over `bytes`, a record of `layout` copied out +/// earlier: inline layouts copy into the value, spilled ones alias the +/// bytes. +/// +/// # Safety +/// `bytes` must hold a record of `layout` whose parked references are still +/// live; both hold for a copy taken in the same evaluation frame. +pub unsafe fn record_from_bytes<'e>(layout: &Layout, bytes: &'e [u8]) -> RecordValue<'e> { + if layout.is_inline() { + let mut value = RecordValue::zeroed(); + unsafe { std::ptr::copy_nonoverlapping(bytes.as_ptr(), value.as_mut_ptr(), bytes.len()) }; + value + } else { + RecordValue::spilled(unsafe { Rec::new(bytes.as_ptr()) }) + } +} + /// A captured record: the layout plus a generation-checked handle to the /// arena copy, materialized by the introspection holder, which owns the /// arena. A dead generation materializes to `None`, never to a stale read. @@ -762,7 +785,7 @@ impl RecordCapture { /// # Safety /// `rec` must be a live record of `layout`. pub unsafe fn capture(layout: &Layout, rec: Rec, arena: &crate::arena::Arena) -> Option { - let bytes: Box<[u8]> = unsafe { std::slice::from_raw_parts(rec.ptr(), layout.size) }.into(); + let bytes = unsafe { copy_record_bytes(layout, rec) }; arena.alloc(bytes).map(|(_, weak)| RecordCapture { layout: layout.clone(), bytes: weak }) } diff --git a/node-graph/nodes/gcore/src/memo.rs b/node-graph/nodes/gcore/src/memo.rs index 3223b335b8..b3a3229a77 100644 --- a/node-graph/nodes/gcore/src/memo.rs +++ b/node-graph/nodes/gcore/src/memo.rs @@ -5,7 +5,7 @@ use core_types::gpoll::{Extent, Finality, GPoll}; use core_types::graphene_hash::CacheHash; use core_types::memo::IORecord; use core_types::node::Node; -use core_types::record::{OwnedRecord, RecordCapture, RecordValue}; +use core_types::record::{OwnedRecord, RecordCapture, RecordValue, copy_record_bytes, record_from_bytes}; use core_types::registry::cache_key; use std::sync::Arc; use std::sync::Mutex; @@ -52,12 +52,12 @@ where node.content.extent(ctx) } -#[node_macro::node(category(""), path(graphene_core::memo), skip_impl, extent(frame_memo_extent))] -fn frame_memo<'e, T: Clone + 'static + Send + Sync>( +#[node_macro::node(category(""), path(graphene_core::memo), extent(frame_memo_extent))] +fn frame_memo<'e>( ctx: impl Ctx + CacheHash + ExtractArena<'e>, - #[data] cell: ArenaCell>, - content: impl Node, Output = T>, -) -> GPoll<&'e T> { + #[data] cell: ArenaCell, 32>>, + content: impl Node, Output = RecordValue<'e>>, +) -> GPoll> { let arena = ctx.arena(); let table = match cell.load(arena) { Some(table) => table, @@ -66,28 +66,38 @@ fn frame_memo<'e, T: Clone + 'static + Send + Sync>( cell.store(weak); table } - None => return park(arena, content.eval(ctx)), + None => return content.eval(&ctx), }, }; + // SAFETY: published bytes are same-frame copies of this edge's records, + // so they carry the edge's layout with live parked references. + let revive = |bytes: &'e Box<[u8]>| unsafe { record_from_bytes(content.layout(), bytes) }; match table.lookup(cache_key(ctx)) { - Lookup::Hit(Finality::AllFinal, value) => GPoll::Final(value), - Lookup::Hit(Finality::Partial, value) => GPoll::Partial(value), - Lookup::Vacant(slot) => match content.eval(ctx) { - GPoll::Final(value) => GPoll::Final(slot.publish(value, Finality::AllFinal)), - GPoll::Partial(value) => GPoll::Partial(slot.publish(value, Finality::Partial)), + Lookup::Hit(Finality::AllFinal, bytes) => GPoll::Final(revive(bytes)), + Lookup::Hit(Finality::Partial, bytes) => GPoll::Partial(revive(bytes)), + Lookup::Vacant(slot) => match content.eval(&ctx) { + GPoll::Final(value) => { + // SAFETY: the value came from this edge, so it carries the edge's layout. + let bytes = unsafe { copy_record_bytes(content.layout(), content.layout().rec(&value)) }; + GPoll::Final(revive(slot.publish(bytes, Finality::AllFinal))) + } + GPoll::Partial(value) => { + // SAFETY: as above. + let bytes = unsafe { copy_record_bytes(content.layout(), content.layout().rec(&value)) }; + GPoll::Partial(revive(slot.publish(bytes, Finality::Partial))) + } unpublishable => { slot.release(); - park(arena, unpublishable) + unpublishable } }, - Lookup::Full => park(arena, content.eval(ctx)), + Lookup::Full => content.eval(&ctx), } } -fn frame_memo_extent(node: &FrameMemoNode, ctx: &C) -> GPoll +fn frame_memo_extent(node: &FrameMemoNode, ctx: &C) -> GPoll where - T: Clone + 'static + Send + Sync, - NodeContent: Node, + NodeContent: Node, { node.content.extent(ctx) } @@ -281,24 +291,24 @@ mod tests { } #[test] - fn frame_memo_turns_an_owned_edge_into_a_lending_edge() { + fn frame_memo_shares_one_record_copy_per_frame() { let arena = Arena::new(4096).unwrap(); let generations = []; let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); - let edge = EdgeHandle::new(Arc::new(ValueNode("lent out".to_string())) as Arc>); - let lending = EdgeHandle::new_ref(Arc::new(FrameMemoNode::new(edge.downcast::().unwrap())) as Arc>); - assert_eq!(*lending.ty(), core_types::registry::lend_edge_type::()); + let layout = element_layout::(); + let memo = FrameMemoNode::new(core_types::record::RecordLift::::new(ValueNode("lent out".to_string())), &layout); - let node = lending.downcast_lend::().unwrap(); - let GPoll::Final(first) = node.eval(&ctx) else { - panic!("lend must fill the frame table and lend"); + let GPoll::Final(first) = memo.eval(&ctx) else { + panic!("the miss must fill the frame table"); }; - let GPoll::Final(second) = node.eval(&ctx) else { - panic!("second eval must lend the published value"); + let GPoll::Final(second) = memo.eval(&ctx) else { + panic!("the hit must revive the published record"); }; + let first: &String = unsafe { core_types::record::borrow_element(layout.rec(&first)) }; + let second: &String = unsafe { core_types::record::borrow_element(layout.rec(&second)) }; assert_eq!(first, "lent out"); - assert!(std::ptr::eq(first, second)); + assert!(std::ptr::eq(first, second), "the hit shares the parked payload"); } }