diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index acc203385f..dd3628e794 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -145,14 +145,15 @@ 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. - /// A record capture materializes its element here against the arena, - /// inside the introspection window, so consumers downcast the element - /// type directly. + /// A monitor's record capture materializes its element here against the + /// arena, inside the introspection window, so consumers downcast the + /// element type directly. The captured input context stays on the + /// serialized io record for consumers that need it. pub fn introspect(&self, node_path: &[NodeId]) -> Result, IntrospectError> { let result = self.tree.introspect(node_path)?; - if let Some(capture) = result.downcast_ref::() { + if let Some(io) = result.downcast_ref::>() { let arena = self.arena.lock().unwrap_or_else(PoisonError::into_inner); - return capture.materialize_element(&arena).map(Arc::from).ok_or(IntrospectError::NoData); + return io.output.materialize_element(&arena).map(Arc::from).ok_or(IntrospectError::NoData); } Ok(result) } diff --git a/node-graph/libraries/core-types/src/memo.rs b/node-graph/libraries/core-types/src/memo.rs index 64cff1a124..80766fc860 100644 --- a/node-graph/libraries/core-types/src/memo.rs +++ b/node-graph/libraries/core-types/src/memo.rs @@ -4,6 +4,13 @@ use std::hash::{Hash, Hasher}; use std::ops::Deref; use std::sync::Arc; +/// Stores both what a node was called with and what it returned. +#[derive(Clone, Debug)] +pub struct IORecord { + pub input: I, + pub output: O, +} + #[derive(Clone, Debug)] pub struct MemoHash { hash: u64, diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 690931f980..74cf24a59e 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -1042,6 +1042,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn quote!(#node_generic: #bound) } }, + ParsedFieldType::Node(NodeParsedField { output_type, .. }) if opaque && is_record_value(output_type) => { + quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #output_type>) + } ParsedFieldType::Node(NodeParsedField { output_type, .. }) => { let bound = lazy_bound(output_type); quote!(#node_generic: #bound) diff --git a/node-graph/nodes/gcore/src/memo.rs b/node-graph/nodes/gcore/src/memo.rs index 5ae8e7dbc4..3223b335b8 100644 --- a/node-graph/nodes/gcore/src/memo.rs +++ b/node-graph/nodes/gcore/src/memo.rs @@ -1,8 +1,9 @@ use core_types::arena::{Arena, ArenaCell}; -use core_types::context::{Ctx, ExtractArena}; +use core_types::context::{Ctx, CtxSnapshot, DeriveCtx, ExtractAll, ExtractArena}; use core_types::frame_table::{FrameTable, Lookup}; use core_types::gpoll::{Extent, Finality, GPoll}; use core_types::graphene_hash::CacheHash; +use core_types::memo::IORecord; use core_types::node::Node; use core_types::record::{OwnedRecord, RecordCapture, RecordValue}; use core_types::registry::cache_key; @@ -119,23 +120,26 @@ fn lend<'e, T: Send + Sync>(ctx: impl Ctx + ExtractArena<'e>, value: T) -> GPoll park(ctx.arena(), GPoll::Final(value)) } -type MonitorValue = Arc>>; +type MonitorValue = Arc>>>; /// The Monitor node is used by the editor to access the data flowing through it. #[node_macro::node(category(""), path(graphene_core::memo), serialize(serialize_monitor), properties("monitor_properties"))] -fn monitor<'e>(ctx: impl Ctx + ExtractArena<'e>, #[data] capture: MonitorValue, content: impl Node, Output = RecordValue<'e>>) -> GPoll> { +fn monitor<'e>(ctx: impl Ctx + DeriveCtx + ExtractAll + ExtractArena<'e>, #[data] io: MonitorValue, content: impl Node, Output = RecordValue<'e>>) -> GPoll> { let result = content.eval(&ctx); if let GPoll::Final(value) | GPoll::Partial(value) = &result { // SAFETY: the value came from this edge, so it carries the edge's layout. let captured = unsafe { RecordCapture::capture(content.layout(), content.layout().rec(value), ctx.arena()) }; - *capture.lock().unwrap() = captured; + *io.lock().unwrap() = captured.map(|output| IORecord { + input: CtxSnapshot::capture(ctx), + output, + }); } result } -fn serialize_monitor(capture: &MonitorValue) -> Option> { - let capture = capture.lock().unwrap(); - capture.as_ref().map(|capture| Arc::new(capture.clone()) as Arc) +fn serialize_monitor(io: &MonitorValue) -> Option> { + let io = io.lock().unwrap(); + io.as_ref().map(|io| Arc::new(io.clone()) as Arc) } #[cfg(test)] @@ -201,9 +205,13 @@ mod tests { panic!("expected a final record"); }; - let capture = handle.serialize().expect("the eval landed a capture"); - let capture = capture.downcast_ref::().expect("the capture is a record capture"); - let element = capture.materialize_element(&arena).expect("the capture materializes inside the window"); + let io = handle.serialize().expect("the eval landed a capture"); + let io = io.downcast_ref::>().expect("the capture is the monitor io"); + assert!( + core_types::context::ExtractFootprint::try_footprint(&io.input).is_none(), + "the root context has no footprint to capture" + ); + let element = io.output.materialize_element(&arena).expect("the capture materializes inside the window"); assert_eq!(*element.downcast_ref::().unwrap(), 11); } diff --git a/node-graph/nodes/gcore/src/record.rs b/node-graph/nodes/gcore/src/record.rs index 22680d64f7..b63f5e99c4 100644 --- a/node-graph/nodes/gcore/src/record.rs +++ b/node-graph/nodes/gcore/src/record.rs @@ -474,15 +474,17 @@ mod tests { assert_eq!(unsafe { layout.rec(&value).element::() }, 4.); } - let capture = Node::::serialize(&monitor).unwrap(); - let capture = capture.downcast_ref::().unwrap(); - let fields = capture.materialize(&arena).unwrap(); + let io = Node::::serialize(&monitor).unwrap(); + let io = io + .downcast_ref::>() + .unwrap(); + let fields = io.output.materialize(&arena).unwrap(); assert_eq!(fields.len(), 1); assert_eq!(fields[0].0, "opacity"); assert_eq!(*fields[0].1.as_any().downcast_ref::().unwrap(), 0.25); arena.reset(); - assert!(capture.materialize(&arena).is_none(), "a dead generation materializes to nothing"); + assert!(io.output.materialize(&arena).is_none(), "a dead generation materializes to nothing"); } #[test]