From 92ec2069ad7808e154c2e9b899846f6758935f9c Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Thu, 27 Aug 2026 11:27:26 +0000 Subject: [PATCH] Carry group content across memo seams in an owned form --- node-graph/libraries/core-types/src/record.rs | 227 ++++++++++++++++-- .../libraries/graphic-types/src/artboard.rs | 29 ++- .../libraries/graphic-types/src/boundary.rs | 19 ++ .../libraries/graphic-types/src/graphic.rs | 127 +++++++++- node-graph/nodes/gcore/src/memo.rs | 8 +- node-graph/nodes/graphic/src/record.rs | 25 +- 6 files changed, 394 insertions(+), 41 deletions(-) diff --git a/node-graph/libraries/core-types/src/record.rs b/node-graph/libraries/core-types/src/record.rs index 15021281bb..92ebc0b804 100644 --- a/node-graph/libraries/core-types/src/record.rs +++ b/node-graph/libraries/core-types/src/record.rs @@ -1247,23 +1247,31 @@ pub fn element_dims() -> (usize, usize) { } } -/// Deep clone-out overrides for element types whose plain clone borrows the +/// Deep-copy overrides for element types whose plain clone borrows the /// evaluation's arena (a `Graphic` holding a group interior). The generic /// element glue consults this registry, so every layout carrying such an /// element deep-copies at memo and capture seams regardless of which -/// constructor built the glue. The override must produce a value of the -/// element's own type that owns all of its content, so the generic re-park -/// replays it unchanged. -static DEEP_ELEMENT_CLONES: std::sync::LazyLock Box>>> = - std::sync::LazyLock::new(Default::default); - -/// Registers `clone_out` as the deep clone-out for elements of `T`. Called at -/// startup from the crate that owns the type. -pub fn register_deep_element_clone(clone_out: unsafe fn(*const u8) -> Box) { - DEEP_ELEMENT_CLONES.lock().unwrap().insert(std::any::TypeId::of::(), clone_out); +/// constructor built the glue. The clone-out must produce a value of the +/// element's own type that owns all of its content; the re-park restores that +/// value's arena-resident form before parking it. +#[derive(Clone, Copy)] +struct DeepElementGlue { + clone_out: unsafe fn(*const u8) -> Box, + repark: unsafe fn(&(dyn std::any::Any + Send + Sync), *mut u8, &crate::arena::Arena) -> Option<()>, } -fn deep_element_clone(type_id: std::any::TypeId) -> Option Box> { +static DEEP_ELEMENT_CLONES: std::sync::LazyLock>> = std::sync::LazyLock::new(Default::default); + +/// Registers the deep copy-out and re-park pair for elements of `T`. Called +/// at startup from the crate that owns the type. +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 }); +} + +fn deep_element_glue(type_id: std::any::TypeId) -> Option { DEEP_ELEMENT_CLONES.lock().unwrap().get(&type_id).copied() } @@ -1271,12 +1279,15 @@ fn deep_element_clone(type_id: std::any::TypeId) -> Option() -> ElementWrite { unsafe fn clone_out(ptr: *const u8) -> Box { - if let Some(deep) = deep_element_clone(std::any::TypeId::of::()) { - return unsafe { deep(ptr) }; + 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)) }) } 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::()) { + return unsafe { (deep.repark)(value, dst, arena) }; + } let value = value.downcast_ref::().expect("an element replays at its own type"); unsafe { write_element(dst, value.clone(), arena) } } @@ -1778,14 +1789,40 @@ where /// `len` records stored in the arena at `layout`'s stride. The layout is /// owned by the value and identifies the run's element type. The records are -/// valid for the current evaluation, like every arena payload. +/// valid for the current evaluation, like every arena payload. An owned item +/// ([`Self::copy_out`]) survives the generation instead, and must +/// [`Self::replay`] into a serving arena before any read. #[derive(Clone, Debug)] pub struct GroupItem { layout: Layout, - frames: *const u8, + storage: ItemStorage, len: usize, } +#[derive(Clone)] +enum ItemStorage { + Resident(*const u8), + Owned(std::sync::Arc), +} + +impl std::fmt::Debug for ItemStorage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ItemStorage::Resident(frames) => write!(f, "Resident({frames:p})"), + ItemStorage::Owned(_) => f.write_str("Owned(..)"), + } + } +} + +/// The lanes deep-copied out of their evaluation: the packed bytes plus owned +/// clones of every parked payload, per lane. The byte image's parked +/// references are stale until [`GroupItem::replay`] re-parks the payloads. +struct OwnedLanes { + bytes: Box<[u8]>, + elements: Option>>, + fields: Vec<(usize, Vec>)>, +} + // SAFETY: the same argument as for `RecordValue`. The element bounds and the // parking discipline make the record bytes thread-safe, and their validity // is tied to the shared arena. @@ -1813,11 +1850,19 @@ impl GroupItem { } Some(Self { layout, - frames: frames.cast_const(), + storage: ItemStorage::Resident(frames.cast_const()), len: batch.len(), }) } + /// The resident frame base. An owned item has none until it replays. + fn frames(&self) -> *const u8 { + match &self.storage { + ItemStorage::Resident(frames) => *frames, + ItemStorage::Owned(_) => panic!("an owned item replays into an arena before it is read"), + } + } + pub fn len(&self) -> usize { self.len } @@ -1842,17 +1887,85 @@ impl GroupItem { assert!(field.repark.is_none() || field.content_hash.is_some(), "a parked field adopts only with content glue"); } Self { - frames: batch.frames_ptr(), + storage: ItemStorage::Resident(batch.frames_ptr()), len: batch.len(), layout, } } - /// A batch view over the stored records. + /// A batch view over the stored records. Panics on an owned item, which + /// must [`Self::replay`] first. pub fn lanes(&self) -> crate::node::RecordBatch<'_> { // SAFETY: the constructors store `len` lanes of `layout` at the // layout's stride. - unsafe { crate::node::RecordBatch::new(self.frames, self.len, &self.layout) } + unsafe { crate::node::RecordBatch::new(self.frames(), self.len, &self.layout) } + } + + /// The item deep-copied out of its evaluation: lane bytes plus owned + /// clones of every parked payload, for storage that outlives the arena + /// generation. An owned item cannot be read until it replays. + pub fn copy_out(&self) -> GroupItem { + if let ItemStorage::Owned(_) = &self.storage { + return self.clone(); + } + let stride = self.layout.lane_stride(); + let frames = self.frames(); + // SAFETY: the constructors store `len` lanes of `layout` at the + // layout's stride, and the erased glue reads each lane's own region. + let bytes: Box<[u8]> = unsafe { std::slice::from_raw_parts(frames, self.len * stride) }.into(); + let elements = self + .layout + .element + .parked + .then(|| (0..self.len).map(|lane| unsafe { (self.layout.element.clone_out)(frames.add(lane * stride)) }).collect()); + let fields = self + .layout + .fields + .iter() + .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(); + (index, values) + }) + .collect(); + GroupItem { + layout: self.layout.clone(), + storage: ItemStorage::Owned(std::sync::Arc::new(OwnedLanes { bytes, elements, fields })), + len: self.len, + } + } + + /// Re-parks an owned item's lanes into `arena`, restoring the resident + /// form; `None` reports arena exhaustion. A resident item returns a plain + /// clone. + pub fn replay(&self, arena: &crate::arena::Arena) -> Option { + let ItemStorage::Owned(owned) = &self.storage else { + return Some(self.clone()); + }; + let stride = self.layout.lane_stride(); + let scratch = arena.alloc_scratch::((self.len * stride).div_ceil(8))?; + let frames = scratch.as_mut_ptr().cast::(); + // SAFETY: the scratch holds `len` lanes at the layout's stride, and the + // re-park glue writes each lane's own region under that layout. + unsafe { std::ptr::copy_nonoverlapping(owned.bytes.as_ptr(), frames, owned.bytes.len()) }; + if let Some(elements) = &owned.elements { + for (lane, element) in elements.iter().enumerate() { + unsafe { (self.layout.element.repark)(&**element, frames.add(lane * stride), arena) }?; + } + } + for (index, values) in &owned.fields { + let field = &self.layout.fields[*index]; + let repark = field.repark.expect("copied fields carry re-park glue"); + for (lane, value) in values.iter().enumerate() { + unsafe { repark(&**value, frames.add(lane * stride + field.offset), arena) }?; + } + } + Some(GroupItem { + layout: self.layout.clone(), + storage: ItemStorage::Resident(frames.cast_const()), + len: self.len, + }) } /// A typed view over the stored records, checked against the layout's @@ -1946,6 +2059,34 @@ pub struct Group { pub content: GroupContent, } +impl Group { + /// The group deep-copied out of its evaluation, every run in owned form. + pub fn copy_out(&self) -> Group { + let content = match &self.content { + GroupContent::Run(item) => GroupContent::Run(item.copy_out()), + GroupContent::Stack(children) => GroupContent::Stack(children.iter().map(Group::copy_out).collect()), + }; + Group { + row: self.row.as_ref().map(GroupItem::copy_out), + content, + } + } + + /// Re-parks an owned group's runs into `arena`; `None` reports arena + /// exhaustion. + pub fn replay(&self, arena: &crate::arena::Arena) -> Option { + let content = match &self.content { + GroupContent::Run(item) => GroupContent::Run(item.replay(arena)?), + GroupContent::Stack(children) => GroupContent::Stack(children.iter().map(|child| child.replay(arena)).collect::>()?), + }; + let row = match &self.row { + Some(row) => Some(row.replay(arena)?), + None => None, + }; + Some(Group { row, content }) + } +} + /// Compares one record region of `layout` by content. Regions without glue /// compare as bytes, which is the content for unparked values. unsafe fn record_content_eq(layout: &Layout, a: *const u8, b: *const u8) -> bool { @@ -1993,8 +2134,9 @@ impl PartialEq for GroupItem { return false; } let stride = self.layout.lane_stride(); + let (a, b) = (self.frames(), other.frames()); // SAFETY: both sides hold `len` lanes of the shared layout. - (0..self.len).all(|lane| unsafe { record_content_eq(&self.layout, self.frames.add(lane * stride), other.frames.add(lane * stride)) }) + (0..self.len).all(|lane| unsafe { record_content_eq(&self.layout, a.add(lane * stride), b.add(lane * stride)) }) } } @@ -2003,9 +2145,10 @@ impl graphene_hash::CacheHash for GroupItem { layout_shape_hash(&self.layout, state); state.write_usize(self.len); let stride = self.layout.lane_stride(); + let frames = self.frames(); for lane in 0..self.len { // SAFETY: `adopt` filled `len` lanes of `layout`. - unsafe { record_content_hash(&self.layout, self.frames.add(lane * stride), state) }; + unsafe { record_content_hash(&self.layout, frames.add(lane * stride), state) }; } } } @@ -2304,6 +2447,46 @@ mod tests { assert_eq!(unsafe { rec.read::<&str>(layout.offset_of("name", 0).unwrap()) }, "field"); } + #[test] + fn owned_items_replay_re_parked_payloads_after_the_source_dies() { + let layout = Layout::default().with_writes(0, element_write_hashed::(), &[FieldWrite::of::(0)]); + let stride = layout.lane_stride(); + let mut bytes = vec![0u8; stride * 2]; + + let owned = { + let arena = crate::arena::Arena::new(1024).unwrap(); + for lane in 0..2 { + let base = unsafe { bytes.as_mut_ptr().add(lane * stride) }; + unsafe { write_element(base, format!("element {lane}"), &arena) }.unwrap(); + let (name, _) = arena.alloc(format!("field {lane}")).unwrap(); + unsafe { write_field::<&str>(base, layout.offset_of("name", 0).unwrap(), name.as_str()) }; + } + let item = unsafe { GroupItem::from_resident(crate::node::RecordBatch::new(bytes.as_ptr(), 2, &layout)) }; + item.copy_out() + }; + bytes.fill(u8::MAX); + + let arena = crate::arena::Arena::new(1024).unwrap(); + let replayed = owned.replay(&arena).unwrap(); + let lanes = replayed.lanes(); + for lane in 0..2 { + let rec = lanes.get(lane).rec(); + assert_eq!(unsafe { read_element::(rec) }, format!("element {lane}")); + assert_eq!(unsafe { rec.read::<&str>(layout.offset_of("name", 0).unwrap()) }, format!("field {lane}")); + } + } + + #[test] + #[should_panic(expected = "an owned item replays")] + fn an_owned_item_refuses_reads() { + let layout = Layout::default().with_writes(0, element_write_hashed::(), &[]); + let mut bytes = vec![0u8; layout.lane_stride()]; + let arena = crate::arena::Arena::new(1024).unwrap(); + unsafe { write_element(bytes.as_mut_ptr(), String::from("parked"), &arena) }.unwrap(); + let item = unsafe { GroupItem::from_resident(crate::node::RecordBatch::new(bytes.as_ptr(), 1, &layout)) }; + item.copy_out().lanes(); + } + #[test] fn record_values_are_one_word() { assert_eq!(size_of::(), 8); diff --git a/node-graph/libraries/graphic-types/src/artboard.rs b/node-graph/libraries/graphic-types/src/artboard.rs index fa3f1fc55a..ac20f25709 100644 --- a/node-graph/libraries/graphic-types/src/artboard.rs +++ b/node-graph/libraries/graphic-types/src/artboard.rs @@ -42,28 +42,47 @@ impl Artboard { } } -/// The deep clone-out for `Artboard` elements: as for `Graphic`, a plain +/// The deep copy-out for `Artboard` elements: as for `Graphic`, a plain /// clone of group content would carry frame pointers into the evaluation's -/// arena, so memo and capture seams copy out the legacy-converted form. +/// arena, so memo and capture seams copy out the owned-group form. /// /// # Safety /// `ptr` must point at a live parked `Artboard` element field. unsafe fn deep_clone_artboard(ptr: *const u8) -> Box { let artboard = unsafe { core_types::record::borrow_element::(core_types::record::Rec::new(ptr)) }; - Box::new(artboard.with_legacy_groups()) + let mut content = artboard.0.clone(); + for element in content.iter_element_values_mut() { + *element = crate::graphic::map_groups_to_owned(element); + } + Box::new(Artboard(content)) +} + +/// The deep re-park for `Artboard` elements: owned content groups replay into +/// the serving arena before the artboard parks. +/// +/// # Safety +/// `value` must hold an `Artboard` and `dst` must be a live `Artboard` +/// element field. +unsafe fn deep_repark_artboard(value: &(dyn std::any::Any + Send + Sync), dst: *mut u8, arena: &core_types::arena::Arena) -> Option<()> { + let artboard = value.downcast_ref::().expect("an element replays at its own type"); + let mut content = artboard.0.clone(); + for element in content.iter_element_values_mut() { + *element = crate::graphic::map_groups_to_resident(element, arena)?; + } + unsafe { core_types::record::write_element(dst, Artboard(content), arena) } } const _: () = { #[cfg(not(target_family = "wasm"))] #[core_types::ctor::ctor] fn register() { - core_types::record::register_deep_element_clone::(deep_clone_artboard); + core_types::record::register_deep_element_clone::(deep_clone_artboard, deep_repark_artboard); } #[cfg(target_family = "wasm")] #[unsafe(export_name = "__node_registry_deep_element_artboard")] extern "C" fn register() { - core_types::record::register_deep_element_clone::(deep_clone_artboard); + core_types::record::register_deep_element_clone::(deep_clone_artboard, deep_repark_artboard); } }; diff --git a/node-graph/libraries/graphic-types/src/boundary.rs b/node-graph/libraries/graphic-types/src/boundary.rs index 35592c827c..8c695e28ac 100644 --- a/node-graph/libraries/graphic-types/src/boundary.rs +++ b/node-graph/libraries/graphic-types/src/boundary.rs @@ -53,6 +53,25 @@ where /// legacy vocabulary or a capture whose arena generation has passed. pub fn capture_to_legacy(capture: &RecordCapture, arena: &Arena) -> Option> { if capture.layout().depth == 0 { + // The group-carrying types legacy-convert while the capture is still + // resident; the deep clone-out would hand back the unreadable owned + // form. + if capture.lanes() > 0 { + let element = &capture.layout().element; + if element.type_id == std::any::TypeId::of::() { + let batch = capture.batch(arena)?; + // SAFETY: the layout records the element type, and a parked + // element stores its reference at offset 0. + let graphic = unsafe { core_types::record::borrow_element::(batch.get(0).rec()) }; + return Some(Box::new(crate::graphic::map_groups_to_legacy(graphic))); + } + if element.type_id == std::any::TypeId::of::() { + let batch = capture.batch(arena)?; + // SAFETY: as for the graphic arm. + let artboard = unsafe { core_types::record::borrow_element::(batch.get(0).rec()) }; + return Some(Box::new(artboard.with_legacy_groups())); + } + } return capture.materialize_element(arena); } let batch = capture.batch(arena)?; diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 5c1864b39b..f78731fde5 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -854,29 +854,73 @@ fn push_lane_paint_into_interiors(list: &mut List) { } } -/// The deep clone-out for `Graphic` elements: a plain clone of a group +/// The graphic with every `Group` deep-copied to its owned form, which +/// survives the arena generation but cannot be read until +/// [`map_groups_to_resident`] re-parks it into a serving arena. +pub fn map_groups_to_owned(graphic: &Graphic) -> Graphic { + match graphic { + Graphic::Group(group) => Graphic::Group(group.copy_out()), + Graphic::Graphic(children) => { + let mut children = children.clone(); + for child in children.iter_element_values_mut() { + *child = map_groups_to_owned(child); + } + Graphic::Graphic(children) + } + other => other.clone(), + } +} + +/// The graphic with every owned `Group` re-parked into `arena`; `None` +/// reports arena exhaustion. +pub fn map_groups_to_resident(graphic: &Graphic, arena: &core_types::arena::Arena) -> Option { + match graphic { + Graphic::Group(group) => group.replay(arena).map(Graphic::Group), + Graphic::Graphic(children) => { + let mut children = children.clone(); + for child in children.iter_element_values_mut() { + *child = map_groups_to_resident(child, arena)?; + } + Some(Graphic::Graphic(children)) + } + other => Some(other.clone()), + } +} + +/// The deep copy-out for `Graphic` elements: a plain clone of a group /// interior would carry frame pointers into the evaluation's arena, so memo -/// and capture seams copy out the legacy-converted form, which owns all of -/// its content. The generic re-park replays it as an ordinary `Graphic`. +/// and capture seams copy out the owned-group form. /// /// # Safety /// `ptr` must point at a live parked `Graphic` element field. unsafe fn deep_clone_graphic(ptr: *const u8) -> Box { let graphic = unsafe { core_types::record::borrow_element::(core_types::record::Rec::new(ptr)) }; - Box::new(map_groups_to_legacy(graphic)) + Box::new(map_groups_to_owned(graphic)) +} + +/// The deep re-park for `Graphic` elements: owned groups replay into the +/// serving arena before the graphic parks. +/// +/// # Safety +/// `value` must hold a `Graphic` and `dst` must be a live `Graphic` element +/// field. +unsafe fn deep_repark_graphic(value: &(dyn std::any::Any + Send + Sync), dst: *mut u8, arena: &core_types::arena::Arena) -> Option<()> { + let graphic = value.downcast_ref::().expect("an element replays at its own type"); + let resident = map_groups_to_resident(graphic, arena)?; + unsafe { core_types::record::write_element(dst, resident, arena) } } const _: () = { #[cfg(not(target_family = "wasm"))] #[core_types::ctor::ctor] fn register() { - core_types::record::register_deep_element_clone::(deep_clone_graphic); + core_types::record::register_deep_element_clone::(deep_clone_graphic, deep_repark_graphic); } #[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); + core_types::record::register_deep_element_clone::(deep_clone_graphic, deep_repark_graphic); } }; @@ -1201,6 +1245,77 @@ mod run_tests { assert_eq!(paint_graphics::(&legacy, 0), paint_graphics::(&run, 0)); } + #[test] + fn an_owned_group_replays_content_equal_after_the_source_dies() { + let paint = List::new_from_element(Graphic::Color(List::new_from_element(Color::BLACK))); + let vector = unit_square_at(DVec2::ZERO); + + 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 { + let base = bytes.as_mut_ptr(); + base.cast::<&Vector>().write(&vector); + base.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)) }; + let group = core_types::record::Group { + row: None, + content: core_types::record::GroupContent::Run(item), + }; + let expected = group_to_legacy_list(&group); + (map_groups_to_owned(&Graphic::Group(group)), expected) + }; + bytes.fill(u8::MAX); + + let arena = core_types::arena::Arena::new(1 << 16).unwrap(); + let resident = map_groups_to_resident(&owned, &arena).expect("the arena holds the replay"); + let Graphic::Group(group) = &resident else { panic!("the replay keeps the group form") }; + assert_eq!(group_to_legacy_list(group), expected); + } + + #[test] + fn an_owned_group_replays_nested_groups_through_the_element_glue() { + let 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()]; + // 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)) }; + let nested = Graphic::Group(core_types::record::Group { + row: None, + content: core_types::record::GroupContent::Run(inner_item), + }); + + let outer_layout = Layout::default().with_writes(0, element_write_hashed::(), &[]); + let mut outer_bytes = vec![0u8; outer_layout.lane_stride()]; + // SAFETY: `outer_bytes` is one lane of `outer_layout`; a parked element + // stores its reference. + unsafe { outer_bytes.as_mut_ptr().cast::<&Graphic>().write(&nested) }; + let (owned, expected) = { + // SAFETY: `outer_bytes` holds one lane of `outer_layout` at its stride. + let outer_item = unsafe { GroupItem::from_resident(RecordBatch::new(outer_bytes.as_ptr(), 1, &outer_layout)) }; + let group = core_types::record::Group { + row: None, + content: core_types::record::GroupContent::Run(outer_item), + }; + let expected = group_to_legacy_list(&group); + (map_groups_to_owned(&Graphic::Group(group)), expected) + }; + outer_bytes.fill(u8::MAX); + inner_bytes.fill(u8::MAX); + + let arena = core_types::arena::Arena::new(1 << 16).unwrap(); + let resident = map_groups_to_resident(&owned, &arena).expect("the arena holds the replay"); + let Graphic::Group(group) = &resident else { panic!("the replay keeps the group form") }; + assert_eq!(group_to_legacy_list(group), 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.))]; diff --git a/node-graph/nodes/gcore/src/memo.rs b/node-graph/nodes/gcore/src/memo.rs index 894aa0bef3..1109277c1e 100644 --- a/node-graph/nodes/gcore/src/memo.rs +++ b/node-graph/nodes/gcore/src/memo.rs @@ -359,7 +359,11 @@ mod tests { let value = unsafe { core_types::record::borrow_element::(core_types::record::Rec::new(ptr)) }; Box::new(Payload(value.0.clone(), value.1 + 1)) } - core_types::record::register_deep_element_clone::(deep); + unsafe fn deep_repark(value: &(dyn std::any::Any + Send + Sync), dst: *mut u8, arena: &Arena) -> Option<()> { + let value = value.downcast_ref::().expect("an element replays at its own type"); + unsafe { core_types::record::write_element(dst, Payload(value.0.clone(), value.1 + 1), arena) } + } + core_types::record::register_deep_element_clone::(deep, deep_repark); let arena = Arena::new(4096).unwrap(); let generations = []; @@ -371,7 +375,7 @@ mod tests { let memoized = core_types::record::RecordExtract::::new(memoized, &layout); assert_eq!(memoized.eval(&ctx), GPoll::Final(Payload("deep".to_string(), 0)), "the miss serves the live value"); - assert_eq!(memoized.eval(&ctx), GPoll::Final(Payload("deep".to_string(), 1)), "the hit replays the deep copy"); + assert_eq!(memoized.eval(&ctx), GPoll::Final(Payload("deep".to_string(), 2)), "the hit replays through both halves of the deep glue"); } #[test] diff --git a/node-graph/nodes/graphic/src/record.rs b/node-graph/nodes/graphic/src/record.rs index 0806ac7d4d..ac95a2dba6 100644 --- a/node-graph/nodes/graphic/src/record.rs +++ b/node-graph/nodes/graphic/src/record.rs @@ -743,7 +743,7 @@ mod tests { } #[test] - fn a_group_element_deep_copies_to_its_legacy_form() { + fn a_group_element_deep_copies_to_its_owned_form_and_replays() { let arena = Arena::new(1 << 16).unwrap(); let generations = []; let scope = scope_fixture(&generations, &arena); @@ -763,12 +763,25 @@ mod tests { panic!("expected a final record"); }; let copy = unsafe { (out.element.clone_out)(out.rec(&value).ptr()) }; - let Graphic::Graphic(list) = *copy.downcast::().expect("the deep copy replays at the element's own type") else { - panic!("expected the legacy-converted form"); + + let replay_arena = Arena::new(1 << 16).unwrap(); + let mut slot = [0u8; 8]; + unsafe { (out.element.repark)(&*copy, slot.as_mut_ptr(), &replay_arena) }.expect("the arena holds the replay"); + // SAFETY: the re-park wrote a parked `Graphic` element into `slot`. + let Graphic::Group(group) = (unsafe { record::borrow_element::(record::Rec::new(slot.as_ptr())) }) else { + panic!("the replay restores the group element"); }; - assert_eq!(list.len(), 2); - assert_eq!(text_of(list.element(0).unwrap()), "a"); - assert_eq!(text_of(list.element(1).unwrap()), "b"); + let record::GroupContent::Run(item) = &group.content else { + panic!("expected a single run"); + }; + assert_eq!(item.len(), 2); + let lanes = item.typed_lanes::().expect("the run holds the adopted graphic lanes"); + let offset = item.layout().offset_of(ATTR_TRANSFORM, 0).unwrap(); + for (lane, (label, x)) in [("a", 1.), ("b", 2.)].into_iter().enumerate() { + assert_eq!(text_of(lanes.element_ref(lane)), label, "lane {lane}"); + let transform: DAffine2 = unsafe { item.lanes().get(lane).rec().read(offset) }; + assert_eq!(transform.translation.x, x, "lane {lane}"); + } } #[test]