From b685ca77f2e402cef9b25178a1524e00aef7893e Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Wed, 26 Aug 2026 15:17:06 +0000 Subject: [PATCH] Serve record runs through the lane source surface --- node-graph/libraries/core-types/src/record.rs | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/node-graph/libraries/core-types/src/record.rs b/node-graph/libraries/core-types/src/record.rs index 067a2558fd..2dfcde7f82 100644 --- a/node-graph/libraries/core-types/src/record.rs +++ b/node-graph/libraries/core-types/src/record.rs @@ -1866,6 +1866,62 @@ impl GroupItem { } } +/// A run read at its element type. Record fields hold each marker's value +/// verbatim, so lane reads are plain typed reads at a hoisted offset. +pub struct RunView<'a, T> { + item: &'a GroupItem, + lanes: crate::node::List<'a, T>, +} + +impl<'a, T: 'static> RunView<'a, T> { + /// `None` where the run holds another element type. + pub fn new(item: &'a GroupItem) -> Option { + item.typed_lanes::().map(|lanes| Self { item, lanes }) + } +} + +/// A marker's field on a run, its offset resolved once. +pub struct RunColumn<'a, A: crate::attribute::Attribute> { + item: &'a GroupItem, + offset: Option, + marker: std::marker::PhantomData, +} + +impl<'a, A: crate::attribute::Attribute> crate::lane::LaneColumn<'a, A> for RunColumn<'a, A> { + fn get(&self, lane: usize) -> A::Value<'a> { + match self.offset { + // SAFETY: the offset comes from the item's own layout, whose field at + // this name holds this marker's value type by census registration. + Some(offset) => unsafe { self.item.lanes().get(lane).rec().ptr().add(offset).cast::>().read() }, + None => A::default(), + } + } +} + +impl<'a, T: 'static> crate::lane::LaneSource for RunView<'a, T> { + type Element = T; + type Column<'b, A: crate::attribute::Attribute> + = RunColumn<'b, A> + where + Self: 'b; + + fn lane_count(&self) -> usize { + self.lanes.len() + } + + fn element(&self, lane: usize) -> Option<&T> { + (lane < self.lanes.len()).then(|| self.lanes.element_ref(lane)) + } + + fn column(&self) -> RunColumn<'_, A> { + RunColumn { + item: self.item, + offset: self.item.layout().offset_of(A::NAME, 0), + marker: std::marker::PhantomData, + } + } +} + /// The records a group stores: a single homogeneous run, or a list of /// segments. #[derive(Clone, Debug)] @@ -2016,6 +2072,58 @@ mod tests { sized_field(name, 8, 8) } + #[test] + fn a_run_and_a_legacy_list_serve_the_same_values() { + use crate::attribute::{Attribute, Opacity, Transform}; + use crate::lane::LaneSource; + use glam::DAffine2; + + let layout = Layout::default().with_writes(0, element_write::(), &[FieldWrite::of::(0), FieldWrite::of::(0)]); + let stride = layout.lane_stride(); + let transform = DAffine2::from_translation((5., 6.).into()); + let mut bytes = vec![0u8; stride * 2]; + for lane in 0..2 { + // SAFETY: `bytes` is `stride` per lane, and the offsets come from `layout`. + unsafe { + let base = bytes.as_mut_ptr().add(lane * stride); + base.cast::().write(lane as f64); + base.add(layout.offset_of(Transform::NAME, 0).unwrap()).cast::().write(transform); + base.add(layout.offset_of(Opacity::NAME, 0).unwrap()).cast::().write(0.25); + } + } + // SAFETY: `bytes` holds two lanes of `layout` at its stride. + let item = unsafe { GroupItem::from_resident(crate::node::RecordBatch::new(bytes.as_ptr(), 2, &layout)) }; + let run = RunView::::new(&item).expect("the run holds f64 elements"); + + let mut list = crate::list::List::new_from_element(0f64); + list.push(crate::list::Item::new_from_element(1f64)); + for lane in 0..2 { + list.set_attribute(Transform::NAME, lane, transform); + list.set_attribute(Opacity::NAME, lane, 0.25); + } + + assert_eq!(run.lane_count(), list.lane_count()); + for lane in 0..2 { + assert_eq!(run.attr::(lane), list.attr::(lane)); + assert_eq!(run.attr::(lane), list.attr::(lane)); + assert_eq!(run.element(lane), list.element(lane)); + } + } + + #[test] + fn a_run_missing_a_column_serves_the_census_default() { + use crate::attribute::Opacity; + use crate::lane::LaneSource; + + let layout = Layout::default().with_writes(0, element_write::(), &[]); + let bytes = vec![0u8; layout.lane_stride()]; + // SAFETY: `bytes` holds one lane of `layout` at its stride. + let item = unsafe { GroupItem::from_resident(crate::node::RecordBatch::new(bytes.as_ptr(), 1, &layout)) }; + let run = RunView::::new(&item).expect("the run holds f64 elements"); + + assert_eq!(run.attr::(0), 1.); + } + #[test] fn canonical_order_and_offsets() { let layout = Layout::default().with_writes(0, element_write::(), &[sized_field("tint", 4, 4), f64_field("opacity"), sized_field("flag", 1, 1)]);