From 34ea9c59fdc8877780667e5e30d5213645cc4551 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Thu, 27 Aug 2026 12:05:14 +0000 Subject: [PATCH] Deepen graphic-list field values at the persistence seams --- node-graph/libraries/core-types/src/record.rs | 64 ++++++++- .../libraries/graphic-types/src/graphic.rs | 124 +++++++++++++++++- 2 files changed, 182 insertions(+), 6 deletions(-) diff --git a/node-graph/libraries/core-types/src/record.rs b/node-graph/libraries/core-types/src/record.rs index 92ebc0b804..c0d1549378 100644 --- a/node-graph/libraries/core-types/src/record.rs +++ b/node-graph/libraries/core-types/src/record.rs @@ -1275,6 +1275,42 @@ fn deep_element_glue(type_id: std::any::TypeId) -> Option { DEEP_ELEMENT_CLONES.lock().unwrap().get(&type_id).copied() } +/// 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: +/// `read_erased` itself stays shallow, since introspection reads captures in +/// generation. Both halves decline when the value already owns all of its +/// content, so group-free values pay no extra clone: `copy_out` returns +/// `None` for unchanged, `replay` returns `Some(None)` for unchanged and +/// `None` for arena exhaustion. +#[derive(Clone, Copy)] +struct DeepFieldGlue { + copy_out: fn(&dyn crate::list::AnyAttributeValue) -> Option>, + replay: fn(&dyn crate::list::AnyAttributeValue, &crate::arena::Arena) -> Option>>, +} + +static DEEP_FIELD_VALUES: std::sync::LazyLock>> = std::sync::LazyLock::new(Default::default); + +/// Registers the deep copy-out and replay pair for field values of `T`. +/// Called at startup from the crate that owns the type. +pub fn register_deep_field_value( + copy_out: fn(&dyn crate::list::AnyAttributeValue) -> Option>, + replay: fn(&dyn crate::list::AnyAttributeValue, &crate::arena::Arena) -> Option>>, +) { + DEEP_FIELD_VALUES.lock().unwrap().insert(std::any::TypeId::of::(), DeepFieldGlue { copy_out, replay }); +} + +fn deep_field_glue(type_id: std::any::TypeId) -> Option { + DEEP_FIELD_VALUES.lock().unwrap().get(&type_id).copied() +} + +fn deepen_field_value(value: Box) -> Box { + match deep_field_glue(value.as_any().type_id()) { + Some(glue) => (glue.copy_out)(&*value).unwrap_or(value), + None => value, + } +} + /// The element slot a record wire of `T` carries, its erased glue bound at /// the statically-known type. pub fn element_write() -> ElementWrite { @@ -1635,7 +1671,7 @@ impl OwnedRecord { .iter() .enumerate() .filter(|(_, field)| field.repark.is_some()) - .map(|(index, field)| (index, unsafe { (field.read_erased)(rec.ptr().add(field.offset)) })) + .map(|(index, field)| (index, deepen_field_value(unsafe { (field.read_erased)(rec.ptr().add(field.offset)) }))) .collect(); OwnedRecord { bytes, element, fields } } @@ -1665,7 +1701,13 @@ impl OwnedRecord { 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) }?; + match deep_field_glue(value.as_any().type_id()) { + Some(glue) => match (glue.replay)(&**value, arena)? { + Some(resident) => unsafe { repark(&*resident, dst.add(field.offset), arena) }?, + None => unsafe { repark(&**value, dst.add(field.offset), arena) }?, + }, + None => unsafe { repark(&**value, dst.add(field.offset), arena) }?, + } } Some(()) } @@ -1925,7 +1967,14 @@ impl GroupItem { .enumerate() .filter(|(_, field)| field.repark.is_some()) .map(|(index, field)| { - let values = (0..self.len).map(|lane| unsafe { (field.read_erased)(frames.add(lane * stride + field.offset)) }).collect(); + let mut values: Vec<_> = (0..self.len).map(|lane| unsafe { (field.read_erased)(frames.add(lane * stride + field.offset)) }).collect(); + if let Some(glue) = values.first().and_then(|value| deep_field_glue(value.as_any().type_id())) { + for value in &mut values { + if let Some(deepened) = (glue.copy_out)(&**value) { + *value = deepened; + } + } + } (index, values) }) .collect(); @@ -1957,8 +2006,15 @@ impl GroupItem { for (index, values) in &owned.fields { let field = &self.layout.fields[*index]; let repark = field.repark.expect("copied fields carry re-park glue"); + let glue = values.first().and_then(|value| deep_field_glue(value.as_any().type_id())); for (lane, value) in values.iter().enumerate() { - unsafe { repark(&**value, frames.add(lane * stride + field.offset), arena) }?; + match glue { + Some(glue) => match (glue.replay)(&**value, arena)? { + Some(resident) => unsafe { repark(&*resident, frames.add(lane * stride + field.offset), arena) }?, + None => unsafe { repark(&**value, frames.add(lane * stride + field.offset), arena) }?, + }, + None => unsafe { repark(&**value, frames.add(lane * stride + field.offset), arena) }?, + } } } Some(GroupItem { diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index f78731fde5..48b52e0034 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -910,17 +910,62 @@ unsafe fn deep_repark_graphic(value: &(dyn std::any::Any + Send + Sync), dst: *m unsafe { core_types::record::write_element(dst, resident, arena) } } +fn graphic_contains_groups(graphic: &Graphic) -> bool { + match graphic { + Graphic::Group(_) => true, + Graphic::Graphic(children) => list_contains_groups(children), + _ => false, + } +} + +fn list_contains_groups(list: &List) -> bool { + (0..list.len()).any(|index| list.element(index).is_some_and(graphic_contains_groups)) +} + +/// The deep copy-out for graphic-list field values (the paint markers' owned +/// form): content groups leave in their owned form. Declines (`None`) for +/// group-free content, which already owns everything. +fn deep_clone_graphic_list(value: &dyn core_types::list::AnyAttributeValue) -> Option> { + let list = value.as_any().downcast_ref::>>().expect("a graphic list field deep-copies at its own type"); + let list = list.as_ref().filter(|list| list_contains_groups(list))?; + let mut list = list.clone(); + for element in list.iter_element_values_mut() { + *element = map_groups_to_owned(element); + } + Some(Box::new(Some(list))) +} + +/// The deep replay for graphic-list field values: owned content groups replay +/// into the serving arena before the field re-parks. `Some(None)` declines +/// for group-free content; `None` reports arena exhaustion. +fn deep_repark_graphic_list(value: &dyn core_types::list::AnyAttributeValue, arena: &core_types::arena::Arena) -> Option>> { + let list = value.as_any().downcast_ref::>>().expect("a graphic list field replays at its own type"); + let Some(list) = list.as_ref().filter(|list| list_contains_groups(list)) else { + return Some(None); + }; + let mut list = list.clone(); + for element in list.iter_element_values_mut() { + *element = map_groups_to_resident(element, arena)?; + } + Some(Some(Box::new(Some(list)))) +} + const _: () = { + fn register_all() { + core_types::record::register_deep_element_clone::(deep_clone_graphic, deep_repark_graphic); + core_types::record::register_deep_field_value::>>(deep_clone_graphic_list, deep_repark_graphic_list); + } + #[cfg(not(target_family = "wasm"))] #[core_types::ctor::ctor] fn register() { - core_types::record::register_deep_element_clone::(deep_clone_graphic, deep_repark_graphic); + register_all(); } #[cfg(target_family = "wasm")] #[unsafe(export_name = "__node_registry_deep_element_graphic")] extern "C" fn register() { - core_types::record::register_deep_element_clone::(deep_clone_graphic, deep_repark_graphic); + register_all(); } }; @@ -1316,6 +1361,81 @@ mod run_tests { assert_eq!(group_to_legacy_list(group), expected); } + fn native_group_paint(vector: &Vector, inner_layout: &Layout, inner_bytes: &mut Vec) -> List { + // SAFETY: `inner_bytes` is one lane of `inner_layout`; a parked element + // stores its reference. + unsafe { inner_bytes.as_mut_ptr().cast::<&Vector>().write(vector) }; + // SAFETY: `inner_bytes` holds one lane of `inner_layout` at its stride. + let inner_item = unsafe { GroupItem::from_resident(RecordBatch::new(inner_bytes.as_ptr(), 1, inner_layout)) }; + List::new_from_element(Graphic::Group(core_types::record::Group { + row: None, + content: core_types::record::GroupContent::Run(inner_item), + })) + } + + #[test] + fn an_owned_run_deep_copies_graphic_list_fields() { + let inner_vector = unit_square_at(DVec2::ZERO); + let inner_layout = Layout::default().with_writes(0, element_write_hashed::(), &[]); + let mut inner_bytes = vec![0u8; inner_layout.lane_stride()]; + let paint = native_group_paint(&inner_vector, &inner_layout, &mut inner_bytes); + + let vector = unit_square_at(DVec2::new(4., 4.)); + let layout = Layout::default().with_writes(0, element_write_hashed::(), &[FieldWrite::of::(0)]); + let mut bytes = vec![0u8; layout.lane_stride()]; + // SAFETY: `bytes` is one lane of `layout`; a parked element stores its + // reference, and the fill field stores the marker's value form. + unsafe { + bytes.as_mut_ptr().cast::<&Vector>().write(&vector); + bytes.as_mut_ptr().add(layout.offset_of(Fill::NAME, 0).unwrap()).cast::>>().write(Some(&paint)); + } + let (owned, expected) = { + // SAFETY: `bytes` holds one lane of `layout` at its stride. + let item = unsafe { GroupItem::from_resident(RecordBatch::new(bytes.as_ptr(), 1, &layout)) }; + (item.copy_out(), map_groups_to_legacy(paint.element(0).unwrap())) + }; + bytes.fill(u8::MAX); + inner_bytes.fill(u8::MAX); + drop(paint); + + let arena = core_types::arena::Arena::new(1 << 16).unwrap(); + let replayed = owned.replay(&arena).expect("the arena holds the replay"); + let run = RunView::::new(&replayed).expect("the run holds vector elements"); + let served = run.attr::(0).expect("the fill replays present"); + assert_eq!(map_groups_to_legacy(served.element(0).unwrap()), expected); + } + + #[test] + fn an_owned_record_deep_copies_graphic_list_fields() { + let inner_vector = unit_square_at(DVec2::ZERO); + let inner_layout = Layout::default().with_writes(0, element_write_hashed::(), &[]); + let mut inner_bytes = vec![0u8; inner_layout.lane_stride()]; + let paint = native_group_paint(&inner_vector, &inner_layout, &mut inner_bytes); + + let vector = unit_square_at(DVec2::new(4., 4.)); + let layout = Layout::default().with_writes(0, element_write_hashed::(), &[FieldWrite::of::(0)]); + let offset = layout.offset_of(Fill::NAME, 0).unwrap(); + let mut bytes = vec![0u8; layout.lane_stride()]; + // SAFETY: as in the run test above. + unsafe { + bytes.as_mut_ptr().cast::<&Vector>().write(&vector); + bytes.as_mut_ptr().add(offset).cast::>>().write(Some(&paint)); + } + // SAFETY: `bytes` is a live record of `layout`. + let owned = unsafe { core_types::record::OwnedRecord::copy_out(&layout, core_types::record::Rec::new(bytes.as_ptr())) }; + let expected = map_groups_to_legacy(paint.element(0).unwrap()); + bytes.fill(u8::MAX); + inner_bytes.fill(u8::MAX); + drop(paint); + + let arena = core_types::arena::Arena::new(1 << 16).unwrap(); + core_types::record::stack::reserve(layout.frame_bytes()); + let value = owned.replay(&layout, &arena).expect("the arena holds the replay"); + // SAFETY: the replay wrote a record of `layout`. + let served = unsafe { layout.rec(&value).read::>>(offset) }.expect("the fill replays present"); + assert_eq!(map_groups_to_legacy(served.element(0).unwrap()), expected); + } + #[test] fn a_run_and_its_legacy_list_agree_on_bounding_boxes() { let vectors = [unit_square_at(DVec2::ZERO), unit_square_at(DVec2::new(4., 4.))];