From b70b28bbe30cfc9c3e6b38969ca97c7fa199830d Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Thu, 30 Jul 2026 10:15:51 +0000 Subject: [PATCH] Restore serialize-based introspection through the typed edges --- .../data_panel/data_panel_message_handler.rs | 6 +++--- editor/src/node_graph_executor.rs | 8 ++++---- editor/src/node_graph_executor/runtime.rs | 10 +++++----- .../src/dynamic_executor.rs | 6 +++--- node-graph/libraries/core-types/src/gnode.rs | 5 +++++ .../libraries/core-types/src/registry.rs | 13 ++++++++++++ node-graph/node-macro/src/gcodegen.rs | 14 +++++++++++++ node-graph/nodes/gcore/src/memo.rs | 20 ++++++++++++++++++- 8 files changed, 66 insertions(+), 16 deletions(-) diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index c66d3f9810..1912f5df42 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -14,7 +14,7 @@ use graphene_std::memo::IORecord; use graphene_std::raster_types::{CPU, GPU, Raster}; use graphene_std::vector::Vector; use graphene_std::vector::style::{FillChoice, FillChoiceUI, GradientSpreadMethod, GradientType}; -use graphene_std::{Artboard, Color, Context, Graphic}; +use graphene_std::{Artboard, Color, CtxSnapshot, Graphic}; use std::any::Any; use std::sync::Arc; @@ -167,7 +167,7 @@ macro_rules! generate_layout_downcast { ($introspected_data:expr, $data:expr, [ $($ty:ty),* $(,)? ]) => { if false { None } $( - else if let Some(io) = $introspected_data.downcast_ref::>() { + else if let Some(io) = $introspected_data.downcast_ref::>() { Some(io.output.layout_with_breadcrumb($data)) } )* @@ -178,7 +178,7 @@ macro_rules! generate_layout_downcast { fn generate_layout(introspected_data: &Arc, data: &mut LayoutData) -> Option> { // `List` is interpreted as a path (e.g. the value produced by `path_of_subgraph`), shown as a // `List` where each item's NodeId resolves against the prefix made up of the items above it. - if let Some(io) = introspected_data.downcast_ref::>>() { + if let Some(io) = introspected_data.downcast_ref::>>() { return Some(table_node_id_path_layout_with_breadcrumb(&io.output, data)); } generate_layout_downcast!(introspected_data, data, [ diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 18e7b77a20..94642736ab 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -15,7 +15,7 @@ use graphene_std::raster::{CPU, Raster}; use graphene_std::renderer::{RenderMetadata, graphic_list_bounding_box}; use graphene_std::transform::Footprint; use graphene_std::vector::{Vector, graphic_types}; -use graphene_std::{ATTR_TRANSFORM, Context, Graphic, NodeInputDecleration}; +use graphene_std::{ATTR_TRANSFORM, CtxSnapshot, Graphic, NodeInputDecleration}; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta; use std::any::Any; use std::sync::Arc; @@ -892,7 +892,7 @@ fn introspected_output(data: &Arc>() { return Some(io.output.clone()); } - if let Some(io) = data.downcast_ref::>() { + if let Some(io) = data.downcast_ref::>() { return Some(io.output.clone()); } None @@ -911,7 +911,7 @@ mod test { use crate::test_utils::test_prelude::{self, NodeGraphLayer}; use graph_craft::ProtoNodeIdentifier; use graph_craft::document::NodeNetwork; - use graphene_std::Context; + use graphene_std::CtxSnapshot; use graphene_std::NodeInputDecleration; use graphene_std::memo::IORecord; use test_prelude::LayerNodeIdentifier; @@ -979,7 +979,7 @@ mod test { Some(x.output.clone()) } else if let Some(x) = dynamic.downcast_ref::>() { Some(x.output.clone()) - } else if let Some(x) = dynamic.downcast_ref::>() { + } else if let Some(x) = dynamic.downcast_ref::>() { Some(x.output.clone()) } else { warn!("cannot downcast type for introspection"); diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 2b66800c63..088e7513bb 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -19,7 +19,7 @@ use graphene_std::renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRend use graphene_std::transform::RenderQuality; use graphene_std::vector::Vector; use graphene_std::vector::style::RenderMode; -use graphene_std::{Artboard, Context, Graphic}; +use graphene_std::{Artboard, CtxSnapshot, Graphic}; use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta}; use interpreted_executor::util::wrap_network_in_scope; use spin::Mutex; @@ -413,7 +413,7 @@ impl NodeRuntime { }; // Graphic list: thumbnail (text-aware bounds, since the `BoundingBox` trait can't lay out `Graphic::Text` content) - if let Some(io) = introspected_data.downcast_ref::>>() { + if let Some(io) = introspected_data.downcast_ref::>>() { if update_thumbnails { let bounds = graphene_std::renderer::graphic_list_bounding_box(&io.output, DAffine2::IDENTITY); Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, bounds, responses) @@ -421,19 +421,19 @@ impl NodeRuntime { } // Artboard thumbnail bounds come from the clipping rectangles, not the content union, since the renderer // clips content to those rectangles so anything outside isn't visible - else if let Some(io) = introspected_data.downcast_ref::>>() { + else if let Some(io) = introspected_data.downcast_ref::>>() { if update_thumbnails { let bounds = artboard_clip_bounds(&io.output); Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, bounds, responses) } } // Vector list: vector modifications - else if let Some(io) = introspected_data.downcast_ref::>>() { + else if let Some(io) = introspected_data.downcast_ref::>>() { // Insert the vector modify self.vector_modify.insert(parent_network_node_id, io.output.element(0).cloned().unwrap_or_default()); } // String list: thumbnail (bounds need text layout, which the `BoundingBox` trait can't do for a bare `String`) - else if let Some(io) = introspected_data.downcast_ref::>>() { + else if let Some(io) = introspected_data.downcast_ref::>>() { if update_thumbnails { let bounds = graphene_std::renderer::text_list_bounding_box(&io.output, DAffine2::IDENTITY); Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, bounds, responses) diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index 48c65b0933..2f904676a5 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -282,11 +282,11 @@ impl BorrowTree { self.nodes.insert(id, (node, path)); } - /// Returns the introspection record for that specific node, for example the cached value for a monitor node. The node path must match the document node path. + /// Calls the `GNode::serialize` for that specific node, returning for example the captured io record for a monitor node. The node path must match the document node path. pub fn introspect(&self, node_path: &[NodeId]) -> Result, IntrospectError> { let (id, _) = self.source_map.get(node_path).ok_or_else(|| IntrospectError::PathNotFound(node_path.to_vec()))?; - let (_node, _path) = self.nodes.get(id).ok_or(IntrospectError::ProtoNodeNotFound(*id))?; - Err(IntrospectError::NoData) + let (node, _path) = self.nodes.get(id).ok_or(IntrospectError::ProtoNodeNotFound(*id))?; + node.serialize().ok_or(IntrospectError::NoData) } pub fn get(&self, id: NodeId) -> Option { diff --git a/node-graph/libraries/core-types/src/gnode.rs b/node-graph/libraries/core-types/src/gnode.rs index 934701b34a..870e5fdc22 100644 --- a/node-graph/libraries/core-types/src/gnode.rs +++ b/node-graph/libraries/core-types/src/gnode.rs @@ -31,6 +31,11 @@ pub trait GNode { GPoll::Final(Extent::Free) } + /// Introspection access to node-resident records, for example the monitor's captured io; `None` for ordinary nodes. + fn serialize(&self) -> Option> { + None + } + fn eval_batch<'a>(&self, input: &'a Input, range: Range, scratch: Option<&'a mut [MaybeUninit]>) -> BatchStatus<'a, Self::Output> where Input: InjectIndex + Copy, diff --git a/node-graph/libraries/core-types/src/registry.rs b/node-graph/libraries/core-types/src/registry.rs index 2fdb0013d4..91602d2710 100644 --- a/node-graph/libraries/core-types/src/registry.rs +++ b/node-graph/libraries/core-types/src/registry.rs @@ -148,6 +148,11 @@ where unsafe { self.ptr.as_ref() }.extent(input) } + fn serialize(&self) -> Option> { + // SAFETY: as in eval. + unsafe { self.ptr.as_ref() }.serialize() + } + fn eval_batch<'a>( &self, input: &'a Input, @@ -165,6 +170,7 @@ where pub struct EdgeHandle { node: Box, share: fn(&DynEdge) -> Box, + serialize: fn(&DynEdge) -> Option>, ty: Type, } @@ -185,11 +191,13 @@ impl EdgeHandle { pub fn new_erased(node: std::sync::Arc, ty: Type) -> Self where + N: for<'c> GNode>, SharedEdge: WasmNotSend + WasmNotSync, { Self { node: Box::new(SharedEdge::new(node)), share: |edge| Box::new(edge.downcast_ref::>().expect("share hook matches the stored edge type").share()), + serialize: |edge| GNode::::serialize(edge.downcast_ref::>().expect("serialize hook matches the stored edge type")), ty, } } @@ -202,10 +210,15 @@ impl EdgeHandle { Self { node: (self.share)(&*self.node), share: self.share, + serialize: self.serialize, ty: self.ty.clone(), } } + pub fn serialize(&self) -> Option> { + (self.serialize)(&*self.node) + } + pub fn downcast(self) -> Result>, ConstructionError> { self.downcast_erased(edge_type::()) } diff --git a/node-graph/node-macro/src/gcodegen.rs b/node-graph/node-macro/src/gcodegen.rs index f6bc80f91c..a3d4a60012 100644 --- a/node-graph/node-macro/src/gcodegen.rs +++ b/node-graph/node-macro/src/gcodegen.rs @@ -234,6 +234,18 @@ pub(crate) fn generate_gnode_code(crate_ident: &CrateIdent, parsed: &ParsedNodeF } }; + let serialize_impl = match &parsed.attributes.serialize { + Some(path) => { + let data_refs = data_names.iter().map(|name| quote!(&self.#name)); + quote! { + fn serialize(&self) -> Option<::std::sync::Arc> { + #path(#(#data_refs),*) + } + } + } + None => quote!(), + }; + let batch_impl = match &parsed.attributes.batch { Some(path) => quote! { fn eval_batch<'__batch>( @@ -432,6 +444,8 @@ pub(crate) fn generate_gnode_code(crate_ident: &CrateIdent, parsed: &ParsedNodeF #extent_impl + #serialize_impl + #batch_impl } }; diff --git a/node-graph/nodes/gcore/src/memo.rs b/node-graph/nodes/gcore/src/memo.rs index 472b06ecaa..9095f30463 100644 --- a/node-graph/nodes/gcore/src/memo.rs +++ b/node-graph/nodes/gcore/src/memo.rs @@ -170,6 +170,24 @@ mod tests { EvalScope::new(Some(0.5), None, None, generations, arena) } + #[test] + fn monitor_serialize_exposes_the_io_record_through_the_edge() { + let arena = Arena::new(1024); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let handle = EdgeHandle::new(Arc::new(MonitorNode::new(ValueNode(11u32))) as Arc>); + assert!(handle.serialize().is_none(), "no record before the first eval"); + + let edge = handle.duplicate().downcast::().unwrap(); + assert_eq!(edge.eval(&ctx), GPoll::Final(11)); + + let record = handle.serialize().expect("the eval landed a record"); + let record = record.downcast_ref::>().expect("the record is the monitor io"); + assert_eq!(record.output, 11); + } + #[test] fn memoize_caches_across_evals() { let arena = Arena::new(1024); @@ -236,7 +254,7 @@ mod tests { let edge = EdgeHandle::new(Arc::new(ValueNode("lent out".to_string())) as Arc>); let lending = EdgeHandle::new_ref(Arc::new(FrameMemoNode::new(edge.downcast::().unwrap())) as Arc>); - assert_eq!(*lending.ty(), Type::Ref(Box::new(concrete!(String)))); + assert_eq!(*lending.ty(), core_types::registry::lend_edge_type::()); let node = lending.downcast_lend::().unwrap(); let GPoll::Final(first) = node.eval(&ctx) else {