From 6f809fda73fb856ca2bb4e162fce28719d2d691b Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Thu, 27 Aug 2026 19:28:41 +0000 Subject: [PATCH] Add the run builder and census-driven list adoption --- .../libraries/core-types/src/attribute.rs | 33 +++- node-graph/libraries/core-types/src/list.rs | 5 + node-graph/libraries/core-types/src/record.rs | 181 ++++++++++++++++++ 3 files changed, 218 insertions(+), 1 deletion(-) diff --git a/node-graph/libraries/core-types/src/attribute.rs b/node-graph/libraries/core-types/src/attribute.rs index e89d28c10a..2076c400ff 100644 --- a/node-graph/libraries/core-types/src/attribute.rs +++ b/node-graph/libraries/core-types/src/attribute.rs @@ -105,6 +105,12 @@ pub struct AttributeInfo { pub align: usize, /// Writes the declared default's bytes into a `size`-long slice. pub write_default_bytes: fn(&mut [u8]), + /// The marker's field form at the given level, for layouts built at runtime. + pub field_write_at: fn(u8) -> crate::record::FieldWrite, + /// Writes a legacy stored value into a field of this marker, parking + /// droppable payloads. A wrong-typed stored value leaves the field + /// untouched; `None` reports arena exhaustion. + pub write_stored: unsafe fn(&dyn AnyAttributeValue, *mut u8, &crate::arena::Arena) -> Option<()>, } fn write_default_bytes(out: &mut [u8]) { @@ -113,6 +119,29 @@ fn write_default_bytes(out: &mut [u8]) { unsafe { std::ptr::copy_nonoverlapping((&raw const value).cast::(), out.as_mut_ptr(), size_of::>()) }; } +fn field_write_at(level: u8) -> crate::record::FieldWrite +where + A::Value<'static>: graphene_hash::CacheHash + PartialEq, +{ + crate::record::FieldWrite::of::(level) +} + +unsafe fn write_stored(stored: &dyn AnyAttributeValue, dst: *mut u8, arena: &crate::arena::Arena) -> Option<()> { + if A::from_stored(stored.as_any()).is_none() { + // A wrong-typed stored value reads as absent, so the field keeps its default. + return Some(()); + } + match A::REPARK { + Some(repark) => unsafe { repark(stored, dst, arena) }, + None => { + let value = A::from_stored(stored.as_any()).expect("checked above"); + // SAFETY: a marker without re-park glue stores a plain value, so the bytes carry no borrowed data. + unsafe { dst.cast::>().write(value) }; + Some(()) + } + } +} + /// All declared attribute names, keyed by name. pub static ATTRIBUTE_REGISTRY: LazyLock>> = LazyLock::new(|| Mutex::new(HashMap::new())); @@ -122,7 +151,7 @@ pub static ATTRIBUTE_REGISTRY: LazyLock() where - A::Value<'static>: AnyAttributeValue, + A::Value<'static>: AnyAttributeValue + graphene_hash::CacheHash + PartialEq, { let info = AttributeInfo { name: A::NAME, @@ -132,6 +161,8 @@ where size: size_of::>(), align: align_of::>(), write_default_bytes: write_default_bytes::, + field_write_at: field_write_at::, + write_stored: write_stored::, }; let conflict = match ATTRIBUTE_REGISTRY.lock().unwrap().entry(A::NAME) { Entry::Vacant(vacant) => { diff --git a/node-graph/libraries/core-types/src/list.rs b/node-graph/libraries/core-types/src/list.rs index 788273fab7..edd34b999d 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -602,6 +602,11 @@ impl ItemAttributeValues { self.0.iter().map(|(key, _)| key.as_str()) } + /// Returns an iterator over the stored (key, value) pairs, in insertion order. + pub fn iter(&self) -> impl Iterator { + self.0.iter().map(|(key, value)| (key.as_str(), &**value)) + } + /// Returns a debug-formatted string representation of the attribute value for the given key, if it exists. /// The `overrides` function can provide custom formatting for specific type. pub fn display_value(&self, key: &str, overrides: fn(&dyn std::any::Any) -> Option) -> Option { diff --git a/node-graph/libraries/core-types/src/record.rs b/node-graph/libraries/core-types/src/record.rs index f89318a67b..5bb84ecf77 100644 --- a/node-graph/libraries/core-types/src/record.rs +++ b/node-graph/libraries/core-types/src/record.rs @@ -1739,6 +1739,100 @@ where } } +/// Builds a resident run lane by lane: fresh frames in the arena at a layout +/// derived from the element glue and field writes, elements pushed in order +/// and attributes written onto pushed lanes. The finished item's frames are +/// arena-resident, valid for the evaluation like every parked payload. +pub struct RunBuilder<'e> { + arena: &'e crate::arena::Arena, + layout: Layout, + frames: *mut u8, + len: usize, + pushed: usize, +} + +impl<'e> RunBuilder<'e> { + /// Fresh frames for `len` lanes of a layout over `element` and `fields`. + /// `None` reports arena exhaustion. + pub fn new(arena: &'e crate::arena::Arena, element: ElementWrite, fields: &[FieldWrite], len: usize) -> Option { + let layout = Layout::default().with_writes(0, element, fields); + assert!(!layout.element.parked || layout.element.content_hash.is_some(), "a parked element adopts only with content glue"); + for field in &layout.fields { + assert!(field.repark.is_none() || field.content_hash.is_some(), "a parked field adopts only with content glue"); + } + let stride = layout.lane_stride(); + let scratch = arena.alloc_scratch::((len * stride).div_ceil(8))?; + Some(Self { + arena, + layout, + frames: scratch.as_mut_ptr().cast(), + len, + pushed: 0, + }) + } + + /// 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"); + assert!(self.pushed < self.len, "the builder holds exactly its declared lane count"); + let lane = self.pushed; + let stride = self.layout.lane_stride(); + // SAFETY: the frames hold `len` lanes at the layout's stride, and + // `lane` is below `len`; the element slot and each field's region are + // disjoint parts of this lane. + let base = unsafe { self.frames.add(lane * stride) }; + unsafe { write_element(base, element, self.arena) }?; + for field in &self.layout.fields { + // SAFETY: as above; the field region is within the lane. + let bytes = unsafe { std::slice::from_raw_parts_mut(base.add(field.offset), field.size) }; + bytes.fill(0); + if let Some(info) = crate::attribute::info(field.name) + && info.size == field.size + { + (info.write_default_bytes)(bytes); + } + } + self.pushed = lane + 1; + Some(lane) + } + + /// Writes the marker's value on an already pushed lane. The layout must + /// carry the marker among its field writes. + pub fn attr(&mut self, lane: usize, value: A::Value<'e>) { + assert!(lane < self.pushed, "attributes write onto pushed lanes"); + let offset = self.layout.offset_of(A::NAME, 0).expect("the layout carries the written marker"); + let field = self.layout.fields.iter().find(|field| field.name == A::NAME && field.level == 0).expect("resolved above"); + assert_eq!(field.size, size_of::>(), "the field was declared at the marker's value type"); + // SAFETY: the offset comes from the builder's own layout and the size + // matches the marker's value type. + unsafe { self.frames.add(lane * self.layout.lane_stride() + offset).cast::>().write(value) }; + } + + /// Writes a legacy stored value on an already pushed lane through the + /// census glue, parking droppable payloads. A marker outside the layout's + /// fields is dropped; a wrong-typed stored value leaves the field's + /// default. `None` reports arena exhaustion. + pub fn attr_stored(&mut self, lane: usize, info: &crate::attribute::AttributeInfo, value: &dyn crate::list::AnyAttributeValue) -> Option<()> { + assert!(lane < self.pushed, "attributes write onto pushed lanes"); + let Some(offset) = self.layout.offset_of(info.name, 0) else { return Some(()) }; + // SAFETY: the offset comes from the builder's own layout, and the + // census writer verifies the stored type before touching the field. + unsafe { (info.write_stored)(value, self.frames.add(lane * self.layout.lane_stride() + offset), self.arena) } + } + + /// The finished run. Panics unless every lane was pushed, since an + /// unwritten parked element slot must never become readable. + pub fn finish(self) -> GroupItem { + assert_eq!(self.pushed, self.len, "every lane pushes before the run finishes"); + GroupItem { + layout: self.layout, + storage: ItemStorage::Resident(self.frames.cast_const()), + len: self.len, + } + } +} + /// `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. An owned item @@ -1807,6 +1901,26 @@ impl GroupItem { }) } + /// A resident run built from a legacy list: one lane per item, the element + /// moved in and every census-declared attribute written through its stored + /// form. An undeclared key has no field form and is dropped; a wrong-typed + /// stored value leaves its field's default, matching the legacy read. + /// `None` reports arena exhaustion. + pub fn from_list(list: crate::list::List, arena: &crate::arena::Arena) -> Option { + let declared: Vec = list.attribute_keys().filter_map(crate::attribute::info).collect(); + let writes: Vec = declared.iter().map(|info| (info.field_write_at)(0)).collect(); + let mut builder = RunBuilder::new(arena, element_write_hashed::(), &writes, list.len())?; + for item in list.into_iter() { + let (element, attributes) = item.into_parts(); + let lane = builder.push(element)?; + for (key, value) in attributes.iter() { + let Some(info) = declared.iter().find(|info| info.name == key) else { continue }; + builder.attr_stored(lane, info, value)?; + } + } + Some(builder.finish()) + } + /// The resident frame base. An owned item has none until it replays. fn frames(&self) -> *const u8 { match &self.storage { @@ -2416,6 +2530,73 @@ mod tests { } } + #[test] + fn a_run_builds_from_a_legacy_list_and_serves_its_rows() { + let mut list = crate::list::List::new_from_element(String::from("row 0")); + list.push(crate::list::Item::new_from_element(String::from("row 1"))); + let transform = glam::DAffine2::from_translation(glam::DVec2::new(3., 4.)); + list.set_attribute(crate::ATTR_TRANSFORM, 0, transform); + list.set_attribute("name", 0, String::from("first")); + list.set_attribute(crate::ATTR_EDITOR_LAYER_PATH, 1, vec![crate::uuid::NodeId(7), crate::uuid::NodeId(9)]); + list.set_attribute("max_width", 1, Some(12.5f64)); + + let arena = crate::arena::Arena::new(1 << 16).unwrap(); + let item = GroupItem::from_list(list, &arena).unwrap(); + assert_eq!(item.len(), 2); + let layout = item.layout().clone(); + let lanes = item.lanes(); + + let rec = lanes.get(0).rec(); + assert_eq!(unsafe { read_element::(rec) }, "row 0"); + assert_eq!(unsafe { rec.read::(layout.offset_of(crate::ATTR_TRANSFORM, 0).unwrap()) }, transform); + assert_eq!(unsafe { rec.read::<&str>(layout.offset_of("name", 0).unwrap()) }, "first"); + assert!(unsafe { rec.read::<&[crate::uuid::NodeId]>(layout.offset_of(crate::ATTR_EDITOR_LAYER_PATH, 0).unwrap()) }.is_empty()); + + let rec = lanes.get(1).rec(); + assert_eq!(unsafe { read_element::(rec) }, "row 1"); + // A lane without the value reads the census default, not garbage. + assert_eq!(unsafe { rec.read::(layout.offset_of(crate::ATTR_TRANSFORM, 0).unwrap()) }, glam::DAffine2::IDENTITY); + assert_eq!(unsafe { rec.read::<&str>(layout.offset_of("name", 0).unwrap()) }, ""); + assert_eq!( + unsafe { rec.read::<&[crate::uuid::NodeId]>(layout.offset_of(crate::ATTR_EDITOR_LAYER_PATH, 0).unwrap()) }, + &[crate::uuid::NodeId(7), crate::uuid::NodeId(9)] + ); + assert_eq!(unsafe { rec.read::>(layout.offset_of("max_width", 0).unwrap()) }, Some(12.5)); + } + + #[test] + fn a_built_run_replays_after_the_source_dies() { + let owned = { + let arena = crate::arena::Arena::new(1 << 16).unwrap(); + let mut list = crate::list::List::new_from_element(String::from("element")); + list.set_attribute("name", 0, String::from("label")); + list.set_attribute(crate::ATTR_EDITOR_LAYER_PATH, 0, vec![crate::uuid::NodeId(3)]); + GroupItem::from_list(list, &arena).unwrap().copy_out() + }; + + let arena = crate::arena::Arena::new(1 << 16).unwrap(); + let replayed = owned.replay(&arena).unwrap(); + let layout = replayed.layout().clone(); + let rec = replayed.lanes().get(0).rec(); + assert_eq!(unsafe { read_element::(rec) }, "element"); + assert_eq!(unsafe { rec.read::<&str>(layout.offset_of("name", 0).unwrap()) }, "label"); + assert_eq!(unsafe { rec.read::<&[crate::uuid::NodeId]>(layout.offset_of(crate::ATTR_EDITOR_LAYER_PATH, 0).unwrap()) }, &[crate::uuid::NodeId(3)]); + } + + #[test] + fn a_wrong_typed_or_undeclared_column_leaves_the_default() { + let mut list = crate::list::List::new_from_element(String::from("element")); + list.set_attribute(crate::ATTR_OPACITY, 0, String::from("not an f64")); + list.set_attribute("never_declared", 0, 5u32); + + let arena = crate::arena::Arena::new(1 << 16).unwrap(); + let item = GroupItem::from_list(list, &arena).unwrap(); + let layout = item.layout().clone(); + assert!(layout.offset_of("never_declared", 0).is_none(), "an undeclared key has no field form"); + let rec = item.lanes().get(0).rec(); + assert_eq!(unsafe { rec.read::(layout.offset_of(crate::ATTR_OPACITY, 0).unwrap()) }, 1., "the wrong-typed value reads as absent"); + } + #[test] #[should_panic(expected = "an owned item replays")] fn an_owned_item_refuses_reads() {