From c6b669fc88a42bcbde008d6424f355c25cbc235b Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Fri, 28 Aug 2026 13:30:05 +0000 Subject: [PATCH] Key the element glue on static type projections --- node-graph/libraries/core-types/src/record.rs | 78 ++++++++++++++----- .../libraries/core-types/src/runtime.rs | 17 +++- node-graph/libraries/core-types/src/value.rs | 20 ++++- node-graph/libraries/wgpu-executor/src/lib.rs | 2 +- node-graph/nodes/brush/src/brush.rs | 8 ++ node-graph/nodes/gcore/src/memo.rs | 6 +- node-graph/nodes/gcore/src/record.rs | 5 +- node-graph/nodes/math/src/lib.rs | 3 +- 8 files changed, 107 insertions(+), 32 deletions(-) diff --git a/node-graph/libraries/core-types/src/record.rs b/node-graph/libraries/core-types/src/record.rs index 6ed3962a7c..1d67eb5f0c 100644 --- a/node-graph/libraries/core-types/src/record.rs +++ b/node-graph/libraries/core-types/src/record.rs @@ -666,7 +666,7 @@ impl<'e, C, N: Node>> RecordEdge<'e, C> for N {} /// Builds an element-only record from a kernel's poll: inline layouts land /// in the value, larger ones spill to the record stack, arena exhaustion of /// a parked element reports as an error poll. -pub fn lift_poll<'e, T: Send + Sync + 'static>(poll: GPoll, layout: &Layout, arena: &'e crate::arena::Arena) -> GPoll> { +pub fn lift_poll<'e, T: Send + Sync>(poll: GPoll, layout: &Layout, arena: &'e crate::arena::Arena) -> GPoll> { let build = |element: T| { if layout.frame_bytes() == 0 { let mut value = RecordValue::zeroed(); @@ -1297,7 +1297,7 @@ pub unsafe fn write_field(dst: *mut u8, offset: usize, value: T) { /// `dst` must be the claimed frame (or inline scratch when `frame_bytes` is /// 0) of a record whose element is `T` and whose frame size is `frame_bytes`, /// with every carried field already written. -pub unsafe fn lift_poll_into<'e, T: Send + Sync + 'static>(poll: GPoll, dst: *mut u8, frame_bytes: usize, arena: &'e crate::arena::Arena) -> GPoll> { +pub unsafe fn lift_poll_into<'e, T: Send + Sync>(poll: GPoll, dst: *mut u8, frame_bytes: usize, arena: &'e crate::arena::Arena) -> GPoll> { let release = || { if frame_bytes != 0 { stack::truncate_above(dst, frame_bytes); @@ -1367,17 +1367,32 @@ static DEEP_ELEMENT_CLONES: std::sync::LazyLock( +pub fn register_deep_element_clone( clone_out: unsafe fn(*const u8) -> Box, repark: unsafe fn(&(dyn std::any::Any + Send + Sync), *mut u8, &crate::arena::Arena) -> Option<()>, ) { - DEEP_ELEMENT_CLONES.lock().unwrap().insert(std::any::TypeId::of::(), DeepElementGlue { clone_out, repark }); + DEEP_ELEMENT_CLONES.lock().unwrap().insert(std::any::TypeId::of::(), DeepElementGlue { clone_out, repark }); } fn deep_element_glue(type_id: std::any::TypeId) -> Option { DEEP_ELEMENT_CLONES.lock().unwrap().get(&type_id).copied() } +/// The value with its lifetimes substituted by `'static`, for erased storage +/// whose reads re-bind a live lifetime. +/// +/// # Safety +/// The erased value's borrows must not be used past their real lifetimes: the +/// stored form may only be read through a surface that re-binds a lifetime no +/// longer than the borrows' own, or after deep glue replaced every borrow with +/// owned content. +pub unsafe fn erase_static(value: T) -> T::Static { + let value = std::mem::ManuallyDrop::new(value); + // SAFETY: `Static` is `Self` with lifetimes substituted, layout-identical + // by `StaticTypeSized`'s contract. + unsafe { std::ptr::read((&raw const value).cast::()) } +} + /// Deep-copy overrides for field values whose content borrows the /// evaluation's arena (a graphic list holding native groups), keyed by the /// field's owned value form. Consulted at the persistence seams only: @@ -1416,18 +1431,29 @@ fn deepen_field_value(value: Box) -> Box() -> ElementWrite { - unsafe fn clone_out(ptr: *const u8) -> Box { - if let Some(deep) = deep_element_glue(std::any::TypeId::of::()) { +pub fn element_write() -> ElementWrite +where + T::Static: Clone + Send + Sync, +{ + unsafe fn clone_out(ptr: *const u8) -> Box + where + T::Static: Clone + Send + Sync, + { + if let Some(deep) = deep_element_glue(std::any::TypeId::of::()) { return unsafe { (deep.clone_out)(ptr) }; } - Box::new(unsafe { read_element::(Rec::new(ptr)) }) + // SAFETY: a lifetime-carrying element type registers deep glue, so + // this shallow path only erases borrow-free values. + Box::new(unsafe { erase_static(read_element::(Rec::new(ptr))) }) } - unsafe fn repark(value: &(dyn std::any::Any + Send + Sync), dst: *mut u8, arena: &crate::arena::Arena) -> Option<()> { - if let Some(deep) = deep_element_glue(std::any::TypeId::of::()) { + unsafe fn repark(value: &(dyn std::any::Any + Send + Sync), dst: *mut u8, arena: &crate::arena::Arena) -> Option<()> + where + T::Static: Clone + Send + Sync, + { + if let Some(deep) = deep_element_glue(std::any::TypeId::of::()) { return unsafe { (deep.repark)(value, dst, arena) }; } - let value = value.downcast_ref::().expect("an element replays at its own type"); + 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::(); @@ -1435,7 +1461,7 @@ pub fn element_write() -> ElementWrite { size, align, parked: element_parked::(), - type_id: std::any::TypeId::of::(), + type_id: std::any::TypeId::of::(), clone_out: clone_out::, repark: repark::, content_hash: None, @@ -1445,7 +1471,10 @@ pub fn element_write() -> ElementWrite { /// [`element_write`] plus the content hashing and equality glue, for element /// types that support them. -pub fn element_write_hashed() -> ElementWrite { +pub fn element_write_hashed() -> ElementWrite +where + T::Static: Clone + Send + Sync, +{ unsafe fn content_hash(ptr: *const u8, state: &mut dyn core::hash::Hasher) { let mut state = state; unsafe { borrow_element::(Rec::new(ptr)) }.cache_hash(&mut state); @@ -1470,7 +1499,10 @@ pub trait ElementWritePickHashed { fn element_write(&self) -> ElementWrite; } -impl ElementWritePickHashed for ElementWritePick { +impl ElementWritePickHashed for ElementWritePick +where + T::Static: Clone + Send + Sync, +{ fn element_write(&self) -> ElementWrite { element_write_hashed::() } @@ -1480,7 +1512,10 @@ pub trait ElementWritePickPlain { fn element_write(&self) -> ElementWrite; } -impl ElementWritePickPlain for &ElementWritePick { +impl ElementWritePickPlain for &ElementWritePick +where + T::Static: Clone + Send + Sync, +{ fn element_write(&self) -> ElementWrite { element_write::() } @@ -1505,7 +1540,7 @@ pub unsafe fn read_element(rec: Rec) -> T { /// # Safety /// `dst` must be fresh element storage of a record whose element is `T`. /// `None` reports arena exhaustion for a parked element. -pub unsafe fn write_element(dst: *mut u8, value: T, arena: &crate::arena::Arena) -> Option<()> { +pub unsafe fn write_element(dst: *mut u8, value: T, arena: &crate::arena::Arena) -> Option<()> { match element_parked::() { true => { let (parked, _) = arena.alloc(value)?; @@ -1803,7 +1838,10 @@ pub struct RecordLift { _marker: std::marker::PhantomData El>, } -impl RecordLift { +impl RecordLift +where + El::Static: Clone + Send + Sync, +{ pub fn new(edge: N) -> Self { Self { edge, @@ -1943,8 +1981,8 @@ impl<'e> RunBuilder<'e> { /// Starts the next lane: moves its element in and default-fills its /// fields. Returns the lane index; `None` reports arena exhaustion. - pub fn push(&mut self, element: T) -> Option { - assert_eq!(std::any::TypeId::of::(), self.layout.element.type_id, "the pushed element must match the layout's element type"); + pub fn push(&mut self, element: T) -> Option { + assert_eq!(std::any::TypeId::of::(), self.layout.element.type_id, "the pushed element must match the layout's element type"); assert!(self.pushed < self.len, "the builder holds exactly its declared lane count"); let lane = self.pushed; let stride = self.layout.lane_stride(); @@ -2558,7 +2596,7 @@ mod tests { fn the_element_write_pick_selects_the_content_glue_by_type() { use super::{ElementWritePickHashed as _, ElementWritePickPlain as _}; - #[derive(Clone)] + #[derive(Clone, dyn_any::DynAny)] struct Opaque; let hashed = (&ElementWritePick::(std::marker::PhantomData)).element_write(); diff --git a/node-graph/libraries/core-types/src/runtime.rs b/node-graph/libraries/core-types/src/runtime.rs index b852846bc0..1aad4f5838 100644 --- a/node-graph/libraries/core-types/src/runtime.rs +++ b/node-graph/libraries/core-types/src/runtime.rs @@ -20,7 +20,7 @@ pub trait Runtime { fn spawn(&self, source: SourceId, future: SourceFuture) -> bool; } -#[derive(Clone)] +#[derive(Clone, dyn_any::DynAny)] pub struct RuntimeHandle(pub Arc); // SAFETY: wasm is single threaded, so the handle never actually crosses a thread. @@ -258,15 +258,24 @@ mod tests { } } - fn element_layout() -> Layout { + fn element_layout() -> Layout + where + T::Static: Clone + Send + Sync, + { Layout::default().with_writes(0, element_write::(), &[]) } - fn lifted(value: T) -> RecordLift> { + fn lifted(value: T) -> RecordLift> + where + T::Static: Clone + Send + Sync, + { RecordLift::new(SourceNode(value)) } - fn extract>>(mut graph: N) -> RecordExtract { + fn extract>>(mut graph: N) -> RecordExtract + where + El::Static: Clone + Send + Sync, + { stack::reserve(1 << 12); let layout = element_layout::(); graph.set_layout(crate::record::RecordLayout { diff --git a/node-graph/libraries/core-types/src/value.rs b/node-graph/libraries/core-types/src/value.rs index 33025b8a02..98712adcb5 100644 --- a/node-graph/libraries/core-types/src/value.rs +++ b/node-graph/libraries/core-types/src/value.rs @@ -20,7 +20,10 @@ pub struct ValueSource { layout: crate::record::Layout, } -impl ValueSource { +impl ValueSource +where + T::Static: Clone + Send + Sync, +{ pub fn new(value: T) -> Self { Self { value, @@ -46,7 +49,10 @@ where } /// The native record edge of a constant. -pub fn record_value_edge(value: T) -> crate::registry::EdgeHandle { +pub fn record_value_edge(value: T) -> crate::registry::EdgeHandle +where + T::Static: Clone + Send + Sync, +{ crate::registry::EdgeHandle::new_record::(std::sync::Arc::new(ValueSource::new(value)) as std::sync::Arc) } @@ -57,7 +63,10 @@ pub struct LeveledValueSource { layout: crate::record::Layout, } -impl LeveledValueSource { +impl LeveledValueSource +where + T::Static: Clone + Send + Sync, +{ pub fn new(values: Vec) -> Self { Self { values, @@ -93,7 +102,10 @@ where } /// The native record edge of a constant level: the edge type is the element's. -pub fn leveled_record_value_edge(values: Vec) -> crate::registry::EdgeHandle { +pub fn leveled_record_value_edge(values: Vec) -> crate::registry::EdgeHandle +where + T::Static: Clone + Send + Sync, +{ crate::registry::EdgeHandle::new_record::(std::sync::Arc::new(LeveledValueSource::new(values)) as std::sync::Arc) } diff --git a/node-graph/libraries/wgpu-executor/src/lib.rs b/node-graph/libraries/wgpu-executor/src/lib.rs index 12d0095f32..15ba51c99a 100644 --- a/node-graph/libraries/wgpu-executor/src/lib.rs +++ b/node-graph/libraries/wgpu-executor/src/lib.rs @@ -61,7 +61,7 @@ impl std::fmt::Debug for WgpuExecutor { } /// Owned Arc handle carrying the executor as an ordinary wire value. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, dyn_any::DynAny)] pub struct WgpuExecutorHandle(pub std::sync::Arc); impl std::ops::Deref for WgpuExecutorHandle { diff --git a/node-graph/nodes/brush/src/brush.rs b/node-graph/nodes/brush/src/brush.rs index 6f051af355..e09f6e3a2f 100644 --- a/node-graph/nodes/brush/src/brush.rs +++ b/node-graph/nodes/brush/src/brush.rs @@ -26,6 +26,14 @@ pub struct BrushStampGenerator { transform: DAffine2, } +// SAFETY: `Static` is `Self` with `P` at its own static projection. +unsafe impl dyn_any::StaticType for BrushStampGenerator

