Cut over to the graphene execution model

This commit is contained in:
Dennis Kobert
2026-08-04 13:15:21 +02:00
parent 7623b68318
commit 76ec799496
71 changed files with 3544 additions and 2378 deletions
@@ -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::<IORecord<Context, $ty>>() {
else if let Some(io) = $introspected_data.downcast_ref::<IORecord<CtxSnapshot, $ty>>() {
Some(io.output.layout_with_breadcrumb($data))
}
)*
@@ -178,7 +178,7 @@ 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<Context, List<NodeId>>>() {
if let Some(io) = introspected_data.downcast_ref::<IORecord<CtxSnapshot, List<NodeId>>>() {
return Some(table_node_id_path_layout_with_breadcrumb(&io.output, data));
}
generate_layout_downcast!(introspected_data, data, [
@@ -919,7 +919,12 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
exports: vec![NodeInput::node(NodeId(1), 0)],
nodes: [
DocumentNode {
inputs: vec![NodeInput::value(TaggedValue::None, false), NodeInput::import(concrete!(String), 1)],
inputs: vec![
NodeInput::value(TaggedValue::None, false),
NodeInput::import(concrete!(String), 1),
NodeInput::scope("graphene_std::runtime::RuntimeNode"),
NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::SourceId),
],
implementation: DocumentNodeImplementation::ProtoNode(platform_application_io::load_resource::IDENTIFIER),
..Default::default()
},
@@ -994,7 +999,13 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::import(generic!(T), 0), NodeInput::import(concrete!(Footprint), 1), NodeInput::node(NodeId(1), 0)],
inputs: vec![
NodeInput::import(generic!(T), 0),
NodeInput::import(concrete!(Footprint), 1),
NodeInput::node(NodeId(1), 0),
NodeInput::scope("graphene_std::runtime::RuntimeNode"),
NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::SourceId),
],
implementation: DocumentNodeImplementation::ProtoNode(platform_application_io::rasterize::IDENTIFIER),
..Default::default()
},
@@ -29,7 +29,7 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
// fallback when deriving `call_argument` so it reflects the impls actually registered, which will usually be `Context`.
let extended_node_registry = &*interpreted_executor::node_registry::NODE_REGISTRY;
let node_registry = NODE_REGISTRY.lock().unwrap();
let empty_implementations: Vec<(NodeConstructor, NodeIOTypes)> = Vec::new();
let empty_implementations: Vec<RegistryEntry> = Vec::new();
let context_type = concrete!(Context);
for (id, metadata) in NODE_METADATA.lock().unwrap().iter() {
let identifier = DefinitionIdentifier::ProtoNode(id.clone());
@@ -48,12 +48,12 @@ pub(super) fn post_process_nodes(custom: Vec<DocumentNodeDefinition>) -> HashMap
let implementations = node_registry.get(id).unwrap_or(&empty_implementations);
let first_node_io = implementations.first().map(|(_, node_io)| node_io).unwrap_or(const { &NodeIOTypes::empty() });
let first_node_io = implementations.first().map(|entry| &entry.io).unwrap_or(const { &NodeIOTypes::empty() });
let call_arguments: Vec<&Type> = if !implementations.is_empty() {
implementations.iter().map(|(_, io)| &io.call_argument).collect()
implementations.iter().map(|entry| &entry.io.call_argument).collect()
} else if let Some(impls) = extended_node_registry.get(id) {
impls.keys().map(|io| &io.call_argument).collect()
impls.iter().map(|entry| &entry.io.call_argument).collect()
} else {
Vec::new()
};
@@ -2360,7 +2360,7 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
return Vec::new();
};
let mut input_types = implementations.keys().filter_map(|item| item.inputs.get(input_index)).collect::<Vec<_>>();
let mut input_types = implementations.iter().filter_map(|entry| entry.io.inputs.get(input_index)).collect::<Vec<_>>();
input_types.sort_by_key(|ty| ty.type_name());
let input_type = input_types.first().cloned();
@@ -95,6 +95,34 @@ impl NodeNetworkInterface {
}
}
}
/// Append the hidden runtime and source-id inputs to async-source protonodes saved before their injection.
/// Runs after the identifier replacement pass, so it matches only current identifier spellings.
pub fn migrate_async_source_inputs(&mut self) {
const PRE_INJECTION_ARITIES: [(&str, usize); 5] = [
("graphene_std::platform_application_io::GetRequestNode", 4),
("graphene_std::platform_application_io::PostRequestNode", 5),
("graphene_std::platform_application_io::LoadResourceNode", 2),
("graphene_std::platform_application_io::RasterizeNode", 3),
("graphene_std::platform_application_io::ResourceNode", 2),
];
fix_network(self.document_network_mut());
fn fix_network(network: &mut NodeNetwork) {
for node in network.nodes.values_mut() {
if let Some(network) = node.implementation.get_network_mut() {
fix_network(network);
}
if let DocumentNodeImplementation::ProtoNode(protonode) = &node.implementation
&& let Some(base) = protonode.as_str().split('<').next()
&& let Some((_, arity)) = PRE_INJECTION_ARITIES.iter().find(|(identifier, _)| *identifier == base)
&& node.inputs.len() == *arity
{
node.inputs.push(NodeInput::scope("graphene_std::runtime::RuntimeNode"));
node.inputs.push(NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::SourceId));
}
}
}
}
}
// Public immutable getters for the network interface
@@ -253,8 +253,9 @@ impl NodeNetworkInterface {
};
let number_of_inputs = self.number_of_inputs(node_id, network_path);
implementations
.keys()
.filter_map(|node_io| {
.iter()
.filter_map(|entry| {
let node_io = &entry.io;
// Check if this NodeIOTypes implementation is valid for the other inputs
let valid_implementation = (0..number_of_inputs).filter(|iterator_index| iterator_index != input_index).all(|iterator_index| {
let input_type = self.input_type_not_invalid(&InputConnector::node(*node_id, iterator_index), network_path);
@@ -293,8 +294,9 @@ impl NodeNetworkInterface {
let valid_output_types = self.valid_output_types(&OutputConnector::node(*node_id, 0), network_path);
implementations
.keys()
.filter_map(|node_io| {
.iter()
.filter_map(|entry| {
let node_io = &entry.io;
if !valid_output_types.iter().any(|output_type| output_type.nested_type() == node_io.return_value.nested_type()) {
return None;
}
@@ -323,7 +325,7 @@ impl NodeNetworkInterface {
log::error!("Protonode {render_node:?} not found in registry");
return Vec::new();
};
implementations.keys().map(|types| types.inputs[1].clone()).collect()
implementations.iter().map(|entry| entry.io.inputs[1].clone()).collect()
}
}
}
@@ -1148,6 +1148,8 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
}
}
document.network_interface.migrate_async_source_inputs();
// The "Brush" wrapper network was replaced with the `brush` proto node directly. Convert old `Network("Brush")` instances to the proto node, forwarding all 3 inputs (Background, Trace, Cache) one-to-one.
// This must run as a pre-pass before the recursive iteration below: replacing the outer Brush's network impl orphans its child paths, and the recursive iteration would log errors for those stale paths.
let brush_layers: Vec<(NodeId, Vec<NodeId>)> = document
+9 -7
View File
@@ -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;
@@ -26,7 +26,7 @@ pub use runtime_io::NodeRuntimeIO;
mod runtime;
pub use runtime::*;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ExecutionRequest {
execution_id: u64,
render_config: RenderConfig,
@@ -375,10 +375,12 @@ impl NodeGraphExecutor {
}
}
let Some((queued_execution_id, execution_context)) = self.futures.pop_front() else {
let execution_context = if self.futures.front().is_some_and(|&(queued_execution_id, _)| queued_execution_id == execution_id) {
let (_, execution_context) = self.futures.pop_front().expect("front was just matched");
execution_context
} else {
panic!("InvalidGenerationId")
};
assert_eq!(queued_execution_id, execution_id, "Missmatch in execution id");
// TODO: Eventually remove this document upgrade code
// Gradient-migration measurement runs only read back the fill's evaluated geometry; they never render to the artwork.
@@ -892,7 +894,7 @@ fn introspected_output<T: Clone + Send + Sync + 'static>(data: &Arc<dyn Any + Se
if let Some(io) = data.downcast_ref::<IORecord<Footprint, T>>() {
return Some(io.output.clone());
}
if let Some(io) = data.downcast_ref::<IORecord<Context, T>>() {
if let Some(io) = data.downcast_ref::<IORecord<CtxSnapshot, T>>() {
return Some(io.output.clone());
}
None
@@ -911,7 +913,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 +981,7 @@ mod test {
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<Context, Input::Result>>() {
} else if let Some(x) = dynamic.downcast_ref::<IORecord<CtxSnapshot, Input::Result>>() {
Some(x.output.clone())
} else {
warn!("cannot downcast type for introspection");
+39 -33
View File
@@ -3,25 +3,26 @@ use crate::messages::frontend::utility_types::{ExportBounds, FileType};
use glam::{DAffine2, DVec2, UVec2};
use graph_craft::application_io::resource::ResourceRegistry;
use graph_craft::application_io::{PlatformApplicationIo, PlatformEditorApi};
use graph_craft::concrete;
use graph_craft::document::value::{RenderOutput, RenderOutputType, TaggedValue};
use graph_craft::document::{NodeId, NodeNetwork};
use graph_craft::graphene_compiler::Compiler;
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::memo::IORecord;
use graphene_std::ops::Convert;
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};
use graphene_std::raster_types::Raster;
use graphene_std::renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRender, SvgSegment};
use graphene_std::runtime::{DynGraphRuntime, DynSpawner, GraphRuntime, NoopSpawner, RuntimeHandle};
use graphene_std::transform::RenderQuality;
use graphene_std::vector::Vector;
use graphene_std::vector::style::RenderMode;
use graphene_std::{Artboard, Context, Graphic};
use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta};
use graphene_std::{Artboard, CtxSnapshot, Graphic};
use interpreted_executor::dynamic_executor::{DynamicExecutor, ResolvedDocumentNodeTypesDelta};
use interpreted_executor::util::wrap_network_in_scope;
use spin::Mutex;
use std::sync::Arc;
@@ -40,6 +41,7 @@ pub struct NodeRuntime {
editor_preferences: EditorPreferences,
old_graph: Option<NodeNetwork>,
update_thumbnails: bool,
graph_runtime: Arc<DynGraphRuntime>,
editor_api: Arc<PlatformEditorApi>,
resources: ResourceRegistry,
@@ -121,18 +123,25 @@ pub static NODE_RUNTIME: once_cell::sync::Lazy<Mutex<Option<NodeRuntime>>> = onc
impl NodeRuntime {
pub fn new(receiver: Receiver<GraphRuntimeRequest>, sender: Sender<NodeGraphUpdate>) -> Self {
let spawner: Box<DynSpawner> = Box::new(NoopSpawner);
let graph_runtime: Arc<DynGraphRuntime> = Arc::new(GraphRuntime::new(spawner));
let mut executor = DynamicExecutor::default();
executor.set_runtime(Arc::clone(&graph_runtime));
Self {
executor: DynamicExecutor::default(),
executor,
receiver,
sender: InternalNodeGraphUpdateSender(sender.clone()),
editor_preferences: EditorPreferences::default(),
old_graph: None,
resources: ResourceRegistry::default(),
update_thumbnails: true,
graph_runtime: Arc::clone(&graph_runtime),
editor_api: PlatformEditorApi {
editor_preferences: Box::new(EditorPreferences::default()),
node_graph_message_sender: Box::new(InternalNodeGraphUpdateSender(sender)),
runtime: RuntimeHandle(graph_runtime),
#[cfg(not(test))]
application_io: None,
@@ -173,7 +182,6 @@ impl NodeRuntime {
}
let for_export = execution_request.render_config.for_export;
execution = Some(request);
// If we get an export request we always execute it immedeatly otherwise it could get deduplicated
@@ -203,11 +211,12 @@ impl NodeRuntime {
application_io: self.editor_api.application_io.clone(),
node_graph_message_sender: Box::new(self.sender.clone()),
editor_preferences: Box::new(preferences),
runtime: self.editor_api.runtime.clone(),
}
.into();
if let Some(graph) = self.old_graph.clone() {
// We ignore this result as compilation errors should have been reported in an earlier iteration
let _ = self.update_network(graph).await;
let _ = self.update_network(graph);
}
}
GraphRuntimeRequest::GraphUpdate(GraphUpdate {
@@ -222,7 +231,7 @@ impl NodeRuntime {
self.resources = resources;
self.node_graph_errors.clear();
let result = self.update_network(network).await;
let result = self.update_network(network);
let node_graph_errors = self.node_graph_errors.clone();
self.update_thumbnails = true;
@@ -237,7 +246,7 @@ impl NodeRuntime {
render_config.export_format = ExportFormat::Svg;
}
let result = self.execute_network(render_config).await;
let result = self.execute_network(render_config);
let mut responses = VecDeque::new();
// TODO: Only process monitor nodes if the graph has changed, not when only the Footprint changes
if !render_config.for_eyedropper {
@@ -258,10 +267,10 @@ impl NodeRuntime {
.application_io
.as_ref()
.unwrap()
.gpu_executor()
.gpu_executor_arc()
.expect("GPU executor should be available when we receive a texture");
let raster_cpu = Raster::new_gpu(texture).convert(Footprint::BOUNDLESS, executor).await;
let raster_cpu = Raster::new_gpu(texture).convert(Footprint::BOUNDLESS, wgpu_executor::WgpuExecutorHandle(executor)).await;
let (data, width, height) = raster_cpu.to_flat_u8();
@@ -282,10 +291,10 @@ impl NodeRuntime {
.application_io
.as_ref()
.unwrap()
.gpu_executor()
.gpu_executor_arc()
.expect("GPU executor should be available when we receive a texture");
let raster_cpu = Raster::new_gpu(texture).convert(Footprint::BOUNDLESS, executor).await;
let raster_cpu = Raster::new_gpu(texture).convert(Footprint::BOUNDLESS, wgpu_executor::WgpuExecutorHandle(executor)).await;
self.sender.send_eyedropper_preview(raster_cpu);
continue;
@@ -345,7 +354,7 @@ impl NodeRuntime {
None
}
async fn update_network(&mut self, graph: NodeNetwork) -> Result<ResolvedDocumentNodeTypesDelta, (ResolvedDocumentNodeTypesDelta, String)> {
fn update_network(&mut self, graph: NodeNetwork) -> Result<ResolvedDocumentNodeTypesDelta, (ResolvedDocumentNodeTypesDelta, String)> {
let mut scoped_network = wrap_network_in_scope(graph, self.editor_api.clone());
if let Err(e) = self.preprocessor.preprocess(&mut scoped_network, &|resource_id| self.resources.hash(&resource_id)) {
@@ -368,20 +377,24 @@ impl NodeRuntime {
.collect::<Vec<_>>();
assert_ne!(proto_network.nodes.len(), 0, "No proto nodes exist?");
self.executor.update(proto_network).await.map_err(|(types, e)| {
self.executor.update(proto_network).map_err(|(types, e)| {
self.node_graph_errors.clone_from(&e);
(types, format!("{e:?}"))
})
}
async fn execute_network(&mut self, render_config: RenderConfig) -> Result<TaggedValue, String> {
fn execute_network(&mut self, render_config: RenderConfig) -> Result<TaggedValue, String> {
use graph_craft::graphene_compiler::Executor;
match self.executor.input_type() {
Some(t) if t == concrete!(RenderConfig) => (&self.executor).execute(render_config).await.map_err(|e| e.to_string()),
Some(t) if t == concrete!(()) => (&self.executor).execute(()).await.map_err(|e| e.to_string()),
Some(t) => Err(format!("Invalid input type {t:?}")),
_ => Err(format!("No input type:\n{:?}", self.node_graph_errors)),
match (&self.executor).execute(render_config).map_err(|e| e.to_string())? {
GPoll::Final(value) | GPoll::Partial(value) => Ok(value),
GPoll::Fallback(boxed) => {
let (value, error) = *boxed;
error!("Node graph evaluation reported an error alongside its fallback output: {error:?}");
Ok(value)
}
GPoll::Pending => Err("Node graph evaluation is pending".to_string()),
GPoll::Error(error) => Err(format!("Node graph evaluation failed: {error:?}")),
}
}
@@ -415,7 +428,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::<IORecord<Context, List<Graphic>>>() {
if let Some(io) = introspected_data.downcast_ref::<IORecord<CtxSnapshot, 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)
@@ -423,19 +436,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::<IORecord<Context, List<Artboard>>>() {
else if let Some(io) = introspected_data.downcast_ref::<IORecord<CtxSnapshot, 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)
}
}
// Vector list: vector modifications
else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, List<Vector>>>() {
else if let Some(io) = introspected_data.downcast_ref::<IORecord<CtxSnapshot, List<Vector>>>() {
// 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::<IORecord<Context, List<String>>>() {
else if let Some(io) = introspected_data.downcast_ref::<IORecord<CtxSnapshot, 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)
@@ -544,14 +557,6 @@ fn expand_to_thumbnail_aspect(bounds: [DVec2; 2]) -> [DVec2; 2] {
[center - half, center + half]
}
pub async fn introspect_node(path: &[NodeId]) -> Result<Arc<dyn std::any::Any + Send + Sync + 'static>, IntrospectError> {
let runtime = NODE_RUNTIME.lock();
if let Some(ref mut runtime) = runtime.as_ref() {
return runtime.executor.introspect(path);
}
Err(IntrospectError::RuntimeNotReady)
}
pub async fn run_node_graph() -> (bool, Option<Texture>) {
let Some(mut runtime) = NODE_RUNTIME.try_lock() else { return (false, None) };
if let Some(ref mut runtime) = runtime.as_mut() {
@@ -577,6 +582,7 @@ impl NodeRuntime {
application_io: Some(application_io.into()),
node_graph_message_sender: Box::new(self.sender.clone()),
editor_preferences: Box::new(self.editor_preferences.clone()),
runtime: self.editor_api.runtime.clone(),
}
.into();
}