diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 2808614ef0..7d7eebfa3f 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -10,7 +10,6 @@ use graph_craft::proto::GraphErrors; use graphene_std::application_io::{ApplicationIo, ExportFormat, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig, Texture}; use graphene_std::bounds::RenderBoundingBox; use graphene_std::core_types::gpoll::GPoll; -use graphene_std::list::List; use graphene_std::ops::ConvertAsync; #[cfg(all(target_family = "wasm", feature = "gpu", feature = "wasm"))] use graphene_std::platform_application_io::canvas_utils::{Canvas, CanvasSurface, CanvasSurfaceHandle}; @@ -492,44 +491,66 @@ impl NodeRuntime { continue; }; - // Extract the monitor node's stored `Graphic` data - let Ok(introspected_data) = self.executor.introspect(monitor_node_path) else { + // Read the monitored run directly, inside the introspection window + let thumbnail_renders = &mut self.thumbnail_renders; + let vector_modify = &mut self.vector_modify; + let result = self.executor.introspect_with(monitor_node_path, |layout, batch, _arena| { + use graphene_std::core_types::record::{Group, GroupContent, GroupItem, RunView}; + let type_id = layout.element.type_id; + // Graphic run: thumbnail (text-aware bounds, since the `BoundingBox` trait can't lay out `Graphic::Text` content) + if type_id == std::any::TypeId::of::() { + if update_thumbnails { + // SAFETY: the batch is resident for the read. + let item = unsafe { GroupItem::from_resident(batch) }; + let bounds = graphene_std::renderer::graphic_list_bounding_box(&RunView::::new(&item)?, DAffine2::IDENTITY); + let group = Graphic::Group(Group { + row: None, + content: GroupContent::Run(item), + }); + Self::render_thumbnail(thumbnail_renders, parent_network_node_id, &group, bounds, responses) + } + Some(()) + } + // 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 type_id == std::any::TypeId::of::() { + if update_thumbnails { + // SAFETY: the batch is resident for the read. + let item = unsafe { GroupItem::from_resident(batch) }; + let run = RunView::::new(&item)?; + let bounds = artboard_clip_bounds(&run); + Self::render_thumbnail(thumbnail_renders, parent_network_node_id, &run, bounds, responses) + } + Some(()) + } + // Vector run: vector modifications + else if type_id == std::any::TypeId::of::() { + // SAFETY: the batch is resident for the read. + let item = unsafe { GroupItem::from_resident(batch) }; + let run = RunView::::new(&item)?; + use graphene_std::core_types::lane::LaneSource; + vector_modify.insert(parent_network_node_id, run.element(0).cloned().unwrap_or_default()); + Some(()) + } + // String run: thumbnail (bounds need text layout, which the `BoundingBox` trait can't do for a bare `String`) + else if type_id == std::any::TypeId::of::() { + if update_thumbnails { + // SAFETY: the batch is resident for the read. + let item = unsafe { GroupItem::from_resident(batch) }; + let run = RunView::::new(&item)?; + let bounds = graphene_std::renderer::text_list_bounding_box(&run, DAffine2::IDENTITY); + Self::render_thumbnail(thumbnail_renders, parent_network_node_id, &run, bounds, responses) + } + Some(()) + } else { + log::warn!("Failed to read monitor node output {parent_network_node_id:?}"); + Some(()) + } + }); + if result.is_err() { // TODO: Fix the root of the issue causing the spam of this warning (this at least temporarily disables it in release builds) #[cfg(debug_assertions)] - warn!("Failed to introspect monitor node {}", self.executor.introspect(monitor_node_path).unwrap_err()); - continue; - }; - - // Graphic list: thumbnail (text-aware bounds, since the `BoundingBox` trait can't lay out `Graphic::Text` content) - if let Some(list) = introspected_data.downcast_ref::>() { - if update_thumbnails { - let bounds = graphene_std::renderer::graphic_list_bounding_box(list, DAffine2::IDENTITY); - Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, list, bounds, responses) - } - } - // 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(list) = introspected_data.downcast_ref::>() { - if update_thumbnails { - let bounds = artboard_clip_bounds(list); - Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, list, bounds, responses) - } - } - // Vector list: vector modifications - else if let Some(list) = introspected_data.downcast_ref::>() { - // Insert the vector modify - self.vector_modify.insert(parent_network_node_id, list.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(list) = introspected_data.downcast_ref::>() { - if update_thumbnails { - let bounds = graphene_std::renderer::text_list_bounding_box(list, DAffine2::IDENTITY); - Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, list, bounds, responses) - } - } - // Other - else { - log::warn!("Failed to downcast monitor node output {parent_network_node_id:?}"); + warn!("Failed to introspect monitor node {}", result.unwrap_err()); } } } @@ -597,11 +618,12 @@ impl NodeRuntime { /// Returns the union of the artboards' clipping rectangles, used as the thumbnail bounds for an artboard layer so the /// framing matches what's actually visible after clipping rather than the unclipped content extents. -fn artboard_clip_bounds(artboards: &List) -> RenderBoundingBox { +fn artboard_clip_bounds>(artboards: &S) -> RenderBoundingBox { + use graphene_std::core_types::attribute::{Dimensions, Location}; let mut combined: Option<[DVec2; 2]> = None; - for index in 0..artboards.len() { - let location: DVec2 = artboards.attribute_cloned_or_default(graphene_std::ATTR_LOCATION, index); - let dimensions: DVec2 = artboards.attribute_cloned_or_default(graphene_std::ATTR_DIMENSIONS, index); + for index in 0..artboards.lane_count() { + let location: DVec2 = artboards.attr::(index); + let dimensions: DVec2 = artboards.attr::(index); let bounds = [location, location + dimensions]; combined = Some(match combined { Some(existing) => [existing[0].min(bounds[0]), existing[1].max(bounds[1])], diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index 396fac8316..4163ea5147 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -1,6 +1,6 @@ use crate::node_registry; use core_types::arena::Arena; -use core_types::context::{ContextImpl, DynSlot, EvalScope, VarArg, VarArgLink, VarArgSlots}; +use core_types::context::{ContextImpl, DynSlot, EvalScope, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, VarArg, VarArgLink, VarArgSlots}; use core_types::gpoll::GPoll; use core_types::node::Node; use core_types::registry::{EdgeHandle, ErasedNode}; @@ -144,21 +144,76 @@ impl DynamicExecutor { Ok(ResolvedDocumentNodeTypesDelta { add, remove }) } - /// 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 monitor's record capture materializes here against the arena, inside - /// the introspection window, so consumers downcast the legacy value type - /// directly: a rank-0 capture yields its element and a level capture its - /// legacy list. The captured input context stays on the serialized io - /// record for consumers that need it. + /// Calls the `Node::serialize` for that specific node. A monitor serializes + /// its stored context snapshot, and this entry recreates the monitored + /// value from it as the legacy value the editor's downcasts expect: a + /// rank-0 wire yields its element and a leveled wire its legacy list. pub fn introspect(&self, node_path: &[NodeId]) -> Result, IntrospectError> { let result = self.tree.introspect(node_path)?; - if let Some(io) = result.downcast_ref::>() { - let arena = self.arena.lock().unwrap_or_else(PoisonError::into_inner); - return graphic_types::boundary::capture_to_legacy(&io.output, &arena).map(Arc::from).ok_or(IntrospectError::NoData); + if result.downcast_ref::().is_some() { + return self + .introspect_with(node_path, |layout, batch, arena| graphic_types::boundary::batch_to_legacy(layout, batch, arena)) + .map(Arc::from); } Ok(result) } + /// Re-evaluates the monitored edge at `node_path` with its stored context + /// snapshot and hands the resulting resident batch to `read`, inside the + /// introspection window. The monitor stores only the context; the value is + /// recreated against current source data, so a read right after an + /// execution serves out of the warm memo entries. + pub fn introspect_with( + &self, + node_path: &[NodeId], + read: impl FnOnce(&core_types::record::Layout, core_types::node::RecordBatch<'_>, &Arena) -> Option, + ) -> Result { + let serialized = self.tree.introspect(node_path)?; + let Some(snapshot) = serialized.downcast_ref::() else { + return Err(IntrospectError::NoData); + }; + let edge = self + .tree + .get_by_path(node_path) + .and_then(EdgeHandle::record_edge) + .ok_or_else(|| IntrospectError::PathNotFound(node_path.to_vec()))?; + let arena = self.arena.lock().unwrap_or_else(PoisonError::into_inner); + core_types::record::stack::reserve(self.tree.stack_need()); + let generations = self.runtime.snapshot(); + let scope = EvalScope::new(snapshot.try_real_time(), snapshot.try_animation_time(), snapshot.try_pointer_position(), &generations, &arena); + let Some(ctx) = snapshot.rehydrate(&scope) else { + return Err(IntrospectError::NoData); + }; + let layout = core_types::node::Node::::layout(&edge); + let mark = core_types::record::stack::sp(); + let result = if layout.depth > 0 { + match core_types::record::materialize_level(&edge, &ctx, &arena) { + core_types::record::LevelStatus::Batch(batch, _) => read(layout, batch, &arena), + _ => None, + } + } else { + match core_types::node::Node::eval(&edge, &ctx) { + GPoll::Final(value) | GPoll::Partial(value) => { + let rec = layout.rec(&value); + // SAFETY: the eval produced one live record of the edge's layout. + let batch = unsafe { core_types::node::RecordBatch::new(rec.ptr(), 1, layout) }; + read(layout, batch, &arena) + } + GPoll::Fallback(boxed) => { + let (value, _) = *boxed; + let rec = layout.rec(&value); + // SAFETY: as for the final arm. + let batch = unsafe { core_types::node::RecordBatch::new(rec.ptr(), 1, layout) }; + read(layout, batch, &arena) + } + _ => None, + } + }; + // SAFETY: the read finished, so no record above the mark is live. + unsafe { core_types::record::stack::rewind(mark) }; + result.ok_or(IntrospectError::NoData) + } + pub fn input_type(&self) -> Option { self.typing_context.type_of(self.output).map(|node_io| node_io.call_argument.clone()) } @@ -325,6 +380,12 @@ impl BorrowTree { self.nodes.get(&id).map(|(node, _)| node.duplicate()) } + /// The edge handle for the node at a document path. + pub fn get_by_path(&self, node_path: &[NodeId]) -> Option { + let (id, _) = self.source_map.get(node_path)?; + self.get(*id) + } + /// Evaluate a node of the [`BorrowTree`], downcasting its edge to the expected output type. pub fn eval(&self, id: NodeId, input: &I) -> Option> where diff --git a/node-graph/libraries/core-types/src/context.rs b/node-graph/libraries/core-types/src/context.rs index c8bafdbf47..1631833148 100644 --- a/node-graph/libraries/core-types/src/context.rs +++ b/node-graph/libraries/core-types/src/context.rs @@ -1004,6 +1004,54 @@ impl CtxSnapshot { pub fn generations(&self) -> &[(SourceId, u64)] { &self.generations } + + /// Rebuilds the captured context against `scope`, allocating the borrowed + /// chains in the scope's arena; `None` reports arena exhaustion. The + /// scope carries the serving generation, so the recreated evaluation reads + /// current source data under the captured addressing. + pub fn rehydrate<'e>(&'e self, scope: &'e EvalScope<'e>) -> Option> { + let arena = scope.arena(); + let mut ctx = ContextImpl::root(scope); + if let Some(footprint) = self.footprint { + let (footprint, _) = arena.alloc(footprint)?; + ctx = ctx.with_footprint(footprint); + } + if let Some(levels) = &self.index { + let mut outer = None; + for &index in levels.iter().skip(1).rev() { + let (link, _) = arena.alloc(IndexLink { index: index as u64, outer })?; + outer = Some(link); + } + ctx.index = IndexLink { + index: levels.first().copied().unwrap_or_default() as u64, + outer, + }; + } + if let Some(positions) = &self.positions { + let mut head = None; + for &position in positions.iter().rev() { + let (link, _) = arena.alloc(PositionLink { position, outer: head })?; + head = Some(link); + } + if let Some(head) = head { + ctx = ctx.with_position(head); + } + } + let mut varargs = None; + for args in self.varargs.iter().rev() { + let slots: Vec> = args.iter().map(|slot| &**slot as DynSlot<'e>).collect(); + let slots = arena.alloc_slice_copy(&slots)?; + let (link, _) = arena.alloc(VarArgLink { + args: VarArgSlots::Slice(slots), + outer: varargs, + })?; + varargs = Some(link); + } + if let Some(varargs) = varargs { + ctx = ctx.with_varargs(varargs); + } + Some(ctx) + } } impl ExtractFootprint for CtxSnapshot { diff --git a/node-graph/libraries/core-types/src/record.rs b/node-graph/libraries/core-types/src/record.rs index c0d1549378..e6336ac22d 100644 --- a/node-graph/libraries/core-types/src/record.rs +++ b/node-graph/libraries/core-types/src/record.rs @@ -1554,96 +1554,6 @@ pub unsafe fn interrupt_frame(entry: usize, layout: &Layout) { claim_frame(layout); } -/// 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, - lanes: usize, - bytes: crate::arena::ArenaWeak>, -} - -impl std::fmt::Debug for RecordCapture { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("RecordCapture(..)") - } -} - -impl RecordCapture { - /// # Safety - /// `rec` must be a live record of `layout`. - pub unsafe fn capture(layout: &Layout, rec: Rec, arena: &crate::arena::Arena) -> Option { - let bytes = unsafe { copy_record_bytes(layout, rec) }; - arena.alloc(bytes).map(|(_, weak)| RecordCapture { - layout: layout.clone(), - lanes: 1, - bytes: weak, - }) - } - - /// Captures every lane of a leveled wire's batch. - /// - /// # Safety - /// `batch` must hold live records of `layout`. - pub unsafe fn capture_level(layout: &Layout, batch: crate::node::RecordBatch<'_>, arena: &crate::arena::Arena) -> Option { - let stride = layout.lane_stride(); - let mut bytes = vec![0u8; batch.len() * stride].into_boxed_slice(); - for lane in 0..batch.len() { - // SAFETY: both sides hold `len` lanes at the shared layout's stride. - unsafe { std::ptr::copy_nonoverlapping(batch.get(lane).rec().ptr(), bytes.as_mut_ptr().add(lane * stride), stride) }; - } - arena.alloc(bytes).map(|(_, weak)| RecordCapture { - layout: layout.clone(), - lanes: batch.len(), - bytes: weak, - }) - } - - pub fn layout(&self) -> &Layout { - &self.layout - } - - /// The captured lane count: one for a rank-0 capture, the whole extent - /// for a level capture. - pub fn lanes(&self) -> usize { - self.lanes - } - - /// A batch view over the captured records, alive while the arena holds - /// the capture's generation. - pub fn batch<'a>(&'a self, arena: &'a crate::arena::Arena) -> Option> { - let bytes = self.bytes.upgrade(arena)?; - // SAFETY: the constructors store `lanes` records of `layout`. - Some(unsafe { crate::node::RecordBatch::new(bytes.as_ptr(), self.lanes, &self.layout) }) - } - - /// The captured element of the first lane, cloned out through the - /// layout's erased glue. - pub fn materialize_element(&self, arena: &crate::arena::Arena) -> Option> { - let bytes = self.bytes.upgrade(arena)?; - match self.lanes { - 0 => None, - _ => Some(unsafe { (self.layout.element.clone_out)(bytes.as_ptr()) }), - } - } - - /// The first lane's attributes, read out through the layout's erased glue. - pub fn materialize(&self, arena: &crate::arena::Arena) -> Option)>> { - let bytes = self.bytes.upgrade(arena)?; - if self.lanes == 0 { - return Some(Vec::new()); - } - Some( - self.layout - .fields - .iter() - .map(|field| (field.name, unsafe { (field.read_erased)(bytes.as_ptr().add(field.offset)) })) - .collect(), - ) - } -} - /// A record deep-copied out of its evaluation: the packed bytes plus owned /// clones of every parked payload, replayable into a later evaluation's /// storage through the layout's erased glue. The layout stays with the @@ -2088,6 +1998,13 @@ impl<'a, T: 'static> crate::lane::LaneSource for RunView<'a, T> { } } +impl crate::render_complexity::RenderComplexity for RunView<'_, T> { + fn render_complexity(&self) -> usize { + use crate::lane::LaneSource; + (0..self.lane_count()).filter_map(|lane| self.element(lane)).map(crate::render_complexity::RenderComplexity::render_complexity).sum() + } +} + impl crate::bounds::BoundingBox for RunView<'_, T> { fn bounding_box(&self, transform: glam::DAffine2, include_stroke: bool) -> crate::bounds::RenderBoundingBox { crate::bounds::lane_bounding_box(self, transform, include_stroke) diff --git a/node-graph/libraries/core-types/src/registry.rs b/node-graph/libraries/core-types/src/registry.rs index d78b1c7adb..35993a7ce0 100644 --- a/node-graph/libraries/core-types/src/registry.rs +++ b/node-graph/libraries/core-types/src/registry.rs @@ -303,6 +303,12 @@ impl EdgeHandle { self.downcast_erased(record_edge_type::()) } + /// The erased record edge, for callers that dispatch on the layout rather + /// than a static element type. `None` for a plain (non-record) edge. + pub fn record_edge(self) -> Option> { + self.node.downcast::>().ok().map(|edge| *edge) + } + pub fn downcast_erased(self, expected: Type) -> Result, ConstructionError> { let found = self.ty; self.node.downcast::>().map(|edge| *edge).map_err(|_| ConstructionError::Type { diff --git a/node-graph/libraries/graphic-types/src/boundary.rs b/node-graph/libraries/graphic-types/src/boundary.rs index 8c695e28ac..dc9ef5ae01 100644 --- a/node-graph/libraries/graphic-types/src/boundary.rs +++ b/node-graph/libraries/graphic-types/src/boundary.rs @@ -10,7 +10,7 @@ use core_types::arena::Arena; use core_types::context::InjectIndex; use core_types::gpoll::{Finality, GraphError}; use core_types::node::Node; -use core_types::record::{Group, GroupContent, GroupItem, LevelStatus, RecordCapture, RecordValue, materialize_level}; +use core_types::record::{Group, GroupContent, GroupItem, LevelStatus, RecordValue, materialize_level}; use core_types::uuid::NodeId; use glam::{DAffine2, DVec2}; use vector_types::GradientStops; @@ -46,36 +46,35 @@ where } } -/// The captured wire as the legacy value the editor's downcasts expect: a -/// rank-0 capture is its element, a leveled `Graphic` capture becomes its -/// legacy list through the group bridge, and another element type becomes a -/// legacy list of that element. `None` for an element type outside the -/// legacy vocabulary or a capture whose arena generation has passed. -pub fn capture_to_legacy(capture: &RecordCapture, arena: &Arena) -> Option> { - if capture.layout().depth == 0 { - // The group-carrying types legacy-convert while the capture is still +/// The resident batch as the legacy value the editor's downcasts expect: a +/// rank-0 wire is its element, a leveled `Graphic` wire becomes its legacy +/// list through the group bridge, and another element type becomes a legacy +/// list of that element. `None` for an element type outside the legacy +/// vocabulary or an empty rank-0 batch. +pub fn batch_to_legacy(layout: &core_types::record::Layout, batch: core_types::node::RecordBatch<'_>, _arena: &Arena) -> Option> { + if layout.depth == 0 { + // The group-carrying types legacy-convert while the batch is // resident; the deep clone-out would hand back the unreadable owned // form. - if capture.lanes() > 0 { - let element = &capture.layout().element; - if element.type_id == std::any::TypeId::of::() { - let batch = capture.batch(arena)?; - // SAFETY: the layout records the element type, and a parked - // element stores its reference at offset 0. - let graphic = unsafe { core_types::record::borrow_element::(batch.get(0).rec()) }; - return Some(Box::new(crate::graphic::map_groups_to_legacy(graphic))); - } - if element.type_id == std::any::TypeId::of::() { - let batch = capture.batch(arena)?; - // SAFETY: as for the graphic arm. - let artboard = unsafe { core_types::record::borrow_element::(batch.get(0).rec()) }; - return Some(Box::new(artboard.with_legacy_groups())); - } + if batch.is_empty() { + return None; } - return capture.materialize_element(arena); + let element = &layout.element; + if element.type_id == std::any::TypeId::of::() { + // SAFETY: the layout records the element type, and a parked + // element stores its reference at offset 0. + let graphic = unsafe { core_types::record::borrow_element::(batch.get(0).rec()) }; + return Some(Box::new(crate::graphic::map_groups_to_legacy(graphic))); + } + if element.type_id == std::any::TypeId::of::() { + // SAFETY: as for the graphic arm. + let artboard = unsafe { core_types::record::borrow_element::(batch.get(0).rec()) }; + return Some(Box::new(artboard.with_legacy_groups())); + } + // SAFETY: lane 0 is a live record of `layout`. + return Some(unsafe { (element.clone_out)(batch.get(0).rec().ptr()) }); } - let batch = capture.batch(arena)?; - // SAFETY: the captured bytes live in the arena for the capture's generation. + // SAFETY: the caller's batch is resident for the read. let item = unsafe { GroupItem::from_resident(batch) }; fn typed(item: &GroupItem) -> Option> { run_to_legacy_list::(item).map(|list| Box::new(list) as Box) diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index ed658d0d81..df4c97be79 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -2966,6 +2966,46 @@ impl Render for List { } } +impl Render for RunView<'_, Artboard> { + fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { + render_artboard_svg(self, render, render_params) + } + + fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { + render_artboard_vello(self, scene, transform, context, render_params) + } + + fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option) { + collect_artboard_metadata(self, metadata, footprint) + } + + fn add_upstream_click_targets(&self, click_targets: &mut Vec) { + add_artboard_upstream_click_targets(self, click_targets) + } + + fn contains_artboard(&self) -> bool { + self.lane_count() > 0 + } +} + +impl Render for RunView<'_, String> { + fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { + render_text_svg(self, render, render_params) + } + + fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) { + render_text_vello(self, scene, transform, render_params) + } + + fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option) { + collect_text_metadata(self, metadata, footprint, caller_element_id) + } + + fn add_upstream_click_targets(&self, click_targets: &mut Vec) { + add_text_upstream_click_targets(self, click_targets) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum SvgSegment { Slice(&'static str), diff --git a/node-graph/nodes/gcore/src/memo.rs b/node-graph/nodes/gcore/src/memo.rs index 1109277c1e..feab498ad1 100644 --- a/node-graph/nodes/gcore/src/memo.rs +++ b/node-graph/nodes/gcore/src/memo.rs @@ -3,8 +3,7 @@ use core_types::context::{Ctx, CtxSnapshot, DeriveCtx, ExtractAll, ModifyIndex}; use core_types::frame_table::{FrameTable, Lookup}; use core_types::gpoll::{Finality, GPoll}; use core_types::graphene_hash::CacheHash; -use core_types::memo::IORecord; -use core_types::record::{LevelStatus, OwnedRecord, RecordCapture, RecordValue, claim_frame, copy_record_bytes, serve_frame}; +use core_types::record::{LevelStatus, OwnedRecord, RecordValue, claim_frame, copy_record_bytes, serve_frame}; use core_types::registry::cache_key; use std::sync::Arc; use std::sync::Mutex; @@ -183,70 +182,27 @@ fn frame_memo<'e>( } } -type MonitorValue = Arc>>>; +type MonitorValue = Arc>>; -/// The Monitor node is used by the editor to access the data flowing through it. +/// The Monitor node is used by the editor to access the data flowing through +/// it. It stores only the evaluation context: the output is pure over +/// (context, source generations), so introspection recreates it by +/// re-evaluating this edge with the rehydrated snapshot. #[node_macro::node(category(""), path(graphene_core::memo), serialize(serialize_monitor), properties("monitor_properties"))] fn monitor<'e>( ctx: impl Ctx + DeriveCtx + ExtractAll + ExtractArena<'e> + ModifyIndex + Copy, #[data] io: MonitorValue, content: impl Node, Output = RecordValue<'e>>, ) -> GPoll> { - let entry_sp = core_types::record::stack::sp(); - let publish = |captured: Option| { - *io.lock().unwrap() = captured.map(|output| IORecord { - input: CtxSnapshot::capture(ctx), - output, - }); - }; - // A leveled capture covers the whole extent, which the materialization below - // computes lane by lane. Serving THIS lane out of that batch rather than - // evaluating the content separately is what keeps the cost linear: the extra - // eval would double the work under every enclosing monitor. - if content.layout().depth > 0 && ctx.index() == 0 { - return match content.materialize_level(ctx, ctx.arena()) { - LevelStatus::Batch(batch, finality) => { - // SAFETY: the batch came from this edge, so it carries the edge's layout. - publish(unsafe { RecordCapture::capture_level(content.layout(), batch, ctx.arena()) }); - let Some(lane) = (!batch.is_empty()).then(|| batch.get(0)) else { - // An empty level ends here; the past-end signal serves drains. - claim_frame(content.layout()); - return GPoll::Error(Box::new(core_types::gpoll::GraphError::past_end())); - }; - // SAFETY: the lane is a live record of this edge's layout. - let value = unsafe { serve_frame(content.layout(), lane.rec().ptr()) }; - match finality { - Finality::AllFinal => GPoll::Final(value), - Finality::Partial => GPoll::Partial(value), - } - } - // A valueless materialization leaves frames no one reads, so the - // entry mark, not the current top, is what this node's frame sits on. - LevelStatus::Pending => { - // SAFETY: nothing borrows the frames above the entry mark. - unsafe { core_types::record::interrupt_frame(entry_sp, content.layout()) }; - GPoll::Pending - } - LevelStatus::Error(error) => { - // SAFETY: nothing borrows the frames above the entry mark. - unsafe { core_types::record::interrupt_frame(entry_sp, content.layout()) }; - GPoll::Error(Box::new(error)) - } - }; + if ctx.index() == 0 { + *io.lock().unwrap() = Some(CtxSnapshot::capture(ctx)); } - let result = content.eval(&ctx); - if ctx.index() == 0 - && let GPoll::Final(value) | GPoll::Partial(value) = &result - { - // SAFETY: the value came from this edge, so it carries the edge's layout. - publish(unsafe { RecordCapture::capture(content.layout(), content.layout().rec(value), ctx.arena()) }); - } - result + content.eval(&ctx) } fn serialize_monitor(io: &MonitorValue) -> Option> { let io = io.lock().unwrap(); - io.as_ref().map(|io| Arc::new(io.clone()) as Arc) + io.as_ref().map(|snapshot| Arc::new(snapshot.clone()) as Arc) } #[cfg(test)] @@ -299,7 +255,7 @@ mod tests { } #[test] - fn monitor_serialize_exposes_the_capture_through_the_edge() { + fn monitor_serialize_recreates_the_value_from_its_snapshot() { let arena = Arena::new(1024).unwrap(); let generations = []; let scope = scope_fixture(&generations, &arena); @@ -308,25 +264,25 @@ mod tests { let layout = element_layout::(); let monitor = MonitorNode::new(core_types::record::RecordLift::::new(ValueNode(11u32)), &layout); let handle = EdgeHandle::new_record::(Arc::new(monitor) as Arc); - assert!(handle.serialize().is_none(), "no capture before the first eval"); + assert!(handle.serialize().is_none(), "no snapshot before the first eval"); let edge = handle.duplicate().downcast_record::().unwrap(); let GPoll::Final(_) = edge.eval(&ctx) else { panic!("expected a final record"); }; - 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); + let io = handle.serialize().expect("the eval landed a snapshot"); + let snapshot = io.downcast_ref::().expect("the monitor serializes its context snapshot"); + let ctx = snapshot.rehydrate(&scope).expect("the arena holds the chains"); + let GPoll::Final(value) = edge.eval(&ctx) else { + panic!("expected a final record"); + }; + // SAFETY: the eval produced a live record of the edge's layout. + assert_eq!(unsafe { layout.rec(&value).element::() }, 11); } #[test] - fn a_leveled_monitor_captures_the_whole_extent() { + fn a_leveled_monitor_recreates_the_whole_extent() { let arena = Arena::new(1 << 12).unwrap(); let generations = []; let scope = scope_fixture(&generations, &arena); @@ -342,10 +298,13 @@ mod tests { panic!("expected a final record"); }; - let io = handle.serialize().expect("the eval landed a capture"); - let io = io.downcast_ref::>().expect("the capture is the monitor io"); - assert_eq!(io.output.lanes(), 3, "the capture holds the whole extent, not the addressed lane"); - let batch = io.output.batch(&arena).expect("the capture lives in this generation"); + let io = handle.serialize().expect("the eval landed a snapshot"); + let snapshot = io.downcast_ref::().expect("the monitor serializes its context snapshot"); + let ctx = snapshot.rehydrate(&scope).expect("the arena holds the chains"); + let LevelStatus::Batch(batch, _) = core_types::record::materialize_level(&edge, &ctx, &arena) else { + panic!("expected a materialized level"); + }; + assert_eq!(batch.len(), 3, "the recreation holds the whole extent, not the addressed lane"); let lanes = unsafe { core_types::node::List::::new(batch) }; let values: Vec = (0..lanes.len()).map(|lane| *lanes.element_ref(lane)).collect(); assert_eq!(values, vec![10, 20, 30]); diff --git a/node-graph/nodes/gcore/src/record.rs b/node-graph/nodes/gcore/src/record.rs index 74b454191e..f46629733d 100644 --- a/node-graph/nodes/gcore/src/record.rs +++ b/node-graph/nodes/gcore/src/record.rs @@ -2576,16 +2576,16 @@ mod tests { } #[test] - fn record_monitor_forwards_and_captures_for_the_introspection_window() { - let mut arena = Arena::new(1024).unwrap(); + fn record_monitor_forwards_and_recreates_from_its_snapshot() { + let arena = Arena::new(1024).unwrap(); let generations = []; let layout = f64_layout(&["opacity"]); reserve_for(&[&layout, &layout]); let monitor = crate::memo::MonitorNode::new(f64_record_source(&layout, 4., vec![(layout.offset_of("opacity", 0).unwrap(), 0.25)]), &layout); + let scope = scope_fixture(&generations, &arena); { - let scope = scope_fixture(&generations, &arena); let ctx = ContextImpl::root(&scope); let GPoll::Final(value) = monitor.eval(&ctx) else { panic!("expected a final record"); @@ -2594,16 +2594,14 @@ mod tests { } 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!(io.output.materialize(&arena).is_none(), "a dead generation materializes to nothing"); + let snapshot = io.downcast_ref::().expect("the monitor serializes its context snapshot"); + let ctx = snapshot.rehydrate(&scope).expect("the arena holds the chains"); + let GPoll::Final(value) = monitor.eval(&ctx) else { + panic!("expected a final record"); + }; + let rec = layout.rec(&value); + assert_eq!(unsafe { rec.element::() }, 4.); + assert_eq!(unsafe { rec.read::(layout.offset_of("opacity", 0).unwrap()) }, 0.25); } #[test] diff --git a/node-graph/nodes/graphic/src/record.rs b/node-graph/nodes/graphic/src/record.rs index ac95a2dba6..6b0c4aa5f6 100644 --- a/node-graph/nodes/graphic/src/record.rs +++ b/node-graph/nodes/graphic/src/record.rs @@ -684,7 +684,7 @@ mod tests { } #[test] - fn a_level_capture_converts_to_its_legacy_list() { + fn a_level_batch_converts_to_its_legacy_list() { let arena = Arena::new(1 << 16).unwrap(); let generations = []; let scope = scope_fixture(&generations, &arena); @@ -695,8 +695,7 @@ mod tests { let record::LevelStatus::Batch(batch, _) = record::materialize_level(&source, &ctx, &arena) else { panic!("expected a batch"); }; - let capture = unsafe { record::RecordCapture::capture_level(&layout, batch, &arena) }.expect("the capture parks in the arena"); - let legacy = graphic_types::boundary::capture_to_legacy(&capture, &arena).expect("f64 is in the legacy vocabulary"); + let legacy = graphic_types::boundary::batch_to_legacy(&layout, batch, &arena).expect("f64 is in the legacy vocabulary"); let list = legacy.downcast_ref::>().unwrap(); assert_eq!(list.len(), 2); assert_eq!(list.element(0).copied(), Some(1.5));