mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-02 00:18:11 +08:00
Add per-field erased-read glue, the record monitor with introspection capture, and rename record constructors to new
This commit is contained in:
@@ -145,8 +145,14 @@ impl DynamicExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Calls the `Node::serialize` for that specific node, returning for example the cached value for a monitor node. The node path must match the document node path.
|
/// Calls the `Node::serialize` for that specific node, returning for example the cached value for a monitor node. The node path must match the document node path.
|
||||||
|
/// A record capture materializes here against the arena, inside the introspection window.
|
||||||
pub fn introspect(&self, node_path: &[NodeId]) -> Result<Arc<dyn std::any::Any + Send + Sync + 'static>, IntrospectError> {
|
pub fn introspect(&self, node_path: &[NodeId]) -> Result<Arc<dyn std::any::Any + Send + Sync + 'static>, IntrospectError> {
|
||||||
self.tree.introspect(node_path)
|
let result = self.tree.introspect(node_path)?;
|
||||||
|
if let Some(capture) = result.downcast_ref::<core_types::record::RecordCapture>() {
|
||||||
|
let arena = self.arena.lock().unwrap_or_else(PoisonError::into_inner);
|
||||||
|
return capture.materialize(&arena).map(|fields| Arc::new(fields) as Arc<_>).ok_or(IntrospectError::NoData);
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn input_type(&self) -> Option<Type> {
|
pub fn input_type(&self) -> Option<Type> {
|
||||||
@@ -624,6 +630,30 @@ mod test {
|
|||||||
assert_eq!((&executor).execute(()).unwrap(), GPoll::Final(TaggedValue::F64(7.)));
|
assert_eq!((&executor).execute(()).unwrap(), GPoll::Final(TaggedValue::F64(7.)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_record_monitor_row_forwards_and_introspects_through_the_executor() {
|
||||||
|
let mut monitor = proto_node("graphene_core::memo::MonitorNode", vec![NodeId(1)]);
|
||||||
|
monitor.original_location.path = Some(vec![NodeId(9)]);
|
||||||
|
let network = ProtoNetwork {
|
||||||
|
inputs: vec![],
|
||||||
|
output: NodeId(3),
|
||||||
|
nodes: vec![
|
||||||
|
(NodeId(0), ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64(7.).into()), vec![])),
|
||||||
|
(NodeId(1), proto_node("core_types::record::RecordLiftNode", vec![NodeId(0)])),
|
||||||
|
(NodeId(2), monitor),
|
||||||
|
(NodeId(3), proto_node("core_types::record::RecordExtractNode", vec![NodeId(2)])),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let executor = DynamicExecutor::new(network).unwrap();
|
||||||
|
assert_eq!((&executor).execute(()).unwrap(), GPoll::Final(TaggedValue::F64(7.)));
|
||||||
|
let fields = executor.introspect(&[NodeId(9)]).unwrap();
|
||||||
|
let fields = fields
|
||||||
|
.downcast_ref::<Vec<(&'static str, Box<dyn core_types::list::AnyAttributeValue>)>>()
|
||||||
|
.expect("a record capture materializes to its fields");
|
||||||
|
assert!(fields.is_empty(), "an element-only record has no attribute fields");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_lift_adapter_is_spliced_between_a_plain_producer_and_a_record_consumer() {
|
fn a_lift_adapter_is_spliced_between_a_plain_producer_and_a_record_consumer() {
|
||||||
let network = ProtoNetwork {
|
let network = ProtoNetwork {
|
||||||
|
|||||||
@@ -376,6 +376,30 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
|
|||||||
lend_node!(f64),
|
lend_node!(f64),
|
||||||
record_lift_node!(f64),
|
record_lift_node!(f64),
|
||||||
record_extract_node!(f64),
|
record_extract_node!(f64),
|
||||||
|
(
|
||||||
|
ProtoNodeIdentifier::new("graphene_core::memo::MonitorNode"),
|
||||||
|
RegistryEntry {
|
||||||
|
io: NodeIOTypes::new(
|
||||||
|
concrete!(Context),
|
||||||
|
core_types::Type::Record(Box::new(core_types::Type::Generic(std::borrow::Cow::Borrowed("T")))),
|
||||||
|
vec![core_types::registry::generic_record_edge_type("T")],
|
||||||
|
),
|
||||||
|
constructor: |inputs| {
|
||||||
|
if inputs.len() != 1 {
|
||||||
|
return Err(ConstructionError::Arity { expected: 1, got: inputs.len() });
|
||||||
|
}
|
||||||
|
let mut inputs = inputs.into_iter();
|
||||||
|
let handle = inputs.next().unwrap();
|
||||||
|
let ty = handle.ty().clone();
|
||||||
|
let Some(layout) = handle.layout().cloned() else {
|
||||||
|
return Err(ConstructionError::MissingLayout);
|
||||||
|
};
|
||||||
|
let edge = handle.downcast_erased::<core_types::registry::ErasedRecordNode>(ty.clone())?;
|
||||||
|
let node = core_types::record::RecordMonitor::new(edge, &layout);
|
||||||
|
Ok(EdgeHandle::new_erased(std::sync::Arc::new(node) as std::sync::Arc<core_types::registry::ErasedRecordNode>, ty))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
clone_node!(f64),
|
clone_node!(f64),
|
||||||
frame_memo_node!(f64),
|
frame_memo_node!(f64),
|
||||||
lend_node!(f32),
|
lend_node!(f32),
|
||||||
@@ -782,7 +806,7 @@ mod node_registry_macros {
|
|||||||
return Err(ConstructionError::Arity { expected: 1, got: inputs.len() });
|
return Err(ConstructionError::Arity { expected: 1, got: inputs.len() });
|
||||||
}
|
}
|
||||||
let mut inputs = inputs.into_iter();
|
let mut inputs = inputs.into_iter();
|
||||||
let node = core_types::record::RecordLift::<$type, _>::wire(inputs.next().unwrap().downcast::<$type>()?);
|
let node = core_types::record::RecordLift::<$type, _>::new(inputs.next().unwrap().downcast::<$type>()?);
|
||||||
Ok(EdgeHandle::new_record::<$type>(std::sync::Arc::new(node) as std::sync::Arc<core_types::registry::ErasedRecordNode>))
|
Ok(EdgeHandle::new_record::<$type>(std::sync::Arc::new(node) as std::sync::Arc<core_types::registry::ErasedRecordNode>))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -801,7 +825,7 @@ mod node_registry_macros {
|
|||||||
return Err(ConstructionError::Arity { expected: 1, got: inputs.len() });
|
return Err(ConstructionError::Arity { expected: 1, got: inputs.len() });
|
||||||
}
|
}
|
||||||
let mut inputs = inputs.into_iter();
|
let mut inputs = inputs.into_iter();
|
||||||
let node = core_types::record::RecordExtract::<$type, _>::wire(inputs.next().unwrap().downcast_record::<$type>()?);
|
let node = core_types::record::RecordExtract::<$type, _>::new(inputs.next().unwrap().downcast_record::<$type>()?);
|
||||||
Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedNode<$type>>))
|
Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedNode<$type>>))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ impl Drop for Arena {
|
|||||||
|
|
||||||
pub struct ArenaWeak<T> {
|
pub struct ArenaWeak<T> {
|
||||||
word: u64,
|
word: u64,
|
||||||
_marker: PhantomData<*const T>,
|
_marker: PhantomData<fn() -> T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Clone for ArenaWeak<T> {
|
impl<T> Clone for ArenaWeak<T> {
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ pub trait Attribute: 'static {
|
|||||||
fn default<'e>() -> Self::Value<'e> {
|
fn default<'e>() -> Self::Value<'e> {
|
||||||
Default::default()
|
Default::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// `ptr` must point at a live field of this marker's value type.
|
||||||
|
unsafe fn read_erased(ptr: *const u8) -> Box<dyn AnyAttributeValue>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A kernel-facing attribute value. A parameter `Attr<A>` is a read of `A`
|
/// A kernel-facing attribute value. A parameter `Attr<A>` is a read of `A`
|
||||||
@@ -154,6 +158,10 @@ macro_rules! attribute {
|
|||||||
$default
|
$default
|
||||||
}
|
}
|
||||||
)?
|
)?
|
||||||
|
|
||||||
|
unsafe fn read_erased(ptr: *const u8) -> ::std::boxed::Box<dyn $crate::list::AnyAttributeValue> {
|
||||||
|
::std::boxed::Box::new(unsafe { ptr.cast::<&$value>().read() }.to_owned())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$crate::attribute!(@register $marker);
|
$crate::attribute!(@register $marker);
|
||||||
@@ -171,6 +179,10 @@ macro_rules! attribute {
|
|||||||
$default
|
$default
|
||||||
}
|
}
|
||||||
)?
|
)?
|
||||||
|
|
||||||
|
unsafe fn read_erased(ptr: *const u8) -> ::std::boxed::Box<dyn $crate::list::AnyAttributeValue> {
|
||||||
|
::std::boxed::Box::new(unsafe { ptr.cast::<$value>().read() })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$crate::attribute!(@register $marker);
|
$crate::attribute!(@register $marker);
|
||||||
@@ -256,6 +268,10 @@ mod tests {
|
|||||||
impl Attribute for Conflict {
|
impl Attribute for Conflict {
|
||||||
const NAME: &'static str = "opacity";
|
const NAME: &'static str = "opacity";
|
||||||
type Value<'e> = bool;
|
type Value<'e> = bool;
|
||||||
|
|
||||||
|
unsafe fn read_erased(ptr: *const u8) -> Box<dyn AnyAttributeValue> {
|
||||||
|
Box::new(unsafe { ptr.cast::<bool>().read() })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
register::<Conflict>();
|
register::<Conflict>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,17 +10,52 @@ use crate::attribute;
|
|||||||
use crate::gpoll::GPoll;
|
use crate::gpoll::GPoll;
|
||||||
use crate::node::Node;
|
use crate::node::Node;
|
||||||
|
|
||||||
|
/// A field write declared at wiring, carrying the marker's erased-read glue
|
||||||
|
/// so introspection and persistence never consult the census at runtime.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct FieldWrite {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub level: u8,
|
||||||
|
pub size: usize,
|
||||||
|
pub align: usize,
|
||||||
|
pub read_erased: unsafe fn(*const u8) -> Box<dyn crate::list::AnyAttributeValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FieldWrite {
|
||||||
|
pub fn of<A: crate::attribute::Attribute>(level: u8) -> Self {
|
||||||
|
Self {
|
||||||
|
name: A::NAME,
|
||||||
|
level,
|
||||||
|
size: size_of::<A::Value<'static>>(),
|
||||||
|
align: align_of::<A::Value<'static>>(),
|
||||||
|
read_erased: A::read_erased,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// One field of a [`Layout`]: a (name, level) key resolved to an offset.
|
/// One field of a [`Layout`]: a (name, level) key resolved to an offset.
|
||||||
/// Levels are numbered innermost-out; only level 0 exists at rank 0.
|
/// Levels are numbered innermost-out; only level 0 exists at rank 0.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
/// Equality is structural: the glue pointer is excluded, since fn-pointer
|
||||||
|
/// identity is not guaranteed across codegen units and layout equality
|
||||||
|
/// drives identity forwarding.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub struct FieldDesc {
|
pub struct FieldDesc {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub level: u8,
|
pub level: u8,
|
||||||
pub offset: usize,
|
pub offset: usize,
|
||||||
pub size: usize,
|
pub size: usize,
|
||||||
pub align: usize,
|
pub align: usize,
|
||||||
|
pub read_erased: unsafe fn(*const u8) -> Box<dyn crate::list::AnyAttributeValue>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq for FieldDesc {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
(self.name, self.level, self.offset, self.size, self.align) == (other.name, other.level, other.offset, other.size, other.align)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for FieldDesc {}
|
||||||
|
|
||||||
/// A record layout: the element at offset 0, then the written attributes in
|
/// A record layout: the element at offset 0, then the written attributes in
|
||||||
/// canonical order (descending alignment, then size, then name, then level).
|
/// canonical order (descending alignment, then size, then name, then level).
|
||||||
/// Layouts are derived data, a pure function of the upstream write set.
|
/// Layouts are derived data, a pure function of the upstream write set.
|
||||||
@@ -43,31 +78,42 @@ impl Layout {
|
|||||||
/// (size, align) at `depth`, in canonical order. A (name, level) written
|
/// (size, align) at `depth`, in canonical order. A (name, level) written
|
||||||
/// at a different size is a type conflict and panics; the census keeps
|
/// at a different size is a type conflict and panics; the census keeps
|
||||||
/// declared names to one type, so this only fires on wiring bugs.
|
/// declared names to one type, so this only fires on wiring bugs.
|
||||||
pub fn with_writes(&self, depth: u8, element: (usize, usize), writes: &[(&'static str, u8, usize, usize)]) -> Layout {
|
pub fn with_writes(&self, depth: u8, element: (usize, usize), writes: &[FieldWrite]) -> Layout {
|
||||||
let mut merged: Vec<(&'static str, u8, usize, usize)> = self.fields.iter().map(|field| (field.name, field.level, field.size, field.align)).collect();
|
let mut merged: Vec<FieldWrite> = self
|
||||||
for &(name, level, size, align) in writes {
|
.fields
|
||||||
match merged.iter().find(|(n, l, ..)| *n == name && *l == level) {
|
.iter()
|
||||||
Some(&(.., existing_size, _)) => assert_eq!(existing_size, size, "attribute `{name}` written at two different sizes"),
|
.map(|field| FieldWrite {
|
||||||
None => merged.push((name, level, size, align)),
|
name: field.name,
|
||||||
|
level: field.level,
|
||||||
|
size: field.size,
|
||||||
|
align: field.align,
|
||||||
|
read_erased: field.read_erased,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
for &write in writes {
|
||||||
|
match merged.iter().find(|field| field.name == write.name && field.level == write.level) {
|
||||||
|
Some(existing) => assert_eq!(existing.size, write.size, "attribute `{}` written at two different sizes", write.name),
|
||||||
|
None => merged.push(write),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
merged.sort_by(|a, b| b.3.cmp(&a.3).then(b.2.cmp(&a.2)).then(a.0.cmp(b.0)).then(a.1.cmp(&b.1)));
|
merged.sort_by(|a, b| b.align.cmp(&a.align).then(b.size.cmp(&a.size)).then(a.name.cmp(b.name)).then(a.level.cmp(&b.level)));
|
||||||
let (element_size, element_align) = element;
|
let (element_size, element_align) = element;
|
||||||
let mut offset = element_size;
|
let mut offset = element_size;
|
||||||
let mut align = element_align.max(1);
|
let mut align = element_align.max(1);
|
||||||
let fields = merged
|
let fields = merged
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(name, level, size, field_align)| {
|
.map(|write| {
|
||||||
offset = offset.next_multiple_of(field_align.max(1));
|
offset = offset.next_multiple_of(write.align.max(1));
|
||||||
align = align.max(field_align);
|
align = align.max(write.align);
|
||||||
let desc = FieldDesc {
|
let desc = FieldDesc {
|
||||||
name,
|
name: write.name,
|
||||||
level,
|
level: write.level,
|
||||||
offset,
|
offset,
|
||||||
size,
|
size: write.size,
|
||||||
align: field_align,
|
align: write.align,
|
||||||
|
read_erased: write.read_erased,
|
||||||
};
|
};
|
||||||
offset += size;
|
offset += write.size;
|
||||||
desc
|
desc
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -89,7 +135,17 @@ impl Layout {
|
|||||||
assert_eq!(union.element_size, layout.element_size, "union layouts must share the element size");
|
assert_eq!(union.element_size, layout.element_size, "union layouts must share the element size");
|
||||||
assert_eq!(union.element_align, layout.element_align, "union layouts must share the element alignment");
|
assert_eq!(union.element_align, layout.element_align, "union layouts must share the element alignment");
|
||||||
assert_eq!(union.depth, layout.depth, "union layouts must share the depth");
|
assert_eq!(union.depth, layout.depth, "union layouts must share the depth");
|
||||||
let writes: Vec<(&'static str, u8, usize, usize)> = layout.fields.iter().map(|field| (field.name, field.level, field.size, field.align)).collect();
|
let writes: Vec<FieldWrite> = layout
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.map(|field| FieldWrite {
|
||||||
|
name: field.name,
|
||||||
|
level: field.level,
|
||||||
|
size: field.size,
|
||||||
|
align: field.align,
|
||||||
|
read_erased: field.read_erased,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
union = union.with_writes(union.depth, (union.element_size, union.element_align), &writes);
|
union = union.with_writes(union.depth, (union.element_size, union.element_align), &writes);
|
||||||
}
|
}
|
||||||
union
|
union
|
||||||
@@ -344,7 +400,7 @@ pub struct RecordSource<N> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<N> RecordSource<N> {
|
impl<N> RecordSource<N> {
|
||||||
pub fn wire(edge: N, source: &Layout, union: &Layout) -> Self {
|
pub fn new(edge: N, source: &Layout, union: &Layout) -> Self {
|
||||||
Self {
|
Self {
|
||||||
edge,
|
edge,
|
||||||
plan: SourcePlan::new(source, union),
|
plan: SourcePlan::new(source, union),
|
||||||
@@ -352,6 +408,78 @@ impl<N> RecordSource<N> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A captured record: the layout plus a generation-checked handle to the
|
||||||
|
/// arena copy, materialized by the introspection holder, which owns the
|
||||||
|
/// arena. A dead generation materializes to `None`, never to a stale read.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RecordCapture {
|
||||||
|
layout: Layout,
|
||||||
|
bytes: crate::arena::ArenaWeak<Box<[u8]>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RecordCapture {
|
||||||
|
pub fn materialize(&self, arena: &crate::arena::Arena) -> Option<Vec<(&'static str, Box<dyn crate::list::AnyAttributeValue>)>> {
|
||||||
|
let bytes = self.bytes.upgrade(arena)?;
|
||||||
|
Some(
|
||||||
|
self.layout
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.map(|field| (field.name, unsafe { (field.read_erased)(bytes.as_ptr().add(field.offset)) }))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Convert to a `#[node_macro::node]` node once routing nodes forward
|
||||||
|
// layouts and the macro grows a capture capability.
|
||||||
|
/// The monitor over a record wire: forwards the record and captures an arena
|
||||||
|
/// copy readable through the introspection window, like a frame memo.
|
||||||
|
pub struct RecordMonitor<N> {
|
||||||
|
edge: N,
|
||||||
|
layout: Layout,
|
||||||
|
capture: std::sync::Mutex<Option<RecordCapture>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<N> RecordMonitor<N> {
|
||||||
|
pub fn new(edge: N, layout: &Layout) -> Self {
|
||||||
|
Self {
|
||||||
|
edge,
|
||||||
|
layout: layout.clone(),
|
||||||
|
capture: std::sync::Mutex::new(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'e, C, N> Node<C> for RecordMonitor<N>
|
||||||
|
where
|
||||||
|
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
|
||||||
|
N: Node<C, Output = RecordValue<'e>>,
|
||||||
|
{
|
||||||
|
type Output = RecordValue<'e>;
|
||||||
|
|
||||||
|
fn eval(&self, input: &C) -> GPoll<RecordValue<'e>> {
|
||||||
|
let value = self.edge.eval(input);
|
||||||
|
if let GPoll::Final(record) | GPoll::Partial(record) = &value {
|
||||||
|
let bytes: Box<[u8]> = unsafe { std::slice::from_raw_parts(record.rec().ptr(), self.layout.size) }.into();
|
||||||
|
let capture = input.arena().alloc(bytes).map(|(_, weak)| RecordCapture {
|
||||||
|
layout: self.layout.clone(),
|
||||||
|
bytes: weak,
|
||||||
|
});
|
||||||
|
*self.capture.lock().unwrap_or_else(std::sync::PoisonError::into_inner) = capture;
|
||||||
|
}
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(&self) -> Option<&Layout> {
|
||||||
|
Some(&self.layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
|
||||||
|
let capture = self.capture.lock().unwrap_or_else(std::sync::PoisonError::into_inner).clone()?;
|
||||||
|
Some(std::sync::Arc::new(capture))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Lifts a plain producer onto a record wire: the element lands at offset 0
|
/// Lifts a plain producer onto a record wire: the element lands at offset 0
|
||||||
/// of a fresh element-only record. `Copy` elements only until droppable
|
/// of a fresh element-only record. `Copy` elements only until droppable
|
||||||
/// elements ride records.
|
/// elements ride records.
|
||||||
@@ -363,7 +491,7 @@ pub struct RecordLift<El, N> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<El: Copy + 'static, N> RecordLift<El, N> {
|
impl<El: Copy + 'static, N> RecordLift<El, N> {
|
||||||
pub fn wire(edge: N) -> Self {
|
pub fn new(edge: N) -> Self {
|
||||||
let layout = Layout::default().with_writes(0, (size_of::<El>(), align_of::<El>()), &[]);
|
let layout = Layout::default().with_writes(0, (size_of::<El>(), align_of::<El>()), &[]);
|
||||||
let frame_bytes = layout.size.next_multiple_of(8);
|
let frame_bytes = layout.size.next_multiple_of(8);
|
||||||
Self {
|
Self {
|
||||||
@@ -405,7 +533,7 @@ pub struct RecordExtract<El, N> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<El, N> RecordExtract<El, N> {
|
impl<El, N> RecordExtract<El, N> {
|
||||||
pub fn wire(edge: N) -> Self {
|
pub fn new(edge: N) -> Self {
|
||||||
Self {
|
Self {
|
||||||
edge,
|
edge,
|
||||||
_marker: std::marker::PhantomData,
|
_marker: std::marker::PhantomData,
|
||||||
@@ -447,13 +575,21 @@ where
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn f64_field(name: &'static str) -> (&'static str, u8, usize, usize) {
|
unsafe fn unread(_: *const u8) -> Box<dyn crate::list::AnyAttributeValue> {
|
||||||
(name, 0, 8, 8)
|
unreachable!("layout-only test field")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sized_field(name: &'static str, size: usize, align: usize) -> FieldWrite {
|
||||||
|
FieldWrite { name, level: 0, size, align, read_erased: unread }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn f64_field(name: &'static str) -> FieldWrite {
|
||||||
|
sized_field(name, 8, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn canonical_order_and_offsets() {
|
fn canonical_order_and_offsets() {
|
||||||
let layout = Layout::default().with_writes(0, (8, 8), &[("tint", 0, 4, 4), f64_field("opacity"), ("flag", 0, 1, 1)]);
|
let layout = Layout::default().with_writes(0, (8, 8), &[sized_field("tint", 4, 4), f64_field("opacity"), sized_field("flag", 1, 1)]);
|
||||||
assert_eq!(layout.offset_of("opacity", 0), Some(8));
|
assert_eq!(layout.offset_of("opacity", 0), Some(8));
|
||||||
assert_eq!(layout.offset_of("tint", 0), Some(16));
|
assert_eq!(layout.offset_of("tint", 0), Some(16));
|
||||||
assert_eq!(layout.offset_of("flag", 0), Some(20));
|
assert_eq!(layout.offset_of("flag", 0), Some(20));
|
||||||
@@ -465,7 +601,7 @@ mod tests {
|
|||||||
#[should_panic(expected = "two different sizes")]
|
#[should_panic(expected = "two different sizes")]
|
||||||
fn size_conflicts_panic() {
|
fn size_conflicts_panic() {
|
||||||
let layout = Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")]);
|
let layout = Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")]);
|
||||||
layout.with_writes(0, (8, 8), &[("opacity", 0, 4, 4)]);
|
layout.with_writes(0, (8, 8), &[sized_field("opacity", 4, 4)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1241,16 +1241,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
let write_descs: Vec<TokenStream2> = shape
|
let write_descs: Vec<TokenStream2> = shape
|
||||||
.write_markers
|
.write_markers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|marker| {
|
.map(|marker| quote!(#core_types::record::FieldWrite::of::<#marker>(0)))
|
||||||
quote! {
|
|
||||||
(
|
|
||||||
<#marker as #core_types::attribute::Attribute>::NAME,
|
|
||||||
0u8,
|
|
||||||
::core::mem::size_of::<<#marker as #core_types::attribute::Attribute>::Value<'static>>(),
|
|
||||||
::core::mem::align_of::<<#marker as #core_types::attribute::Attribute>::Value<'static>>(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
let element_dims = match &shape.element_write {
|
let element_dims = match &shape.element_write {
|
||||||
Some(ty) => quote!((::core::mem::size_of::<#ty>(), ::core::mem::align_of::<#ty>())),
|
Some(ty) => quote!((::core::mem::size_of::<#ty>(), ::core::mem::align_of::<#ty>())),
|
||||||
@@ -1306,7 +1297,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
#[automatically_derived]
|
#[automatically_derived]
|
||||||
impl<#(#data_field_generic_idents,)* #(#node_generics,)*> #mod_name::#struct_name<#(#struct_type_params,)*> {
|
impl<#(#data_field_generic_idents,)* #(#node_generics,)*> #mod_name::#struct_name<#(#struct_type_params,)*> {
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
#vis fn wire(#(#edge_args,)* #(#carrier_layout_param)*) -> Self {
|
#vis fn new(#(#edge_args,)* #(#carrier_layout_param)*) -> Self {
|
||||||
#layout_binding
|
#layout_binding
|
||||||
#plan_binding
|
#plan_binding
|
||||||
#(#read_inits)*
|
#(#read_inits)*
|
||||||
@@ -1905,7 +1896,7 @@ fn record_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fie
|
|||||||
}
|
}
|
||||||
let mut inputs = inputs.into_iter();
|
let mut inputs = inputs.into_iter();
|
||||||
#(#downcasts)*
|
#(#downcasts)*
|
||||||
let __node = #struct_name::wire(#(#names,)* #(#wire_layout_arg)*);
|
let __node = #struct_name::new(#(#names,)* #(#wire_layout_arg)*);
|
||||||
#construct_output
|
#construct_output
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
|||||||
@@ -135,7 +135,16 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn f64_layout(names: &[&'static str]) -> Layout {
|
fn f64_layout(names: &[&'static str]) -> Layout {
|
||||||
let writes: Vec<(&'static str, u8, usize, usize)> = names.iter().map(|name| (*name, 0, 8, 8)).collect();
|
let writes: Vec<core_types::record::FieldWrite> = names
|
||||||
|
.iter()
|
||||||
|
.map(|name| core_types::record::FieldWrite {
|
||||||
|
name,
|
||||||
|
level: 0,
|
||||||
|
size: 8,
|
||||||
|
align: 8,
|
||||||
|
read_erased: <Opacity as AttributeMarker>::read_erased,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
Layout::default().with_writes(0, (8, 8), &writes)
|
Layout::default().with_writes(0, (8, 8), &writes)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,8 +177,8 @@ mod tests {
|
|||||||
let stacked = multiply_opacity_layout(&modified);
|
let stacked = multiply_opacity_layout(&modified);
|
||||||
reserve_for(&[&source_layout, &modified, &stacked]);
|
reserve_for(&[&source_layout, &modified, &stacked]);
|
||||||
|
|
||||||
let chain = MultiplyOpacityNode::wire(
|
let chain = MultiplyOpacityNode::new(
|
||||||
MultiplyOpacityNode::wire(bare_source(&source_layout, 2.), ValueNode(0.5), &source_layout),
|
MultiplyOpacityNode::new(bare_source(&source_layout, 2.), ValueNode(0.5), &source_layout),
|
||||||
ValueNode(0.5),
|
ValueNode(0.5),
|
||||||
&modified,
|
&modified,
|
||||||
);
|
);
|
||||||
@@ -193,7 +202,7 @@ mod tests {
|
|||||||
let measured = measure_layout(&source_layout);
|
let measured = measure_layout(&source_layout);
|
||||||
reserve_for(&[&source_layout, &measured]);
|
reserve_for(&[&source_layout, &measured]);
|
||||||
|
|
||||||
let chain = MeasureNode::wire(bare_source(&source_layout, -2.), &source_layout);
|
let chain = MeasureNode::new(bare_source(&source_layout, -2.), &source_layout);
|
||||||
let GPoll::Final(value) = chain.eval(&ctx) else {
|
let GPoll::Final(value) = chain.eval(&ctx) else {
|
||||||
panic!("expected a final record");
|
panic!("expected a final record");
|
||||||
};
|
};
|
||||||
@@ -214,7 +223,7 @@ mod tests {
|
|||||||
let measured = measure_layout(&modified);
|
let measured = measure_layout(&modified);
|
||||||
reserve_for(&[&source_layout, &modified, &measured]);
|
reserve_for(&[&source_layout, &modified, &measured]);
|
||||||
|
|
||||||
let chain = MeasureNode::wire(MultiplyOpacityNode::wire(bare_source(&source_layout, -2.), ValueNode(0.5), &source_layout), &modified);
|
let chain = MeasureNode::new(MultiplyOpacityNode::new(bare_source(&source_layout, -2.), ValueNode(0.5), &source_layout), &modified);
|
||||||
let GPoll::Final(value) = chain.eval(&ctx) else {
|
let GPoll::Final(value) = chain.eval(&ctx) else {
|
||||||
panic!("expected a final record");
|
panic!("expected a final record");
|
||||||
};
|
};
|
||||||
@@ -235,13 +244,13 @@ mod tests {
|
|||||||
let shaded = shade_layout(&modified);
|
let shaded = shade_layout(&modified);
|
||||||
reserve_for(&[&source_layout, &modified, &shaded]);
|
reserve_for(&[&source_layout, &modified, &shaded]);
|
||||||
|
|
||||||
let bare = ShadeNode::wire(bare_source(&source_layout, 4.), &source_layout);
|
let bare = ShadeNode::new(bare_source(&source_layout, 4.), &source_layout);
|
||||||
let GPoll::Final(value) = bare.eval(&ctx) else {
|
let GPoll::Final(value) = bare.eval(&ctx) else {
|
||||||
panic!("expected a final record");
|
panic!("expected a final record");
|
||||||
};
|
};
|
||||||
assert_eq!(unsafe { value.rec().element::<f64>() }, 4.);
|
assert_eq!(unsafe { value.rec().element::<f64>() }, 4.);
|
||||||
|
|
||||||
let chain = ShadeNode::wire(MultiplyOpacityNode::wire(bare_source(&source_layout, 4.), ValueNode(0.5), &source_layout), &modified);
|
let chain = ShadeNode::new(MultiplyOpacityNode::new(bare_source(&source_layout, 4.), ValueNode(0.5), &source_layout), &modified);
|
||||||
let GPoll::Final(value) = chain.eval(&ctx) else {
|
let GPoll::Final(value) = chain.eval(&ctx) else {
|
||||||
panic!("expected a final record");
|
panic!("expected a final record");
|
||||||
};
|
};
|
||||||
@@ -263,7 +272,7 @@ mod tests {
|
|||||||
let u32_faded = fade_layout(&u32_source);
|
let u32_faded = fade_layout(&u32_source);
|
||||||
reserve_for(&[&f64_source, &f64_faded, &u32_source, &u32_faded]);
|
reserve_for(&[&f64_source, &f64_faded, &u32_source, &u32_faded]);
|
||||||
|
|
||||||
let wide = FadeNode::wire(bare_source(&f64_source, 8.), ValueNode(0.5), &f64_source);
|
let wide = FadeNode::new(bare_source(&f64_source, 8.), ValueNode(0.5), &f64_source);
|
||||||
let GPoll::Final(value) = wide.eval(&ctx) else {
|
let GPoll::Final(value) = wide.eval(&ctx) else {
|
||||||
panic!("expected a final record");
|
panic!("expected a final record");
|
||||||
};
|
};
|
||||||
@@ -271,7 +280,7 @@ mod tests {
|
|||||||
assert_eq!(unsafe { rec.element::<f64>() }, 8.);
|
assert_eq!(unsafe { rec.element::<f64>() }, 8.);
|
||||||
assert_eq!(unsafe { rec.read::<f64>(f64_faded.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5);
|
assert_eq!(unsafe { rec.read::<f64>(f64_faded.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5);
|
||||||
|
|
||||||
let narrow = FadeNode::wire(
|
let narrow = FadeNode::new(
|
||||||
RecordSourceNode {
|
RecordSourceNode {
|
||||||
frame_bytes: frame_bytes(&u32_source),
|
frame_bytes: frame_bytes(&u32_source),
|
||||||
element: 7u32,
|
element: 7u32,
|
||||||
@@ -299,7 +308,7 @@ mod tests {
|
|||||||
let layout = source_opacity_layout();
|
let layout = source_opacity_layout();
|
||||||
reserve_for(&[&layout]);
|
reserve_for(&[&layout]);
|
||||||
|
|
||||||
let node = SourceOpacityNode::wire(ValueNode(3.), ValueNode(0.25));
|
let node = SourceOpacityNode::new(ValueNode(3.), ValueNode(0.25));
|
||||||
assert_eq!(Node::<ContextImpl>::layout(&node), Some(&layout));
|
assert_eq!(Node::<ContextImpl>::layout(&node), Some(&layout));
|
||||||
let GPoll::Final(value) = node.eval(&ctx) else {
|
let GPoll::Final(value) = node.eval(&ctx) else {
|
||||||
panic!("expected a final record");
|
panic!("expected a final record");
|
||||||
@@ -320,7 +329,7 @@ mod tests {
|
|||||||
let modified = multiply_opacity_layout(&source_layout);
|
let modified = multiply_opacity_layout(&source_layout);
|
||||||
reserve_for(&[&source_layout, &modified]);
|
reserve_for(&[&source_layout, &modified]);
|
||||||
|
|
||||||
let chain = MultiplyOpacityNode::wire(
|
let chain = MultiplyOpacityNode::new(
|
||||||
RecordSourceNode {
|
RecordSourceNode {
|
||||||
frame_bytes: frame_bytes(&source_layout),
|
frame_bytes: frame_bytes(&source_layout),
|
||||||
element: 1.,
|
element: 1.,
|
||||||
@@ -347,13 +356,13 @@ mod tests {
|
|||||||
let modified = checked_multiply_opacity_layout(&source_layout);
|
let modified = checked_multiply_opacity_layout(&source_layout);
|
||||||
reserve_for(&[&source_layout, &modified]);
|
reserve_for(&[&source_layout, &modified]);
|
||||||
|
|
||||||
let ok = CheckedMultiplyOpacityNode::wire(bare_source(&source_layout, 1.), ValueNode(0.5), &source_layout);
|
let ok = CheckedMultiplyOpacityNode::new(bare_source(&source_layout, 1.), ValueNode(0.5), &source_layout);
|
||||||
let GPoll::Final(value) = ok.eval(&ctx) else {
|
let GPoll::Final(value) = ok.eval(&ctx) else {
|
||||||
panic!("expected a final record");
|
panic!("expected a final record");
|
||||||
};
|
};
|
||||||
assert_eq!(unsafe { value.rec().read::<f64>(modified.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5);
|
assert_eq!(unsafe { value.rec().read::<f64>(modified.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5);
|
||||||
|
|
||||||
let failing = CheckedMultiplyOpacityNode::wire(bare_source(&source_layout, 1.), ValueNode(-1.), &source_layout);
|
let failing = CheckedMultiplyOpacityNode::new(bare_source(&source_layout, 1.), ValueNode(-1.), &source_layout);
|
||||||
let GPoll::Error(error) = failing.eval(&ctx) else {
|
let GPoll::Error(error) = failing.eval(&ctx) else {
|
||||||
panic!("expected an error");
|
panic!("expected an error");
|
||||||
};
|
};
|
||||||
@@ -384,8 +393,8 @@ mod tests {
|
|||||||
let scaled = scale_layout(&modified);
|
let scaled = scale_layout(&modified);
|
||||||
reserve_for(&[&source_layout, &modified, &scaled]);
|
reserve_for(&[&source_layout, &modified, &scaled]);
|
||||||
|
|
||||||
let chain = ScaleNode::wire(
|
let chain = ScaleNode::new(
|
||||||
MultiplyOpacityNode::wire(bare_source(&source_layout, 2.), ValueNode(0.5), &source_layout),
|
MultiplyOpacityNode::new(bare_source(&source_layout, 2.), ValueNode(0.5), &source_layout),
|
||||||
StaticLendNode(&FACTOR),
|
StaticLendNode(&FACTOR),
|
||||||
&modified,
|
&modified,
|
||||||
);
|
);
|
||||||
@@ -409,8 +418,8 @@ mod tests {
|
|||||||
let relabeled = label_layout(&labeled);
|
let relabeled = label_layout(&labeled);
|
||||||
reserve_for(&[&source_layout, &labeled, &relabeled]);
|
reserve_for(&[&source_layout, &labeled, &relabeled]);
|
||||||
|
|
||||||
let chain = LabelNode::wire(
|
let chain = LabelNode::new(
|
||||||
LabelNode::wire(bare_source(&source_layout, 1.), ValueNode(String::from("a")), &source_layout),
|
LabelNode::new(bare_source(&source_layout, 1.), ValueNode(String::from("a")), &source_layout),
|
||||||
ValueNode(String::from("b")),
|
ValueNode(String::from("b")),
|
||||||
&labeled,
|
&labeled,
|
||||||
);
|
);
|
||||||
@@ -425,7 +434,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn census_fills_reference_defaults_from_static_data() {
|
fn census_fills_reference_defaults_from_static_data() {
|
||||||
let source = f64_layout(&[]);
|
let source = f64_layout(&[]);
|
||||||
let labeled = Layout::default().with_writes(0, (8, 8), &[(Label::NAME, 0, 16, 8)]);
|
let labeled = Layout::default().with_writes(0, (8, 8), &[core_types::record::FieldWrite::of::<Label>(0)]);
|
||||||
|
|
||||||
let plan = core_types::record::SourcePlan::new(&source, &labeled).unwrap();
|
let plan = core_types::record::SourcePlan::new(&source, &labeled).unwrap();
|
||||||
let record = [5f64];
|
let record = [5f64];
|
||||||
@@ -444,6 +453,35 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn record_monitor_forwards_and_captures_for_the_introspection_window() {
|
||||||
|
let mut arena = Arena::new(1024).unwrap();
|
||||||
|
let generations = [];
|
||||||
|
|
||||||
|
let layout = f64_layout(&["opacity"]);
|
||||||
|
reserve_for(&[&layout, &layout]);
|
||||||
|
|
||||||
|
let monitor = core_types::record::RecordMonitor::new(f64_record_source(&layout, 4., vec![(layout.offset_of("opacity", 0).unwrap(), 0.25)]), &layout);
|
||||||
|
{
|
||||||
|
let scope = scope_fixture(&generations, &arena);
|
||||||
|
let ctx = ContextImpl::root(&scope);
|
||||||
|
let GPoll::Final(value) = monitor.eval(&ctx) else {
|
||||||
|
panic!("expected a final record");
|
||||||
|
};
|
||||||
|
assert_eq!(unsafe { value.rec().element::<f64>() }, 4.);
|
||||||
|
}
|
||||||
|
|
||||||
|
let capture = Node::<ContextImpl>::serialize(&monitor).unwrap();
|
||||||
|
let capture = capture.downcast_ref::<core_types::record::RecordCapture>().unwrap();
|
||||||
|
let fields = capture.materialize(&arena).unwrap();
|
||||||
|
assert_eq!(fields.len(), 1);
|
||||||
|
assert_eq!(fields[0].0, "opacity");
|
||||||
|
assert_eq!(*fields[0].1.as_any().downcast_ref::<f64>().unwrap(), 0.25);
|
||||||
|
|
||||||
|
arena.reset();
|
||||||
|
assert!(capture.materialize(&arena).is_none(), "a dead generation materializes to nothing");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn routing_unions_branch_layouts_and_fills_census_defaults() {
|
fn routing_unions_branch_layouts_and_fills_census_defaults() {
|
||||||
let arena = Arena::new(1024).unwrap();
|
let arena = Arena::new(1024).unwrap();
|
||||||
@@ -459,8 +497,8 @@ mod tests {
|
|||||||
let taken = |second: bool| {
|
let taken = |second: bool| {
|
||||||
PickNode::new(
|
PickNode::new(
|
||||||
ValueNode(second),
|
ValueNode(second),
|
||||||
RecordSource::wire(f64_record_source(&layout_a, 1., vec![(layout_a.offset_of("opacity", 0).unwrap(), 0.5)]), &layout_a, &union),
|
RecordSource::new(f64_record_source(&layout_a, 1., vec![(layout_a.offset_of("opacity", 0).unwrap(), 0.5)]), &layout_a, &union),
|
||||||
RecordSource::wire(f64_record_source(&layout_b, 3., vec![(layout_b.offset_of("length", 0).unwrap(), 3.)]), &layout_b, &union),
|
RecordSource::new(f64_record_source(&layout_b, 3., vec![(layout_b.offset_of("length", 0).unwrap(), 3.)]), &layout_b, &union),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -495,8 +533,8 @@ mod tests {
|
|||||||
|
|
||||||
let chain = HoldFirstNode::new(
|
let chain = HoldFirstNode::new(
|
||||||
ValueNode(false),
|
ValueNode(false),
|
||||||
RecordSource::wire(f64_record_source(&layout_a, 1., vec![(layout_a.offset_of("opacity", 0).unwrap(), 0.5)]), &layout_a, &union),
|
RecordSource::new(f64_record_source(&layout_a, 1., vec![(layout_a.offset_of("opacity", 0).unwrap(), 0.5)]), &layout_a, &union),
|
||||||
RecordSource::wire(f64_record_source(&layout_b, 3., vec![(layout_b.offset_of("length", 0).unwrap(), 3.)]), &layout_b, &union),
|
RecordSource::new(f64_record_source(&layout_b, 3., vec![(layout_b.offset_of("length", 0).unwrap(), 3.)]), &layout_b, &union),
|
||||||
);
|
);
|
||||||
|
|
||||||
let GPoll::Final(value) = chain.eval(&ctx) else {
|
let GPoll::Final(value) = chain.eval(&ctx) else {
|
||||||
@@ -520,7 +558,7 @@ mod tests {
|
|||||||
let base = stack::push(0);
|
let base = stack::push(0);
|
||||||
stack::pop(base);
|
stack::pop(base);
|
||||||
|
|
||||||
let chain = ForwardRecordNode::new(RecordSource::wire(
|
let chain = ForwardRecordNode::new(RecordSource::new(
|
||||||
f64_record_source(&layout, 4., vec![(layout.offset_of("opacity", 0).unwrap(), 0.25)]),
|
f64_record_source(&layout, 4., vec![(layout.offset_of("opacity", 0).unwrap(), 0.25)]),
|
||||||
&layout,
|
&layout,
|
||||||
&layout.clone(),
|
&layout.clone(),
|
||||||
@@ -545,7 +583,7 @@ mod tests {
|
|||||||
let layout = f64_layout(&["opacity"]);
|
let layout = f64_layout(&["opacity"]);
|
||||||
reserve_for(&[&layout]);
|
reserve_for(&[&layout]);
|
||||||
|
|
||||||
let chain = ForwardRecordNode::new(RecordSource::wire(
|
let chain = ForwardRecordNode::new(RecordSource::new(
|
||||||
RecordSourceNode {
|
RecordSourceNode {
|
||||||
frame_bytes: frame_bytes(&layout),
|
frame_bytes: frame_bytes(&layout),
|
||||||
element: 4.,
|
element: 4.,
|
||||||
|
|||||||
Reference in New Issue
Block a user