diff --git a/node-graph/libraries/core-types/src/attribute.rs b/node-graph/libraries/core-types/src/attribute.rs index 2076c400ff..c8831fc92f 100644 --- a/node-graph/libraries/core-types/src/attribute.rs +++ b/node-graph/libraries/core-types/src/attribute.rs @@ -26,7 +26,9 @@ pub trait Attribute: 'static { const NAME: &'static str; /// The value type every read and write of this name shares. The lifetime /// is the evaluation the value flows in; non-reference values ignore it. - type Value<'e>: Copy + Default + std::fmt::Debug; + /// The value outlives that evaluation, so its `'static` instantiation is + /// the one the census registers and layouts stamp their type id from. + type Value<'e>: Copy + Default + std::fmt::Debug + 'e; /// The name-specific default, filled where an item lacks the attribute. /// Producing a value for any `'e` from no inputs, reference defaults can /// only point at `'static` data, which is what lets the census fill them diff --git a/node-graph/libraries/core-types/src/node.rs b/node-graph/libraries/core-types/src/node.rs index 947840513c..5503645b2b 100644 --- a/node-graph/libraries/core-types/src/node.rs +++ b/node-graph/libraries/core-types/src/node.rs @@ -174,13 +174,27 @@ impl<'a> RecordLane<'a> { unsafe { self.rec.element::() } } + /// Attribute `A` through a token, `None` where the token was minted against + /// another layout than this lane's. + pub fn try_attr_at(&self, field: crate::record::FieldOffset) -> Option> { + let offset = field.resolve(self.layout)?; + // SAFETY: resolving against this lane's own layout pins the offset and + // the field's value type, and the batch's contract makes the lane a live + // record of that layout. + Some(unsafe { self.rec.read::>(offset) }) + } + + /// Attribute `A` through a token, or its census default where the token is + /// absent or names another layout. + pub fn attr_at(&self, field: Option>) -> A::Value<'a> { + field.and_then(|field| self.try_attr_at(field)).unwrap_or_else(A::default) + } + /// Attribute `A` at the record's top level, or its census default when the - /// layout does not carry it. + /// layout does not carry it. Mints a token per call; lane loops hoist the + /// mint instead. pub fn attr(&self) -> A::Value<'a> { - match self.layout.offset_of(A::NAME, 0) { - Some(offset) => unsafe { self.rec.read::>(offset) }, - None => A::default(), - } + self.attr_at(crate::record::FieldOffset::::of(self.layout, 0)) } } diff --git a/node-graph/libraries/core-types/src/record.rs b/node-graph/libraries/core-types/src/record.rs index 624ed032eb..d5a7744ae5 100644 --- a/node-graph/libraries/core-types/src/record.rs +++ b/node-graph/libraries/core-types/src/record.rs @@ -263,6 +263,61 @@ impl Layout { } } +/// A field handle minted once against a layout: the marker's (name, level) +/// resolved to a field index and offset, with the marker-to-type proof taken +/// there. Resolving it against a layout re-checks that one index instead of +/// scanning names, so a token paired with any other layout resolves to +/// nothing rather than to the wrong bytes. +pub struct FieldOffset { + index: usize, + offset: usize, + level: u8, + marker: std::marker::PhantomData A>, +} + +impl Clone for FieldOffset { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for FieldOffset {} + +impl std::fmt::Debug for FieldOffset { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FieldOffset").field("name", &A::NAME).field("offset", &self.offset).field("level", &self.level).finish() + } +} + +impl FieldOffset { + /// The marker's field at `level`, `None` where the layout does not carry + /// it. Panics where the layout declares the name at another value type, + /// which the census forbids. + pub fn of(layout: &Layout, level: u8) -> Option { + let (index, field) = layout.fields.iter().enumerate().find(|(_, field)| field.name == A::NAME && field.level == level)?; + assert_eq!(field.type_id, std::any::TypeId::of::>(), "attribute `{}` is declared at another value type", A::NAME); + Some(Self { + index, + offset: field.offset, + level, + marker: std::marker::PhantomData, + }) + } + + pub fn offset(self) -> usize { + self.offset + } + + /// The offset this token names in `layout`, `None` unless `layout` is the + /// one it was minted against. The field's value type is re-checked, so a + /// resolved offset carries the marker-to-type proof into `layout`. + pub fn resolve(self, layout: &Layout) -> Option { + let field = layout.fields.get(self.index)?; + let same = field.offset == self.offset && field.level == self.level && field.name == A::NAME && field.type_id == std::any::TypeId::of::>(); + same.then_some(self.offset) + } +} + /// The stand-in fed to a kernel's unbounded `element: T` parameter. The type /// system forces the kernel to route it to the element position of its return /// tuple, so the passthrough is explicit in the signature while the lowering @@ -2411,8 +2466,7 @@ impl<'a, T: dyn_any::StaticTypeSized> RunView<'a, T> { /// A marker's field on a run, its offset resolved once. pub struct RunColumn<'a, A: crate::attribute::Attribute> { item: &'a GroupItem<'a>, - offset: Option, - marker: std::marker::PhantomData, + field: Option>, } impl<'a, A: crate::attribute::Attribute> RunColumn<'a, A> { @@ -2420,17 +2474,14 @@ impl<'a, A: crate::attribute::Attribute> RunColumn<'a, A> { pub fn of(item: &'a GroupItem<'a>) -> Self { Self { item, - offset: item.layout().offset_of(A::NAME, 0), - marker: std::marker::PhantomData, + field: FieldOffset::of(item.layout(), 0), } } } impl<'a, A: crate::attribute::Attribute> crate::lane::LaneColumn<'a, A> for RunColumn<'a, A> { fn try_get(&self, lane: usize) -> Option> { - // SAFETY: the offset comes from the item's own layout, whose field at - // this name holds this marker's value type by census registration. - self.offset.map(|offset| unsafe { self.item.lanes().get(lane).rec().ptr().add(offset).cast::>().read() }) + self.item.lanes().get(lane).try_attr_at(self.field?) } } @@ -2680,6 +2731,19 @@ mod tests { layout.with_writes(0, element_write::(), &[conflicting]); } + #[test] + fn a_token_resolves_only_in_the_layout_it_was_minted_against() { + use crate::attribute::{Attribute, Opacity, Transform}; + + let layout = Layout::default().with_writes(0, element_write::(), &[FieldWrite::of::(0)]); + let token = FieldOffset::::of(&layout, 0).expect("the layout carries the marker"); + assert_eq!(token.resolve(&layout), layout.offset_of(Opacity::NAME, 0)); + + let shifted = Layout::default().with_writes(0, element_write::(), &[FieldWrite::of::(0), FieldWrite::of::(0)]); + assert_eq!(FieldOffset::::of(&shifted, 0).and_then(|token| token.resolve(&layout)), None); + assert!(FieldOffset::::of(&Layout::default(), 0).is_none()); + } + #[test] fn union_is_order_independent() { let a = Layout::default().with_writes(0, element_write::(), &[f64_field("opacity")]); diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index 9e6a5e4f00..8aaa8e9f37 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -1,10 +1,11 @@ use crate::markers::{ATTR_FILL, ATTR_STROKE, Fill, Stroke}; -use core_types::attribute::{Attribute, EditorLayerPath, Opacity, OpacityFill, Transform}; +use core_types::attribute::{Attribute, ClippingMask, EditorLayerPath, Opacity, OpacityFill, Transform}; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::graphene_hash::CacheHash; use core_types::lane::{LaneColumn, LaneSource}; use core_types::list::{AttributeValueDyn, Item, ItemAttributeValues, List}; use core_types::ops::{FromAnchorPosition, ListConvert}; +use core_types::record::FieldOffset; use core_types::render_complexity::RenderComplexity; use core_types::uuid::NodeId; use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color}; @@ -654,29 +655,28 @@ impl<'e> Graphic<'e> { } } -/// One run's attribute offsets, resolved once so the lane loops read raw. +/// One run's attribute tokens, minted once so the lane loops read at an offset. struct RunAttrs { - transform: Option, - opacity: Option, - opacity_fill: Option, - clipping_mask: Option, + transform: Option>, + opacity: Option>, + opacity_fill: Option>, + clipping_mask: Option>, } impl RunAttrs { fn of(item: &core_types::record::GroupItem) -> Self { let layout = item.layout(); Self { - transform: layout.offset_of(ATTR_TRANSFORM, 0), - opacity: layout.offset_of(ATTR_OPACITY, 0), - opacity_fill: layout.offset_of(ATTR_OPACITY_FILL, 0), - clipping_mask: layout.offset_of(ATTR_CLIPPING_MASK, 0), + transform: FieldOffset::of(layout, 0), + opacity: FieldOffset::of(layout, 0), + opacity_fill: FieldOffset::of(layout, 0), + clipping_mask: FieldOffset::of(layout, 0), } } - fn read_or(item: &core_types::record::GroupItem, offset: Option, lane: usize, default: T) -> T { - match offset { - // SAFETY: the offset comes from the item's own layout. - Some(offset) => unsafe { item.lanes().get(lane).rec().read(offset) }, + fn read_or<'i, A: Attribute>(item: &'i core_types::record::GroupItem, field: Option>, lane: usize, default: A::Value<'i>) -> A::Value<'i> { + match field.and_then(|field| item.lanes().get(lane).try_attr_at(field)) { + Some(value) => value, None => default, } } diff --git a/node-graph/nodes/graphic/src/record.rs b/node-graph/nodes/graphic/src/record.rs index 52f38126f0..8894326ecd 100644 --- a/node-graph/nodes/graphic/src/record.rs +++ b/node-graph/nodes/graphic/src/record.rs @@ -27,10 +27,9 @@ pub(crate) fn group_leaf_count(group: &core_types::record::Group, fully_flatten: pub(crate) fn group_locate<'e>(group: &core_types::record::Group<'e>, transform: DAffine2, fully_flatten: bool, depth: usize, remaining: &mut usize) -> Option<(Graphic<'e>, DAffine2)> { let item = &group.content; let lanes = item.typed_lanes::().expect("guarded by group_expands"); - let offset = item.layout().offset_of(ATTR_TRANSFORM, 0); + let field = core_types::record::FieldOffset::::of(item.layout(), 0); (0..lanes.len()).find_map(|lane| { - // SAFETY: the offset comes from the item's own layout. - let lane_transform = offset.map(|offset| unsafe { item.lanes().get(lane).rec().read::(offset) }).unwrap_or(DAffine2::IDENTITY); + let lane_transform = item.lanes().get(lane).attr_at(field); locate(lanes.element_ref(lane), transform * lane_transform, fully_flatten, depth + 1, remaining) }) }