Define the monitor through the record-opaque class and move introspection to element reads

This commit is contained in:
Dennis Kobert
2026-08-06 09:07:57 +00:00
parent a2de2a9c09
commit daf3f25d21
9 changed files with 80 additions and 267 deletions

View File

@@ -10,11 +10,10 @@ use graphene_std::blending::BlendMode;
use graphene_std::color::SRGBA8;
use graphene_std::gradient::GradientStops;
use graphene_std::list::List;
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, CtxSnapshot, Graphic};
use graphene_std::{Artboard, Color, Graphic};
use std::any::Any;
use std::sync::Arc;
@@ -167,8 +166,8 @@ macro_rules! generate_layout_downcast {
($introspected_data:expr, $data:expr, [ $($ty:ty),* $(,)? ]) => {
if false { None }
$(
else if let Some(io) = $introspected_data.downcast_ref::<IORecord<CtxSnapshot, $ty>>() {
Some(io.output.layout_with_breadcrumb($data))
else if let Some(element) = $introspected_data.downcast_ref::<$ty>() {
Some(element.layout_with_breadcrumb($data))
}
)*
else { None }
@@ -178,8 +177,8 @@ macro_rules! generate_layout_downcast {
fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'static>, data: &mut LayoutData) -> Option<Vec<LayoutGroup>> {
// `List<NodeId>` 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::<IORecord<CtxSnapshot, List<NodeId>>>() {
return Some(table_node_id_path_layout_with_breadcrumb(&io.output, data));
if let Some(list) = introspected_data.downcast_ref::<List<NodeId>>() {
return Some(table_node_id_path_layout_with_breadcrumb(list, data));
}
generate_layout_downcast!(introspected_data, data, [
List<Artboard>,

View File

@@ -10,12 +10,11 @@ use graphene_std::application_io::{ExportFormat, NodeGraphUpdateMessage, RenderC
use graphene_std::bounds::RenderBoundingBox;
use graphene_std::color::SRGBA8;
use graphene_std::list::List;
use graphene_std::memo::IORecord;
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, CtxSnapshot, Graphic, NodeInputDecleration};
use graphene_std::{ATTR_TRANSFORM, Graphic, NodeInputDecleration};
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta;
use std::any::Any;
use std::sync::Arc;
@@ -900,18 +899,9 @@ fn measure_fill_geometry(data: &Arc<dyn Any + Send + Sync>) -> Option<(DAffine2,
}
// TODO: Eventually remove this document upgrade code
/// Extract a monitor node's recorded output, trying each context type the runtime may have evaluated it under.
/// Extract a monitor node's captured output element.
fn introspected_output<T: Clone + Send + Sync + 'static>(data: &Arc<dyn Any + Send + Sync>) -> Option<T> {
if let Some(io) = data.downcast_ref::<IORecord<(), T>>() {
return Some(io.output.clone());
}
if let Some(io) = data.downcast_ref::<IORecord<Footprint, T>>() {
return Some(io.output.clone());
}
if let Some(io) = data.downcast_ref::<IORecord<CtxSnapshot, T>>() {
return Some(io.output.clone());
}
None
data.downcast_ref::<T>().cloned()
}
// Re-export for usage by tests in other modules
@@ -927,9 +917,7 @@ mod test {
use crate::test_utils::test_prelude::{self, NodeGraphLayer};
use graph_craft::ProtoNodeIdentifier;
use graph_craft::document::NodeNetwork;
use graphene_std::CtxSnapshot;
use graphene_std::NodeInputDecleration;
use graphene_std::memo::IORecord;
use test_prelude::LayerNodeIdentifier;
/// Stores all of the monitor nodes that have been attached to a graph
@@ -990,17 +978,11 @@ mod test {
where
Input::Result: Send + Sync + Clone + 'static,
{
// This is quite inflexible since it only allows the footprint as inputs.
if let Some(x) = dynamic.downcast_ref::<IORecord<(), Input::Result>>() {
Some(x.output.clone())
} else if let Some(x) = dynamic.downcast_ref::<IORecord<Footprint, Input::Result>>() {
Some(x.output.clone())
} else if let Some(x) = dynamic.downcast_ref::<IORecord<CtxSnapshot, Input::Result>>() {
Some(x.output.clone())
} else {
let element = dynamic.downcast_ref::<Input::Result>().cloned();
if element.is_none() {
warn!("cannot downcast type for introspection");
None
}
element
}
/// Grab all of the values of the input every time it occurs in the graph.

View File

@@ -11,7 +11,6 @@ use graphene_std::application_io::{ApplicationIo, ExportFormat, NodeGraphUpdateM
use graphene_std::bounds::RenderBoundingBox;
use graphene_std::core_types::gpoll::GPoll;
use graphene_std::list::List;
use graphene_std::memo::IORecord;
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};
@@ -21,7 +20,7 @@ use graphene_std::runtime::{DynGraphRuntime, DynNotifier, DynSpawner, GraphRunti
use graphene_std::transform::RenderQuality;
use graphene_std::vector::Vector;
use graphene_std::vector::style::RenderMode;
use graphene_std::{Artboard, CtxSnapshot, Graphic};
use graphene_std::{Artboard, Graphic};
use interpreted_executor::dynamic_executor::{DynamicExecutor, ResolvedDocumentNodeTypesDelta};
use interpreted_executor::util::wrap_network_in_scope;
use spin::Mutex;
@@ -502,30 +501,30 @@ 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::<IORecord<CtxSnapshot, List<Graphic>>>() {
if let Some(list) = introspected_data.downcast_ref::<List<Graphic>>() {
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)
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(io) = introspected_data.downcast_ref::<IORecord<CtxSnapshot, List<Artboard>>>() {
else if let Some(list) = introspected_data.downcast_ref::<List<Artboard>>() {
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)
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(io) = introspected_data.downcast_ref::<IORecord<CtxSnapshot, List<Vector>>>() {
else if let Some(list) = introspected_data.downcast_ref::<List<Vector>>() {
// Insert the vector modify
self.vector_modify.insert(parent_network_node_id, io.output.element(0).cloned().unwrap_or_default());
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(io) = introspected_data.downcast_ref::<IORecord<CtxSnapshot, List<String>>>() {
else if let Some(list) = introspected_data.downcast_ref::<List<String>>() {
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)
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

View File

@@ -145,12 +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.
/// A record capture materializes here against the arena, inside the introspection window.
/// A record capture materializes its element here against the arena,
/// inside the introspection window, so consumers downcast the element
/// type directly.
pub fn introspect(&self, node_path: &[NodeId]) -> Result<Arc<dyn std::any::Any + Send + Sync + 'static>, IntrospectError> {
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);
return capture.materialize_element(&arena).map(Arc::from).ok_or(IntrospectError::NoData);
}
Ok(result)
}
@@ -648,11 +650,9 @@ mod test {
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");
let element = executor.introspect(&[NodeId(9)]).unwrap();
let element = element.downcast_ref::<f64>().expect("a record capture materializes to its element");
assert_eq!(*element, 7.);
}
#[test]

View File

@@ -2,7 +2,6 @@ use glam::{DAffine2, DVec2, IVec2};
use graph_craft::application_io::PlatformEditorApi;
use graph_craft::document::DocumentNode;
use graph_craft::document::value::RenderOutput;
use graphene_std::application_io::Texture;
use graphene_std::brush::brush_stroke::BrushStroke;
use graphene_std::gradient::GradientStops;
use graphene_std::list::{AttributeDyn, AttributeValueDyn, List, ListDyn};
@@ -20,7 +19,7 @@ use graphene_std::transform::Footprint;
use graphene_std::uuid::NodeId;
use graphene_std::vector::Vector;
use graphene_std::{Artboard, Context, Graphic, ProtoNodeIdentifier, SourceId, concrete, fn_type};
use node_registry_macros::{async_node, clone_node, convert_node, frame_memo_node, into_node, lend_node, record_extract_node, record_lift_node};
use node_registry_macros::{clone_node, convert_node, frame_memo_node, into_node, lend_node, record_extract_node, record_lift_node};
use std::collections::HashMap;
#[cfg(feature = "gpu")]
use wgpu_executor::WgpuExecutorHandle;
@@ -109,83 +108,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
// =============
// MONITOR NODES
// =============
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => ()]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List<Artboard>]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List<Graphic>]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List<Vector>]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List<Raster<CPU>>]),
#[cfg(feature = "gpu")]
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List<Raster<GPU>>]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List<Color>]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List<GradientStops>]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => Image<Color>]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => String]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => IVec2]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => DVec2]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => DAffine2]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => Option<DAffine2>]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => bool]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => f64]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => u32]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => u64]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => BlendMode]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => Texture]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::application_io::resource::Resource]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::BooleanOperation]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeCap]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeJoin]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::PaintOrder]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeAlign]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Stroke]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => Box<graphene_std::vector::VectorModification>]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => Option<f64>]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List<String>]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List<NodeId>]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List<f64>]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List<u8>]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List<bool>]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List<DAffine2>]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List<BlendMode>]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List<graphene_std::vector::style::GradientType>]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List<graphene_std::vector::style::GradientSpreadMethod>]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => AttributeDyn]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => AttributeValueDyn]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => ListDyn]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => Graphic]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::text::Font]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List<BrushStroke>]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => DocumentNode]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::Footprint]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::blending::BlendMode]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::LuminanceCalculation]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::extract_xy::XY]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::text_nodes::StringCapitalization]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RedGreenBlue]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RedGreenBlueAlpha]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::animation::RealTimeMode]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::NoiseType]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::FractalType]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::CellularDistanceFunction]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::CellularReturnType]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::DomainWarpType]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RelativeAbsolute]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::SelectiveColorChoice]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::GridType]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ArcType]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::RowsOrColumns]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::MergeByDistanceAlgorithm]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::ExtrudeJoiningAlgorithm]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientType]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientSpreadMethod]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::text::TextAlign]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::ScaleType]),
async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::InterpolationDistribution]),
// ==========
// MEMO NODES
// ==========
@@ -354,31 +276,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
record_lift_node!(wgpu_executor::WgpuPipelineCache),
#[cfg(feature = "gpu")]
record_extract_node!(wgpu_executor::WgpuPipelineCache),
(
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),
frame_memo_node!(f64),
lend_node!(f32),
clone_node!(f32),
@@ -630,30 +527,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
pub static NODE_REGISTRY: once_cell::sync::Lazy<HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>>> = once_cell::sync::Lazy::new(node_registry);
mod node_registry_macros {
macro_rules! async_node {
// This `params` variant of the macro wraps the normal `fn_params` variant and is used as a shorthand for writing `T` instead of `() => T`
($path:ty, input: $input:ty, params: [$($type:ty),*]) => {
async_node!($path, input: $input, fn_params: [ $(() => $type),*])
};
($path:ty, input: $input:ty, fn_params: [$first_arg:ty => $first:ty $(, $arg:ty => $type:ty)*]) => {
(
ProtoNodeIdentifier::new(stringify!($path)),
RegistryEntry {
io: NodeIOTypes::new(concrete!($input), concrete!($first), vec![fn_type!($first_arg, $first) $(, fn_type!($arg, $type))*]),
constructor: |inputs| {
let expected = [stringify!($first) $(, stringify!($type))*].len();
if inputs.len() != expected {
return Err(ConstructionError::Arity { expected, got: inputs.len() });
}
let mut inputs = inputs.into_iter();
let node = <$path>::new(inputs.next().unwrap().downcast::<$first>()? $(, inputs.next().unwrap().downcast::<$type>()?)*);
Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedNode<$first>>))
},
},
)
};
}
macro_rules! into_node {
(from: $from:ty, to: $to:ty) => {
(
@@ -854,7 +727,6 @@ mod node_registry_macros {
};
}
pub(crate) use async_node;
pub(crate) use clone_node;
pub(crate) use convert_node;
pub(crate) use frame_memo_node;

View File

@@ -4,13 +4,6 @@ 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<I, O> {
pub input: I,
pub output: O,
}
#[derive(Clone, Debug)]
pub struct MemoHash<T: CacheHash> {
hash: u64,

View File

@@ -752,7 +752,26 @@ pub struct RecordCapture {
bytes: crate::arena::ArenaWeak<Box<[u8]>>,
}
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<RecordCapture> {
let bytes: Box<[u8]> = unsafe { std::slice::from_raw_parts(rec.ptr(), layout.size) }.into();
arena.alloc(bytes).map(|(_, weak)| RecordCapture { layout: layout.clone(), bytes: weak })
}
/// The captured element, cloned out through the layout's erased glue.
pub fn materialize_element(&self, arena: &crate::arena::Arena) -> Option<Box<dyn std::any::Any + Send + Sync>> {
let bytes = self.bytes.upgrade(arena)?;
Some(unsafe { (self.layout.element.clone_out)(bytes.as_ptr()) })
}
pub fn materialize(&self, arena: &crate::arena::Arena) -> Option<Vec<(&'static str, Box<dyn crate::list::AnyAttributeValue>)>> {
let bytes = self.bytes.upgrade(arena)?;
Some(
@@ -765,56 +784,6 @@ impl RecordCapture {
}
}
// 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(self.layout.rec(record).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))
}
}
/// 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

View File

@@ -1,11 +1,10 @@
use core_types::arena::{Arena, ArenaCell};
use core_types::context::{Ctx, CtxSnapshot, DeriveCtx, ExtractAll, ExtractArena};
use core_types::context::{Ctx, ExtractArena};
use core_types::frame_table::{FrameTable, Lookup};
use core_types::gpoll::{Extent, Finality, GPoll, Interrupt};
use core_types::gpoll::{Extent, Finality, GPoll};
use core_types::graphene_hash::CacheHash;
use core_types::memo::*;
use core_types::node::Node;
use core_types::record::{OwnedRecord, RecordValue};
use core_types::record::{OwnedRecord, RecordCapture, RecordValue};
use core_types::registry::cache_key;
use std::sync::Arc;
use std::sync::Mutex;
@@ -120,28 +119,23 @@ fn lend<'e, T: Send + Sync>(ctx: impl Ctx + ExtractArena<'e>, value: T) -> GPoll
park(ctx.arena(), GPoll::Final(value))
}
type MonitorValue<T> = Arc<Mutex<Option<Arc<IORecord<CtxSnapshot, T>>>>>;
type MonitorValue = Arc<Mutex<Option<RecordCapture>>>;
/// 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"), skip_impl, plain)]
fn monitor<T: Clone + 'static + Send + Sync>(
ctx: impl Ctx + DeriveCtx + ExtractAll,
#[allow(clippy::type_complexity)]
#[data]
io: MonitorValue<T>,
content: impl Node<Context<'_>, Output = T>,
) -> Result<T, Interrupt> {
let output = content.eval(&ctx.derived())?;
*io.lock().unwrap() = Some(Arc::new(IORecord {
input: CtxSnapshot::capture(ctx),
output: output.clone(),
}));
Ok(output)
#[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<Context<'_>, Output = RecordValue<'e>>) -> GPoll<RecordValue<'e>> {
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;
}
result
}
fn serialize_monitor<T: Clone + 'static + Send + Sync>(io: &MonitorValue<T>) -> Option<Arc<dyn std::any::Any + Send + Sync>> {
let io = io.lock().unwrap();
io.as_ref().map(|output| output.clone() as Arc<dyn std::any::Any + Send + Sync>)
fn serialize_monitor(capture: &MonitorValue) -> Option<Arc<dyn std::any::Any + Send + Sync>> {
let capture = capture.lock().unwrap();
capture.as_ref().map(|capture| Arc::new(capture.clone()) as Arc<dyn std::any::Any + Send + Sync>)
}
#[cfg(test)]
@@ -191,21 +185,26 @@ mod tests {
}
#[test]
fn monitor_serialize_exposes_the_io_record_through_the_edge() {
fn monitor_serialize_exposes_the_capture_through_the_edge() {
let arena = Arena::new(1024).unwrap();
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<ErasedNode<u32>>);
assert!(handle.serialize().is_none(), "no record before the first eval");
let layout = element_layout::<u32>();
let monitor = MonitorNode::new(core_types::record::RecordLift::<u32, _>::new(ValueNode(11u32)), &layout);
let handle = EdgeHandle::new_record::<u32>(Arc::new(monitor) as Arc<ErasedRecordNode>);
assert!(handle.serialize().is_none(), "no capture before the first eval");
let edge = handle.duplicate().downcast::<u32>().unwrap();
assert_eq!(edge.eval(&ctx), GPoll::Final(11));
let edge = handle.duplicate().downcast_record::<u32>().unwrap();
let GPoll::Final(_) = edge.eval(&ctx) else {
panic!("expected a final record");
};
let record = handle.serialize().expect("the eval landed a record");
let record = record.downcast_ref::<IORecord<CtxSnapshot, u32>>().expect("the record is the monitor io");
assert_eq!(record.output, 11);
let capture = handle.serialize().expect("the eval landed a capture");
let capture = capture.downcast_ref::<RecordCapture>().expect("the capture is a record capture");
let element = capture.materialize_element(&arena).expect("the capture materializes inside the window");
assert_eq!(*element.downcast_ref::<u32>().unwrap(), 11);
}
#[test]

View File

@@ -464,7 +464,7 @@ mod tests {
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 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 ctx = ContextImpl::root(&scope);