diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index e10e354763..d8622d6e08 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -392,6 +392,7 @@ impl ProtoNetwork { lane_invariant, named_writes: Vec::new(), named_reads: Vec::new(), + named_read_defaults: Vec::new(), layout, }), ConstructionArgs::Nodes(inputs) => node.resolved.layout_meta.as_ref().and_then(|meta| { diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index e8f4e35280..09200278a2 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -841,6 +841,13 @@ mod test { assert_eq!(read_back(read_attribute_network("opacity", "opacity", TaggedValue::F64(0.5))), 0.5); } + #[test] + fn an_absent_census_named_read_serves_the_census_default() { + // `opacity` is declared `f64` defaulting to 1, so its absence reads as + // the census default rather than the value type's. + assert_eq!(read_back(read_attribute_network("novel:count", "opacity", TaggedValue::F64(2.5))), 1.); + } + #[test] fn a_named_read_disagreeing_with_its_write_is_refused() { // The name is written at a path upstream and read at `f64` here, which diff --git a/node-graph/libraries/core-types/src/record/access.rs b/node-graph/libraries/core-types/src/record/access.rs index c03f8565d1..f1872ef258 100644 --- a/node-graph/libraries/core-types/src/record/access.rs +++ b/node-graph/libraries/core-types/src/record/access.rs @@ -114,6 +114,29 @@ pub unsafe fn read_at<'e, A: attribute::Attribute>(rec: Rec<'_>, offset: Option< }) } +/// [`read_at`] for a name-generic read, whose marker carries the value type +/// but not the name and so cannot know the name's own default. `absent` is +/// that default's bytes, which the compiler takes from the census once the +/// name is folded; without one the value type's default applies, which is +/// exactly the rule for a name the census does not declare. +/// +/// # Safety +/// As [`read_at`]. `absent`, where present, must hold the object +/// representation of `A::Value`, which the compiler takes from the census row +/// for the folded name after checking that row's value type against `A`'s. A +/// reference-valued default addresses `'static` data, so the value it yields +/// outlives any evaluation. +pub unsafe fn read_at_defaulting<'e, A: attribute::Attribute>(rec: Rec<'_>, offset: Option, absent: Option<&[u8]>) -> attribute::Attr<'e, A> { + attribute::Attr(match (offset, absent) { + // SAFETY: the caller's contract. + (Some(offset), _) => unsafe { rec.read::>(offset) }, + // SAFETY: the caller's contract; the bytes are unaligned storage, so + // the value is read out rather than referenced in place. + (None, Some(bytes)) => unsafe { bytes.as_ptr().cast::>().read_unaligned() }, + (None, None) => A::default(), + }) +} + /// The read-less [`DerivedLazyInput`] glue: the token alone. /// /// # Safety diff --git a/node-graph/libraries/core-types/src/record/layout.rs b/node-graph/libraries/core-types/src/record/layout.rs index 1ddf6f3f02..d322398076 100644 --- a/node-graph/libraries/core-types/src/record/layout.rs +++ b/node-graph/libraries/core-types/src/record/layout.rs @@ -377,6 +377,11 @@ pub struct RecordLayout { /// name and the read input's finished layout sit together, so `set_layout` /// only copies the numbers into the read slots. pub named_reads: Vec>, + /// The census default's bytes for each absent name-from-input read whose + /// name the census declares, in the same order. A name the census does not + /// declare has `None` and reads as its value type's default, which is that + /// case's own rule; a read that resolved to an offset needs no default. + pub named_read_defaults: Vec>>, } /// A write whose name comes from the graph rather than from a marker: the @@ -552,12 +557,31 @@ impl LayoutMeta { // A named read resolves against the input it reads, whose layout is // finished by the time this node folds. An absent attribute stays // `None`, which the read serves as the name's forced default. - let named_reads = self + let named_reads: Vec> = self .named_reads .iter() .zip(&self.folded_read_names) .map(|(read, name)| inputs.get(read.input as usize).copied().flatten().and_then(|layout| layout.offset_of(name, read.template.level))) .collect(); + // An absent read serves the name's own default, which for a declared + // name is the census's rather than the value type's. The census stages + // it the same way it fills any absent field: as the declared default's + // bytes. One name carries one value type, checked when the name folds, + // so the size agreement below is a guard rather than a branch. + let named_read_defaults = self + .named_reads + .iter() + .zip(&self.folded_read_names) + .zip(&named_reads) + .map(|((read, name), offset)| { + let row = offset.is_none().then(|| attribute::info(name))??; + (row.value_type == read.template.type_id && row.size == read.template.size).then(|| { + let mut bytes = vec![0u8; row.size]; + (row.write_default_bytes)(&mut bytes); + bytes.into_boxed_slice() + }) + }) + .collect(); RecordLayout { layout, frame_bytes, @@ -565,6 +589,7 @@ impl LayoutMeta { lane_invariant: 0, named_writes: self.folded_names.clone(), named_reads, + named_read_defaults, } } diff --git a/node-graph/libraries/core-types/src/record/mod.rs b/node-graph/libraries/core-types/src/record/mod.rs index 9a5b5ef9fa..254237617a 100644 --- a/node-graph/libraries/core-types/src/record/mod.rs +++ b/node-graph/libraries/core-types/src/record/mod.rs @@ -21,7 +21,7 @@ mod serve; mod test_support; mod testkit; -pub use access::{Rec, RecordValue, apply_plan, borrow_element, erase_static, read_at, read_element, token_only, write_element, write_element_sized, write_field}; +pub use access::{Rec, RecordValue, apply_plan, borrow_element, erase_static, read_at, read_at_defaulting, read_element, token_only, write_element, write_element_sized, write_field}; pub use frames::{FrameArena, FrameScope, Frames}; pub use input::{DerivedLazyInput, DerivedRecordInput, ElementInput, ElementLazyInput, LevelStatus, RecordExtract, RecordInput, RecordLazyInput, fill_frames, materialize_batch, materialize_level}; pub use layout::{ diff --git a/node-graph/libraries/core-types/src/runtime.rs b/node-graph/libraries/core-types/src/runtime.rs index f4d4f70910..fe365d2850 100644 --- a/node-graph/libraries/core-types/src/runtime.rs +++ b/node-graph/libraries/core-types/src/runtime.rs @@ -280,6 +280,7 @@ mod tests { lane_invariant: u32::MAX, named_writes: Vec::new(), named_reads: Vec::new(), + named_read_defaults: Vec::new(), }); RecordExtract::new(graph, &layout) } diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 3477b3be19..626d4ead38 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -240,6 +240,18 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let slot = format_ident!("__read_{index}"); quote!(pub(super) #slot: Option) })); + // A name-generic read carries the folded name's own default beside its + // offset, since its marker names the value type but not the name. + state.extend( + field_reads(&struct_regular_fields) + .iter() + .enumerate() + .filter(|(_, (_, read))| crate::parsing::named_marker(&read.marker).is_some()) + .map(|(index, _)| { + let slot = format_ident!("__read_default_{index}"); + quote!(pub(super) #slot: Option<::std::boxed::Box<[u8]>>) + }), + ); state.extend((0..node.output.shape.attrs.len()).map(|index| { let slot = format_ident!("__write_{index}"); quote!(pub(super) #slot: usize) @@ -1244,7 +1256,15 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let read_binding = |slot: usize, read: &AttributeRead, rec: TokenStream2| { let pat = &read.pat_ident; let marker = &read.marker; + let default_slot = format_ident!("__read_default_{slot}"); let slot = format_ident!("__read_{slot}"); + // A name-generic marker carries the value type but not the name, so it + // cannot know the name's own default; the compiler supplies it. + if crate::parsing::named_marker(marker).is_some() { + return quote! { + let #pat = unsafe { #core_types::record::read_at_defaulting::<#marker>(#rec, self.#slot, self.#default_slot.as_deref()) }; + }; + } quote! { let #pat = unsafe { #core_types::record::read_at::<#marker>(#rec, self.#slot) }; } @@ -2554,10 +2574,14 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn .enumerate() .filter(|(_, (_, read))| crate::parsing::named_marker(&read.marker).is_some()) .map(|(slot, _)| { + let default_slot = format_ident!("__read_default_{slot}"); let slot = format_ident!("__read_{slot}"); let position = folded_read; folded_read += 1; - quote!(self.#slot = __resolved.named_reads[#position];) + quote! { + self.#slot = __resolved.named_reads[#position]; + self.#default_slot = __resolved.named_read_defaults[#position].clone(); + } }) .collect(); let plan = (!skips_carrier || gather_carrier).then(|| quote!(self.__plan = __resolved.plan;)); @@ -2711,6 +2735,16 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }); let plan_default = (!skips_carrier || gather_carrier).then(|| quote!(__plan: ::std::vec::Vec::new(),)).into_iter(); let read_names = (0..flat_reads.len()).map(|index| format_ident!("__read_{index}")).map(|slot| quote!(#slot,)); + // The folded name's default arrives with the layout, so the constructor + // leaves the slot empty and `set_layout` fills it. + let read_default_inits = flat_reads + .iter() + .enumerate() + .filter(|(_, (_, read))| crate::parsing::named_marker(&read.marker).is_some()) + .map(|(index, _)| { + let slot = format_ident!("__read_default_{index}"); + quote!(#slot: ::core::option::Option::None,) + }); let write_defaults = (0..write_markers.len()).map(|index| format_ident!("__write_{index}")).map(|slot| quote!(#slot: 0,)); let mat_cache_defaults = materialized_indices(®ular_fields, &node).into_iter().map(|index| { let slot = format_ident!("__mat_cache_{index}"); @@ -2757,6 +2791,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn __frame_bytes: 0, __lane_invariant: 0, #(#read_names)* + #(#read_default_inits)* #(#write_defaults)* #(#mat_cache_defaults)* #(#slot_default)* diff --git a/node-graph/nodes/gcore/src/record.rs b/node-graph/nodes/gcore/src/record.rs index a9e3565243..92d688be54 100644 --- a/node-graph/nodes/gcore/src/record.rs +++ b/node-graph/nodes/gcore/src/record.rs @@ -664,6 +664,7 @@ mod tests { let resolved = core_types::record::RecordLayout { named_writes: Vec::new(), named_reads: Vec::new(), + named_read_defaults: Vec::new(), lane_invariant: u32::MAX, ..meta.resolve(inputs) }; @@ -675,6 +676,7 @@ mod tests { let bundle = core_types::record::RecordLayout { named_writes: Vec::new(), named_reads: Vec::new(), + named_read_defaults: Vec::new(), frame_bytes: layout.frame_bytes(), plan: Vec::new(), layout: layout.clone(), @@ -1581,6 +1583,7 @@ mod tests { let resolved = core_types::record::RecordLayout { named_writes: Vec::new(), named_reads: Vec::new(), + named_read_defaults: Vec::new(), lane_invariant: 0, ..mirror_layout_meta().resolve(&[Some(&layout)]) }; diff --git a/node-graph/nodes/graphic/src/record.rs b/node-graph/nodes/graphic/src/record.rs index b7343547bc..869019228a 100644 --- a/node-graph/nodes/graphic/src/record.rs +++ b/node-graph/nodes/graphic/src/record.rs @@ -287,6 +287,7 @@ mod tests { let resolved = record::RecordLayout { named_writes: Vec::new(), named_reads: Vec::new(), + named_read_defaults: Vec::new(), lane_invariant: u32::MAX, ..meta.resolve(inputs) }; @@ -298,6 +299,7 @@ mod tests { let bundle = record::RecordLayout { named_writes: Vec::new(), named_reads: Vec::new(), + named_read_defaults: Vec::new(), frame_bytes: layout.frame_bytes(), plan: Vec::new(), layout: layout.clone(), diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index 1136da432f..326970ad51 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -274,6 +274,7 @@ mod tests { lane_invariant: u32::MAX, named_writes: Vec::new(), named_reads: Vec::new(), + named_read_defaults: Vec::new(), }, ); let GPoll::Final(result) = core_types::record::serve_input(&graph, &ctx, &frames) else { diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index f48f3dd1bc..11614d6494 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -1055,6 +1055,7 @@ mod graphene_test { node.set_layout(core_types::record::RecordLayout { named_writes: Vec::new(), named_reads: Vec::new(), + named_read_defaults: Vec::new(), frame_bytes: layout.frame_bytes(), plan: Vec::new(), layout: layout.clone(), @@ -1118,6 +1119,7 @@ mod graphene_test { wired.set_layout(core_types::record::RecordLayout { named_writes: Vec::new(), named_reads: Vec::new(), + named_read_defaults: Vec::new(), frame_bytes: layout.frame_bytes(), plan: Vec::new(), layout: layout.clone(), @@ -1166,6 +1168,7 @@ mod graphene_test { wired.set_layout(core_types::record::RecordLayout { named_writes: Vec::new(), named_reads: Vec::new(), + named_read_defaults: Vec::new(), frame_bytes: layout.frame_bytes(), plan: Vec::new(), layout: layout.clone(),