+where + P::Static: Pixel + Alpha, +{ + type Static = BrushStampGenerator; +} + impl Transform for BrushStampGenerator

{ fn transform(&self) -> DAffine2 { self.transform diff --git a/node-graph/nodes/gcore/src/memo.rs b/node-graph/nodes/gcore/src/memo.rs index 657849bed4..333f3d0683 100644 --- a/node-graph/nodes/gcore/src/memo.rs +++ b/node-graph/nodes/gcore/src/memo.rs @@ -250,7 +250,10 @@ mod tests { EvalScope::new(Some(0.5), None, None, generations, arena) } - fn element_layout() -> core_types::record::Layout { + fn element_layout() -> core_types::record::Layout + where + T::Static: Clone + Send + Sync, + { core_types::record::Layout::default().with_writes(0, core_types::record::element_write::(), &[]) } @@ -312,6 +315,7 @@ mod tests { #[test] fn memo_copy_out_consults_the_deep_element_clone() { #[derive(Clone, Debug, PartialEq)] + #[derive(dyn_any::DynAny)] struct Payload(String, u32); unsafe fn deep(ptr: *const u8) -> Box { let value = unsafe { core_types::record::borrow_element::(core_types::record::Rec::new(ptr)) }; diff --git a/node-graph/nodes/gcore/src/record.rs b/node-graph/nodes/gcore/src/record.rs index add057dd75..0e982d67d4 100644 --- a/node-graph/nodes/gcore/src/record.rs +++ b/node-graph/nodes/gcore/src/record.rs @@ -604,7 +604,10 @@ mod tests { node } - fn lifted_value(value: T) -> (core_types::record::RecordLift>, Layout) { + fn lifted_value(value: T) -> (core_types::record::RecordLift>, Layout) + where + T::Static: Clone + Send + Sync, + { let lift = core_types::record::RecordLift::::new(ValueNode(value)); let layout = Node::::layout(&lift).clone(); (lift, layout) diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index c669b0bc3f..258760c0a5 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -1051,7 +1051,8 @@ mod graphene_test { /// element-only layout for the generated node's constructor. fn lifted(node: N) -> (RecordLift, Layout) where - T: Clone + Send + Sync + 'static, + T: Clone + Send + Sync + core_types::StaticTypeSized + 'static, + ::Static: Clone + Send + Sync, N: for<'c> Node, Output = T>, { let lift = RecordLift::::new(node);