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
Generated
+1
View File
@@ -2038,6 +2038,7 @@ dependencies = [
"core-types", "core-types",
"dyn-any", "dyn-any",
"glam", "glam",
"graphene-hash",
"graphene-resource", "graphene-resource",
"log", "log",
"raster-types", "raster-types",
+1 -1
View File
@@ -228,7 +228,7 @@ impl RenderState {
return; return;
}; };
let size = glam::UVec2::new(viewport_texture.width(), viewport_texture.height()); let size = glam::UVec2::new(viewport_texture.width(), viewport_texture.height());
let result = futures::executor::block_on(self.executor.render_vello_scene(&scene, size, &Default::default(), None)); let result = self.executor.render_vello_scene(&scene, size, &Default::default(), None);
match result { match result {
Ok(texture) => { Ok(texture) => {
self.overlays_texture = Some(texture.into()); self.overlays_texture = Some(texture.into());
@@ -14,7 +14,7 @@ use graphene_std::memo::IORecord;
use graphene_std::raster_types::{CPU, GPU, Raster}; use graphene_std::raster_types::{CPU, GPU, Raster};
use graphene_std::vector::Vector; use graphene_std::vector::Vector;
use graphene_std::vector::style::{FillChoice, FillChoiceUI, GradientSpreadMethod, GradientType}; 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::any::Any;
use std::sync::Arc; use std::sync::Arc;
@@ -167,7 +167,7 @@ macro_rules! generate_layout_downcast {
($introspected_data:expr, $data:expr, [ $($ty:ty),* $(,)? ]) => { ($introspected_data:expr, $data:expr, [ $($ty:ty),* $(,)? ]) => {
if false { None } 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)) 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>> { 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<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. // `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)); return Some(table_node_id_path_layout_with_breadcrumb(&io.output, data));
} }
generate_layout_downcast!(introspected_data, 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)], exports: vec![NodeInput::node(NodeId(1), 0)],
nodes: [ nodes: [
DocumentNode { 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), implementation: DocumentNodeImplementation::ProtoNode(platform_application_io::load_resource::IDENTIFIER),
..Default::default() ..Default::default()
}, },
@@ -994,7 +999,13 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
..Default::default() ..Default::default()
}, },
DocumentNode { 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), implementation: DocumentNodeImplementation::ProtoNode(platform_application_io::rasterize::IDENTIFIER),
..Default::default() ..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`. // 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 extended_node_registry = &*interpreted_executor::node_registry::NODE_REGISTRY;
let node_registry = NODE_REGISTRY.lock().unwrap(); 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); let context_type = concrete!(Context);
for (id, metadata) in NODE_METADATA.lock().unwrap().iter() { for (id, metadata) in NODE_METADATA.lock().unwrap().iter() {
let identifier = DefinitionIdentifier::ProtoNode(id.clone()); 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 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() { 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) { } 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 { } else {
Vec::new() Vec::new()
}; };
@@ -2360,7 +2360,7 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
return Vec::new(); 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()); input_types.sort_by_key(|ty| ty.type_name());
let input_type = input_types.first().cloned(); 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 // 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); let number_of_inputs = self.number_of_inputs(node_id, network_path);
implementations implementations
.keys() .iter()
.filter_map(|node_io| { .filter_map(|entry| {
let node_io = &entry.io;
// Check if this NodeIOTypes implementation is valid for the other inputs // 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 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); 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); let valid_output_types = self.valid_output_types(&OutputConnector::node(*node_id, 0), network_path);
implementations implementations
.keys() .iter()
.filter_map(|node_io| { .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()) { if !valid_output_types.iter().any(|output_type| output_type.nested_type() == node_io.return_value.nested_type()) {
return None; return None;
} }
@@ -323,7 +325,7 @@ impl NodeNetworkInterface {
log::error!("Protonode {render_node:?} not found in registry"); log::error!("Protonode {render_node:?} not found in registry");
return Vec::new(); 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. // 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. // 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 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::renderer::{RenderMetadata, graphic_list_bounding_box};
use graphene_std::transform::Footprint; use graphene_std::transform::Footprint;
use graphene_std::vector::{Vector, graphic_types}; 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 interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta;
use std::any::Any; use std::any::Any;
use std::sync::Arc; use std::sync::Arc;
@@ -26,7 +26,7 @@ pub use runtime_io::NodeRuntimeIO;
mod runtime; mod runtime;
pub use runtime::*; pub use runtime::*;
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ExecutionRequest { pub struct ExecutionRequest {
execution_id: u64, execution_id: u64,
render_config: RenderConfig, 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") panic!("InvalidGenerationId")
}; };
assert_eq!(queued_execution_id, execution_id, "Missmatch in execution id");
// TODO: Eventually remove this document upgrade code // 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. // 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>>() { if let Some(io) = data.downcast_ref::<IORecord<Footprint, T>>() {
return Some(io.output.clone()); 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()); return Some(io.output.clone());
} }
None None
@@ -911,7 +913,7 @@ mod test {
use crate::test_utils::test_prelude::{self, NodeGraphLayer}; use crate::test_utils::test_prelude::{self, NodeGraphLayer};
use graph_craft::ProtoNodeIdentifier; use graph_craft::ProtoNodeIdentifier;
use graph_craft::document::NodeNetwork; use graph_craft::document::NodeNetwork;
use graphene_std::Context; use graphene_std::CtxSnapshot;
use graphene_std::NodeInputDecleration; use graphene_std::NodeInputDecleration;
use graphene_std::memo::IORecord; use graphene_std::memo::IORecord;
use test_prelude::LayerNodeIdentifier; use test_prelude::LayerNodeIdentifier;
@@ -979,7 +981,7 @@ mod test {
Some(x.output.clone()) Some(x.output.clone())
} else if let Some(x) = dynamic.downcast_ref::<IORecord<Footprint, Input::Result>>() { } else if let Some(x) = dynamic.downcast_ref::<IORecord<Footprint, Input::Result>>() {
Some(x.output.clone()) 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()) Some(x.output.clone())
} else { } else {
warn!("cannot downcast type for introspection"); 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 glam::{DAffine2, DVec2, UVec2};
use graph_craft::application_io::resource::ResourceRegistry; use graph_craft::application_io::resource::ResourceRegistry;
use graph_craft::application_io::{PlatformApplicationIo, PlatformEditorApi}; use graph_craft::application_io::{PlatformApplicationIo, PlatformEditorApi};
use graph_craft::concrete;
use graph_craft::document::value::{RenderOutput, RenderOutputType, TaggedValue}; use graph_craft::document::value::{RenderOutput, RenderOutputType, TaggedValue};
use graph_craft::document::{NodeId, NodeNetwork}; use graph_craft::document::{NodeId, NodeNetwork};
use graph_craft::graphene_compiler::Compiler; use graph_craft::graphene_compiler::Compiler;
use graph_craft::proto::GraphErrors; use graph_craft::proto::GraphErrors;
use graphene_std::application_io::{ApplicationIo, ExportFormat, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig, Texture}; use graphene_std::application_io::{ApplicationIo, ExportFormat, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig, Texture};
use graphene_std::bounds::RenderBoundingBox; use graphene_std::bounds::RenderBoundingBox;
use graphene_std::core_types::gpoll::GPoll;
use graphene_std::list::List; use graphene_std::list::List;
use graphene_std::memo::IORecord; use graphene_std::memo::IORecord;
use graphene_std::ops::Convert; use graphene_std::ops::ConvertAsync;
#[cfg(all(target_family = "wasm", feature = "gpu", feature = "wasm"))] #[cfg(all(target_family = "wasm", feature = "gpu", feature = "wasm"))]
use graphene_std::platform_application_io::canvas_utils::{Canvas, CanvasSurface, CanvasSurfaceHandle}; use graphene_std::platform_application_io::canvas_utils::{Canvas, CanvasSurface, CanvasSurfaceHandle};
use graphene_std::raster_types::Raster; use graphene_std::raster_types::Raster;
use graphene_std::renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRender, SvgSegment}; 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::transform::RenderQuality;
use graphene_std::vector::Vector; use graphene_std::vector::Vector;
use graphene_std::vector::style::RenderMode; 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::dynamic_executor::{DynamicExecutor, ResolvedDocumentNodeTypesDelta};
use interpreted_executor::util::wrap_network_in_scope; use interpreted_executor::util::wrap_network_in_scope;
use spin::Mutex; use spin::Mutex;
use std::sync::Arc; use std::sync::Arc;
@@ -40,6 +41,7 @@ pub struct NodeRuntime {
editor_preferences: EditorPreferences, editor_preferences: EditorPreferences,
old_graph: Option<NodeNetwork>, old_graph: Option<NodeNetwork>,
update_thumbnails: bool, update_thumbnails: bool,
graph_runtime: Arc<DynGraphRuntime>,
editor_api: Arc<PlatformEditorApi>, editor_api: Arc<PlatformEditorApi>,
resources: ResourceRegistry, resources: ResourceRegistry,
@@ -121,18 +123,25 @@ pub static NODE_RUNTIME: once_cell::sync::Lazy<Mutex<Option<NodeRuntime>>> = onc
impl NodeRuntime { impl NodeRuntime {
pub fn new(receiver: Receiver<GraphRuntimeRequest>, sender: Sender<NodeGraphUpdate>) -> Self { 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 { Self {
executor: DynamicExecutor::default(), executor,
receiver, receiver,
sender: InternalNodeGraphUpdateSender(sender.clone()), sender: InternalNodeGraphUpdateSender(sender.clone()),
editor_preferences: EditorPreferences::default(), editor_preferences: EditorPreferences::default(),
old_graph: None, old_graph: None,
resources: ResourceRegistry::default(), resources: ResourceRegistry::default(),
update_thumbnails: true, update_thumbnails: true,
graph_runtime: Arc::clone(&graph_runtime),
editor_api: PlatformEditorApi { editor_api: PlatformEditorApi {
editor_preferences: Box::new(EditorPreferences::default()), editor_preferences: Box::new(EditorPreferences::default()),
node_graph_message_sender: Box::new(InternalNodeGraphUpdateSender(sender)), node_graph_message_sender: Box::new(InternalNodeGraphUpdateSender(sender)),
runtime: RuntimeHandle(graph_runtime),
#[cfg(not(test))] #[cfg(not(test))]
application_io: None, application_io: None,
@@ -173,7 +182,6 @@ impl NodeRuntime {
} }
let for_export = execution_request.render_config.for_export; let for_export = execution_request.render_config.for_export;
execution = Some(request); execution = Some(request);
// If we get an export request we always execute it immedeatly otherwise it could get deduplicated // 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(), application_io: self.editor_api.application_io.clone(),
node_graph_message_sender: Box::new(self.sender.clone()), node_graph_message_sender: Box::new(self.sender.clone()),
editor_preferences: Box::new(preferences), editor_preferences: Box::new(preferences),
runtime: self.editor_api.runtime.clone(),
} }
.into(); .into();
if let Some(graph) = self.old_graph.clone() { if let Some(graph) = self.old_graph.clone() {
// We ignore this result as compilation errors should have been reported in an earlier iteration // 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 { GraphRuntimeRequest::GraphUpdate(GraphUpdate {
@@ -222,7 +231,7 @@ impl NodeRuntime {
self.resources = resources; self.resources = resources;
self.node_graph_errors.clear(); 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(); let node_graph_errors = self.node_graph_errors.clone();
self.update_thumbnails = true; self.update_thumbnails = true;
@@ -237,7 +246,7 @@ impl NodeRuntime {
render_config.export_format = ExportFormat::Svg; 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(); let mut responses = VecDeque::new();
// TODO: Only process monitor nodes if the graph has changed, not when only the Footprint changes // TODO: Only process monitor nodes if the graph has changed, not when only the Footprint changes
if !render_config.for_eyedropper { if !render_config.for_eyedropper {
@@ -258,10 +267,10 @@ impl NodeRuntime {
.application_io .application_io
.as_ref() .as_ref()
.unwrap() .unwrap()
.gpu_executor() .gpu_executor_arc()
.expect("GPU executor should be available when we receive a texture"); .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(); let (data, width, height) = raster_cpu.to_flat_u8();
@@ -282,10 +291,10 @@ impl NodeRuntime {
.application_io .application_io
.as_ref() .as_ref()
.unwrap() .unwrap()
.gpu_executor() .gpu_executor_arc()
.expect("GPU executor should be available when we receive a texture"); .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); self.sender.send_eyedropper_preview(raster_cpu);
continue; continue;
@@ -345,7 +354,7 @@ impl NodeRuntime {
None 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()); 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)) { 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<_>>(); .collect::<Vec<_>>();
assert_ne!(proto_network.nodes.len(), 0, "No proto nodes exist?"); 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); self.node_graph_errors.clone_from(&e);
(types, format!("{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; use graph_craft::graphene_compiler::Executor;
match self.executor.input_type() { match (&self.executor).execute(render_config).map_err(|e| e.to_string())? {
Some(t) if t == concrete!(RenderConfig) => (&self.executor).execute(render_config).await.map_err(|e| e.to_string()), GPoll::Final(value) | GPoll::Partial(value) => Ok(value),
Some(t) if t == concrete!(()) => (&self.executor).execute(()).await.map_err(|e| e.to_string()), GPoll::Fallback(boxed) => {
Some(t) => Err(format!("Invalid input type {t:?}")), let (value, error) = *boxed;
_ => Err(format!("No input type:\n{:?}", self.node_graph_errors)), 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) // 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 { if update_thumbnails {
let bounds = graphene_std::renderer::graphic_list_bounding_box(&io.output, DAffine2::IDENTITY); 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) 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 // 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 // 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 { if update_thumbnails {
let bounds = artboard_clip_bounds(&io.output); let bounds = artboard_clip_bounds(&io.output);
Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, bounds, responses) Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, bounds, responses)
} }
} }
// Vector list: vector modifications // 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 // 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, 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`) // 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 { if update_thumbnails {
let bounds = graphene_std::renderer::text_list_bounding_box(&io.output, DAffine2::IDENTITY); 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) 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] [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>) { pub async fn run_node_graph() -> (bool, Option<Texture>) {
let Some(mut runtime) = NODE_RUNTIME.try_lock() else { return (false, None) }; let Some(mut runtime) = NODE_RUNTIME.try_lock() else { return (false, None) };
if let Some(ref mut runtime) = runtime.as_mut() { if let Some(ref mut runtime) = runtime.as_mut() {
@@ -577,6 +582,7 @@ impl NodeRuntime {
application_io: Some(application_io.into()), application_io: Some(application_io.into()),
node_graph_message_sender: Box::new(self.sender.clone()), node_graph_message_sender: Box::new(self.sender.clone()),
editor_preferences: Box::new(self.editor_preferences.clone()), editor_preferences: Box::new(self.editor_preferences.clone()),
runtime: self.editor_api.runtime.clone(),
} }
.into(); .into();
} }
+9 -4
View File
@@ -9,7 +9,7 @@ pub use graphene_application_io::ApplicationIo;
#[derive(Default)] #[derive(Default)]
pub struct PlatformApplicationIo { pub struct PlatformApplicationIo {
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
gpu_executor: Option<WgpuExecutor>, gpu_executor: Option<std::sync::Arc<WgpuExecutor>>,
resources: Option<Box<dyn resource::LoadResource>>, resources: Option<Box<dyn resource::LoadResource>>,
} }
@@ -26,7 +26,7 @@ impl PlatformApplicationIo {
Self { Self {
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
gpu_executor: executor, gpu_executor: executor.map(std::sync::Arc::new),
resources: None, resources: None,
} }
} }
@@ -39,7 +39,7 @@ impl PlatformApplicationIo {
set_wgpu_available(wgpu_available); set_wgpu_available(wgpu_available);
Self { Self {
gpu_executor: executor, gpu_executor: executor.map(std::sync::Arc::new),
resources: None, resources: None,
} }
} }
@@ -57,7 +57,12 @@ impl ApplicationIo for PlatformApplicationIo {
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
fn gpu_executor(&self) -> Option<&Self::Executor> { fn gpu_executor(&self) -> Option<&Self::Executor> {
self.gpu_executor.as_ref() self.gpu_executor.as_deref()
}
#[cfg(feature = "wgpu")]
fn gpu_executor_arc(&self) -> Option<std::sync::Arc<Self::Executor>> {
self.gpu_executor.clone()
} }
fn load_resource(&self, hash: resource::ResourceHash) -> resource::ResourceFuture<'_> { fn load_resource(&self, hash: resource::ResourceHash) -> resource::ResourceFuture<'_> {
+21 -3
View File
@@ -8,7 +8,6 @@ pub use core_types::uuid::generate_uuid;
use core_types::{Context, ContextDependencies, Cow, MemoHash, ProtoNodeIdentifier, Type}; use core_types::{Context, ContextDependencies, Cow, MemoHash, ProtoNodeIdentifier, Type};
use dyn_any::DynAny; use dyn_any::DynAny;
use glam::IVec2; use glam::IVec2;
use log::Metadata;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use std::collections::HashMap; use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
@@ -215,12 +214,14 @@ impl InlineRust {
#[derive(Debug, Clone, PartialEq, Hash, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, PartialEq, Hash, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
pub enum DocumentNodeMetadata { pub enum DocumentNodeMetadata {
DocumentNodePath, DocumentNodePath,
SourceId,
} }
impl DocumentNodeMetadata { impl DocumentNodeMetadata {
pub fn ty(&self) -> Type { pub fn ty(&self) -> Type {
match self { match self {
DocumentNodeMetadata::DocumentNodePath => concrete!(core_types::list::List<NodeId>), DocumentNodeMetadata::DocumentNodePath => concrete!(core_types::list::List<NodeId>),
DocumentNodeMetadata::SourceId => concrete!(u64),
} }
} }
} }
@@ -273,7 +274,7 @@ impl NodeInput {
NodeInput::Import { import_type, .. } => import_type.clone(), NodeInput::Import { import_type, .. } => import_type.clone(),
NodeInput::Inline(_) => panic!("ty() called on NodeInput::Inline"), NodeInput::Inline(_) => panic!("ty() called on NodeInput::Inline"),
NodeInput::Scope(_) => panic!("ty() called on NodeInput::Scope"), NodeInput::Scope(_) => panic!("ty() called on NodeInput::Scope"),
NodeInput::Reflection(_) => concrete!(Metadata), NodeInput::Reflection(metadata) => metadata.ty(),
} }
} }
@@ -879,7 +880,7 @@ impl NodeNetwork {
// Replace value inputs with dedicated value nodes // Replace value inputs with dedicated value nodes
if node.implementation != DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("core_types::value::ClonedNode")) { if node.implementation != DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("core_types::value::ClonedNode")) {
Self::replace_value_inputs_with_nodes(&mut node.inputs, &mut self.nodes, &path, gen_id, map_ids, id); Self::replace_value_inputs_with_nodes(&mut node.inputs, &mut self.nodes, &path, gen_id, map_ids, id, Some(&mut node.context_features));
} }
let DocumentNodeImplementation::Network(mut inner_network) = node.implementation else { let DocumentNodeImplementation::Network(mut inner_network) = node.implementation else {
@@ -898,6 +899,7 @@ impl NodeNetwork {
gen_id, gen_id,
map_ids, map_ids,
id, id,
None,
); );
// Connect all network inputs to either the parent network nodes, or newly created value nodes for the parent node. // Connect all network inputs to either the parent network nodes, or newly created value nodes for the parent node.
@@ -978,6 +980,12 @@ impl NodeNetwork {
} }
} }
fn source_id_for_path(path: &[NodeId]) -> u64 {
let mut hasher = graphene_hash::FxHasher64::new();
path.hash(&mut hasher);
hasher.finish()
}
#[inline(never)] #[inline(never)]
fn replace_value_inputs_with_nodes( fn replace_value_inputs_with_nodes(
inputs: &mut [NodeInput], inputs: &mut [NodeInput],
@@ -986,6 +994,7 @@ impl NodeNetwork {
gen_id: impl Fn() -> NodeId + Copy, gen_id: impl Fn() -> NodeId + Copy,
map_ids: impl Fn(NodeId, NodeId) -> NodeId + Copy, map_ids: impl Fn(NodeId, NodeId) -> NodeId + Copy,
id: NodeId, id: NodeId,
mut context_features: Option<&mut ContextDependencies>,
) { ) {
// Replace value exports and imports with value nodes, added inside the nested network // Replace value exports and imports with value nodes, added inside the nested network
for export in inputs { for export in inputs {
@@ -996,6 +1005,13 @@ impl NodeNetwork {
NodeInput::Value { tagged_value, exposed } => (tagged_value, exposed), NodeInput::Value { tagged_value, exposed } => (tagged_value, exposed),
NodeInput::Reflection(reflect) => match reflect { NodeInput::Reflection(reflect) => match reflect {
DocumentNodeMetadata::DocumentNodePath => (TaggedValue::NodeIdPath(path.to_vec()).into(), false), DocumentNodeMetadata::DocumentNodePath => (TaggedValue::NodeIdPath(path.to_vec()).into(), false),
DocumentNodeMetadata::SourceId => {
let source_id = Self::source_id_for_path(path);
if let Some(context_features) = context_features.as_deref_mut() {
core_types::context::merge_sorted_sources(&mut context_features.sources, &[source_id]);
}
(TaggedValue::U64(source_id).into(), false)
}
}, },
previous_export => { previous_export => {
*export = previous_export; *export = previous_export;
@@ -1208,7 +1224,9 @@ fn migrate_call_argument<'de, D: serde::Deserializer<'de>>(deserializer: D) -> R
Old(Option<Type>), Old(Option<Type>),
} }
// TODO: Eventually remove this migration document upgrade code
Ok(match CallArg::deserialize(deserializer)? { Ok(match CallArg::deserialize(deserializer)? {
CallArg::New(Type::Concrete(descriptor)) if descriptor.name.ends_with("OwnedContextImpl>>") => concrete!(Context),
CallArg::New(ty) => ty, CallArg::New(ty) => ty,
CallArg::Old(ty) => ty.unwrap_or_default(), CallArg::Old(ty) => ty.unwrap_or_default(),
}) })
+90 -43
View File
@@ -1,13 +1,18 @@
use super::DocumentNode; use super::DocumentNode;
use crate::application_io::PlatformEditorApi; use crate::application_io::PlatformEditorApi;
use crate::application_io::resource::Resource; use crate::application_io::resource::Resource;
use crate::proto::{Any as DAny, FutureAny}; use crate::proto::Any as DAny;
use brush_nodes::brush_stroke::BrushStroke; use brush_nodes::brush_stroke::BrushStroke;
use core_types::color::SRGBA8; use core_types::color::SRGBA8;
use core_types::context::Context;
use core_types::gpoll::GPoll;
use core_types::list::List; use core_types::list::List;
use core_types::node::Node;
use core_types::registry::{EdgeHandle, edge_type};
use core_types::transform::Footprint; use core_types::transform::Footprint;
use core_types::uuid::NodeId; use core_types::uuid::NodeId;
use core_types::{CacheHash, Color, ContextFeatures, MemoHash, Node, Type, TypeDescriptor}; use core_types::value::value_edge;
use core_types::{CacheHash, Color, ContextModification, MemoHash, Type, TypeDescriptor};
use dyn_any::DynAny; use dyn_any::DynAny;
pub use dyn_any::StaticType; pub use dyn_any::StaticType;
pub use glam::{DAffine2, DVec2, IVec2, UVec2}; pub use glam::{DAffine2, DVec2, IVec2, UVec2};
@@ -19,7 +24,6 @@ use graphic_types::{Artboard, Graphic, Vector};
use rendering::RenderMetadata; use rendering::RenderMetadata;
use std::fmt::Display; use std::fmt::Display;
use std::hash::Hash; use std::hash::Hash;
use std::marker::PhantomData;
use std::str::FromStr; use std::str::FromStr;
pub use std::sync::Arc; pub use std::sync::Arc;
use text_nodes::Font; use text_nodes::Font;
@@ -90,7 +94,7 @@ macro_rules! tagged_value {
DocumentNode(DocumentNode), DocumentNode(DocumentNode),
/// Carried by context nullification proto nodes constructed at proto node compilation time in `insert_context_nullification_nodes`. /// Carried by context nullification proto nodes constructed at proto node compilation time in `insert_context_nullification_nodes`.
#[serde(skip)] #[serde(skip)]
ContextFeatures(ContextFeatures), ContextModification(ContextModification),
#[serde(skip)] #[serde(skip)]
EditorApi(Arc<PlatformEditorApi>), EditorApi(Arc<PlatformEditorApi>),
/// Only used by the `resource` node, should never be serialized /// Only used by the `resource` node, should never be serialized
@@ -120,7 +124,7 @@ macro_rules! tagged_value {
// ======================= // =======================
Self::NodeIdPath(path) => path.hash(state), Self::NodeIdPath(path) => path.hash(state),
Self::DocumentNode(node) => node.cache_hash(state), Self::DocumentNode(node) => node.cache_hash(state),
Self::ContextFeatures(features) => features.cache_hash(state), Self::ContextModification(modification) => modification.cache_hash(state),
Self::RenderOutput(x) => x.cache_hash(state), Self::RenderOutput(x) => x.cache_hash(state),
Self::EditorApi(x) => x.cache_hash(state), Self::EditorApi(x) => x.cache_hash(state),
Self::ResourceHash(x) => x.cache_hash(state), Self::ResourceHash(x) => x.cache_hash(state),
@@ -175,7 +179,7 @@ macro_rules! tagged_value {
Box::new(list) Box::new(list)
} }
Self::DocumentNode(node) => Box::new(node), Self::DocumentNode(node) => Box::new(node),
Self::ContextFeatures(features) => Box::new(features), Self::ContextModification(modification) => Box::new(modification),
Self::EditorApi(x) => Box::new(x), Self::EditorApi(x) => Box::new(x),
Self::ResourceHash(x) => Box::new(x), Self::ResourceHash(x) => Box::new(x),
} }
@@ -225,7 +229,7 @@ macro_rules! tagged_value {
Arc::new(list) Arc::new(list)
} }
Self::DocumentNode(node) => Arc::new(node), Self::DocumentNode(node) => Arc::new(node),
Self::ContextFeatures(features) => Arc::new(features), Self::ContextModification(modification) => Arc::new(modification),
Self::EditorApi(x) => Arc::new(x), Self::EditorApi(x) => Arc::new(x),
Self::ResourceHash(x) => Arc::new(x), Self::ResourceHash(x) => Arc::new(x),
} }
@@ -253,12 +257,88 @@ macro_rules! tagged_value {
Self::RenderOutput(_) => concrete!(RenderOutput), Self::RenderOutput(_) => concrete!(RenderOutput),
Self::NodeIdPath(_) => concrete!(List<NodeId>), Self::NodeIdPath(_) => concrete!(List<NodeId>),
Self::DocumentNode(_) => concrete!(DocumentNode), Self::DocumentNode(_) => concrete!(DocumentNode),
Self::ContextFeatures(_) => concrete!(ContextFeatures), Self::ContextModification(_) => concrete!(ContextModification),
Self::EditorApi(_) => concrete!(&PlatformEditorApi), Self::EditorApi(_) => concrete!(Arc<PlatformEditorApi>),
Self::ResourceHash(_) => concrete!(ResourceHash), Self::ResourceHash(_) => concrete!(ResourceHash),
} }
} }
/// Materializes the value as [`Self::to_dynany`] does, wrapped in a `ClonedNode` edge typed by [`Self::ty`].
pub fn to_edge(self) -> Result<EdgeHandle, String> {
match self {
// ===============
// MANUAL VARIANTS
// ===============
Self::None => Ok(value_edge(())),
Self::TypeDefault(td) => {
// Same direct-construction path as `to_dynany` for the same reason as in `to_dynany`.
let name = td.name.as_ref();
macro_rules! check {
($type_default:ty) => {
if name == std::any::type_name::<$type_default>() { return Ok(value_edge(<$type_default>::default())); }
};
}
for_each_type_default!(check);
Self::from_type_or_none(&Type::Concrete(td)).to_edge()
}
Self::F64Array(values) => {
let list: List<f64> = values.into_iter().map(core_types::list::Item::new_from_element).collect();
Ok(value_edge(list))
}
Self::Color(color) => {
let list: List<Color> = color.into_iter().map(core_types::list::Item::new_from_element).collect();
Ok(value_edge(list))
}
Self::Gradient(stops) => Ok(value_edge(List::<GradientStops>::new_from_element(stops))),
Self::BrushStrokes(strokes) => {
let list: List<BrushStroke> = strokes.into_iter().map(core_types::list::Item::new_from_element).collect();
Ok(value_edge(list))
}
// =======================
// AUTO-GENERATED VARIANTS
// =======================
$( Self::$identifier(x) => Ok(value_edge(x)), )*
// =======================
// NON-SERIALIZED VARIANTS
// =======================
Self::RenderOutput(x) => Ok(value_edge(x)),
Self::NodeIdPath(path) => {
let list: List<NodeId> = path.into_iter().map(core_types::list::Item::new_from_element).collect();
Ok(value_edge(list))
}
Self::DocumentNode(node) => Ok(value_edge(node)),
Self::ContextModification(modification) => Ok(value_edge(modification)),
Self::EditorApi(x) => Ok(value_edge(x)),
Self::ResourceHash(x) => Ok(value_edge(x)),
}
}
/// Evaluates a typed edge and converts the landed value into a tagged value, with the coverage of [`Self::try_from_any`].
pub fn from_edge(handle: EdgeHandle, ctx: &Context) -> Result<GPoll<Self>, String> {
let ty = handle.ty().clone();
// ===============
// MANUAL VARIANTS
// ===============
if ty == edge_type::<()>() {
return Ok(handle.downcast::<()>().map_err(|e| format!("{e:?}"))?.eval(ctx).map(|_| TaggedValue::None));
}
// =======================
// AUTO-GENERATED VARIANTS
// =======================
$(
if ty == edge_type::<$ty>() {
return Ok(handle.downcast::<$ty>().map_err(|e| format!("{e:?}"))?.eval(ctx).map(TaggedValue::$identifier));
}
)*
// =======================
// NON-SERIALIZED VARIANTS
// =======================
if ty == edge_type::<RenderOutput>() {
return Ok(handle.downcast::<RenderOutput>().map_err(|e| format!("{e:?}"))?.eval(ctx).map(TaggedValue::RenderOutput));
}
Err(format!("Cannot convert edge of type {ty} to TaggedValue"))
}
/// Attempts to downcast the dynamic type to a tagged value /// Attempts to downcast the dynamic type to a tagged value
pub fn try_from_any(input: Box<dyn DynAny<'a> + 'a>) -> Result<Self, String> { pub fn try_from_any(input: Box<dyn DynAny<'a> + 'a>) -> Result<Self, String> {
use dyn_any::downcast; use dyn_any::downcast;
@@ -360,7 +440,7 @@ macro_rules! tagged_value {
Self::RenderOutput(_) => "RenderOutput".to_string(), Self::RenderOutput(_) => "RenderOutput".to_string(),
Self::NodeIdPath(path) => format!("NodeIdPath({path:?})"), Self::NodeIdPath(path) => format!("NodeIdPath({path:?})"),
Self::DocumentNode(node) => format!("DocumentNode({node:?})"), Self::DocumentNode(node) => format!("DocumentNode({node:?})"),
Self::ContextFeatures(features) => format!("ContextFeatures({features:?})"), Self::ContextModification(modification) => format!("ContextModification({modification:?})"),
Self::EditorApi(_) => "PlatformEditorApi".to_string(), Self::EditorApi(_) => "PlatformEditorApi".to_string(),
Self::ResourceHash(hash) => format!("ResourceHash({hash:?})"), Self::ResourceHash(hash) => format!("ResourceHash({hash:?})"),
} }
@@ -686,39 +766,6 @@ impl Display for TaggedValue {
} }
} }
pub struct UpcastNode {
value: MemoHash<TaggedValue>,
}
impl<'input> Node<'input, DAny<'input>> for UpcastNode {
type Output = FutureAny<'input>;
fn eval(&'input self, _: DAny<'input>) -> Self::Output {
let memo_clone = MemoHash::clone(&self.value);
Box::pin(async move { memo_clone.into_inner().as_ref().clone().to_dynany() })
}
}
impl UpcastNode {
pub fn new(value: MemoHash<TaggedValue>) -> Self {
Self { value }
}
}
#[derive(Default, Debug, Clone, Copy)]
pub struct UpcastAsRefNode<T: AsRef<U> + Sync + Send, U: Sync + Send>(pub T, PhantomData<U>);
impl<'i, T: 'i + AsRef<U> + Sync + Send, U: 'i + StaticType + Sync + Send> Node<'i, DAny<'i>> for UpcastAsRefNode<T, U> {
type Output = FutureAny<'i>;
#[inline(always)]
fn eval(&'i self, _: DAny<'i>) -> Self::Output {
Box::pin(async move { Box::new(self.0.as_ref()) as DAny<'i> })
}
}
impl<T: AsRef<U> + Sync + Send, U: Sync + Send> UpcastAsRefNode<T, U> {
pub const fn new(value: T) -> UpcastAsRefNode<T, U> {
UpcastAsRefNode(value, PhantomData)
}
}
#[derive(Debug, Clone, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)]
pub struct RenderOutput { pub struct RenderOutput {
pub data: RenderOutputType, pub data: RenderOutputType,
@@ -1,5 +1,5 @@
use crate::document::NodeNetwork; use crate::document::NodeNetwork;
use crate::proto::{LocalFuture, ProtoNetwork}; use crate::proto::ProtoNetwork;
use std::error::Error; use std::error::Error;
pub struct Compiler {} pub struct Compiler {}
@@ -33,5 +33,5 @@ impl Compiler {
} }
pub trait Executor<I, O> { pub trait Executor<I, O> {
fn execute(&self, input: I) -> LocalFuture<'_, Result<O, Box<dyn Error>>>; fn execute(&self, input: I) -> Result<O, Box<dyn Error>>;
} }
+153 -35
View File
@@ -294,6 +294,10 @@ impl ProtoNetwork {
(inwards_edges, id_map) (inwards_edges, id_map)
} }
pub fn source_ids(&self) -> Vec<SourceId> {
self.nodes.iter().flat_map(|(_, node)| node.context_features.sources().iter().copied()).collect()
}
/// Inserts context nullification nodes to optimize caching. /// Inserts context nullification nodes to optimize caching.
/// This analysis is performed after topological sorting to ensure proper dependency tracking. /// This analysis is performed after topological sorting to ensure proper dependency tracking.
pub fn insert_context_nullification_nodes(&mut self) -> Result<(), String> { pub fn insert_context_nullification_nodes(&mut self) -> Result<(), String> {
@@ -308,7 +312,7 @@ impl ProtoNetwork {
Ok(()) Ok(())
} }
fn insert_context_nullification_node(&mut self, node_id: NodeId, context_deps: ContextFeatures) -> NodeId { fn insert_context_nullification_node(&mut self, node_id: NodeId, context_deps: ContextModification) -> NodeId {
let (_, node) = &self.nodes[node_id.0 as usize]; let (_, node) = &self.nodes[node_id.0 as usize];
let mut path = node.original_location.path.clone(); let mut path = node.original_location.path.clone();
@@ -338,7 +342,7 @@ impl ProtoNetwork {
self.nodes.push(( self.nodes.push((
nullification_value_node_id, nullification_value_node_id,
ProtoNode { ProtoNode {
construction_args: ConstructionArgs::Value(MemoHash::new(TaggedValue::ContextFeatures(context_deps))), construction_args: ConstructionArgs::Value(MemoHash::new(TaggedValue::ContextModification(context_deps))),
call_argument: concrete!(Context), call_argument: concrete!(Context),
identifier: ProtoNodeIdentifier::new("core_types::value::ClonedNode"), identifier: ProtoNodeIdentifier::new("core_types::value::ClonedNode"),
original_location: OriginalLocation { original_location: OriginalLocation {
@@ -365,42 +369,43 @@ impl ProtoNetwork {
nullification_node_id nullification_node_id
} }
fn find_context_dependencies(&mut self, id: NodeId) -> (ContextFeatures, Option<NodeId>) { fn find_context_dependencies(&mut self, id: NodeId) -> (ContextModification, Option<NodeId>) {
let mut branch_dependencies = Vec::new(); let mut branch_dependencies = Vec::new();
let mut combined_deps = ContextFeatures::default(); let mut combined_deps = ContextModification::default();
let node_index = id.0 as usize; let node_index = id.0 as usize;
let (extract, inject) = { let (extract, inject, own_deps) = {
let dependencies = &self.nodes[node_index].1.context_features; let dependencies = &self.nodes[node_index].1.context_features;
(dependencies.extract, dependencies.inject) let own_deps = ContextModification::from_sources(dependencies.extract, dependencies.sources());
(dependencies.extract, dependencies.inject, own_deps)
}; };
let mut inputs = match &self.nodes[node_index].1.construction_args { let mut inputs = match &self.nodes[node_index].1.construction_args {
// We pretend like we have already placed context modification nodes after ourselves because value nodes don't need to be cached // We pretend like we have already placed context modification nodes after ourselves because value nodes don't need to be cached
ConstructionArgs::Value(_) => return (extract, Some(id)), ConstructionArgs::Value(_) => return (own_deps, Some(id)),
ConstructionArgs::Nodes(items) => items.clone(), ConstructionArgs::Nodes(items) => items.clone(),
ConstructionArgs::Inline(_) => return (extract, Some(id)), ConstructionArgs::Inline(_) => return (own_deps, Some(id)),
}; };
// Compute the dependencies for each branch and combine all of them // Compute the dependencies for each branch and combine all of them
for &node in &inputs { for &node in &inputs {
let branch = self.find_context_dependencies(node); let branch = self.find_context_dependencies(node);
combined_deps |= &branch.0;
branch_dependencies.push(branch); branch_dependencies.push(branch);
combined_deps |= branch.0;
} }
let mut new_deps = combined_deps; let mut new_deps = combined_deps.clone();
// Remove requirements which this node provides // Remove requirements which this node provides
new_deps &= !inject; new_deps &= !inject;
// Add requirements we have // Add requirements we have
new_deps |= extract; new_deps |= own_deps;
// If we either introduce new dependencies, we can cache all children which don't yet need that dependency // If we either introduce new dependencies, we can cache all children which don't yet need that dependency
let we_introduce_new_deps = !combined_deps.contains(new_deps); let we_introduce_new_deps = !combined_deps.contains(&new_deps);
// For diverging branches, we can add a cache node for all branches which don't reqire all dependencies // For diverging branches, we can add a cache node for all branches which don't reqire all dependencies
for (child_node, (deps, new_id)) in inputs.iter_mut().zip(branch_dependencies.into_iter()) { for (child_node, (deps, new_id)) in inputs.iter_mut().zip(branch_dependencies) {
if let Some(new_id) = new_id { if let Some(new_id) = new_id {
*child_node = new_id; *child_node = new_id;
} else if we_introduce_new_deps || deps != combined_deps { } else if we_introduce_new_deps || deps != combined_deps {
@@ -413,15 +418,15 @@ impl ProtoNetwork {
let net_injections = inject.difference(extract); let net_injections = inject.difference(extract);
// Which dependencies still need to be met after this node? // Which dependencies still need to be met after this node?
let remaining_deps_from_children = combined_deps.difference(net_injections); let remaining_deps_from_children = combined_deps.features.difference(net_injections);
// Do we satisfy any existing dependencies? // Do we satisfy any existing dependencies?
let we_supply_existing_deps = !combined_deps.difference(remaining_deps_from_children).is_empty(); let we_supply_existing_deps = !combined_deps.features.difference(remaining_deps_from_children).is_empty();
let mut new_id = None; let mut new_id = None;
if we_supply_existing_deps { if we_supply_existing_deps {
// Our set of context dependencies has shrunk so we can add a cache node after the current node // Our set of context dependencies has shrunk so we can add a cache node after the current node
new_id = Some(self.insert_context_nullification_node(id, new_deps)); new_id = Some(self.insert_context_nullification_node(id, new_deps.clone()));
} }
(new_deps, new_id) (new_deps, new_id)
@@ -548,6 +553,7 @@ pub enum GraphErrorType {
}, },
NoImplementations, NoImplementations,
NoConstructor, NoConstructor,
ConstructionFailed(String),
/// The `inputs` represents a formatted list of input indices corresponding to their types. /// The `inputs` represents a formatted list of input indices corresponding to their types.
/// Each element in `error_inputs` represents a valid `NodeIOTypes` implementation. /// Each element in `error_inputs` represents a valid `NodeIOTypes` implementation.
/// The inner Vec stores the inputs which need to be changed and what type each needs to be changed to. /// The inner Vec stores the inputs which need to be changed and what type each needs to be changed to.
@@ -568,6 +574,7 @@ impl Debug for GraphErrorType {
GraphErrorType::UnexpectedGenerics { index, inputs } => write!(f, "Generic inputs should not exist but found at {index}: {inputs:?}"), GraphErrorType::UnexpectedGenerics { index, inputs } => write!(f, "Generic inputs should not exist but found at {index}: {inputs:?}"),
GraphErrorType::NoImplementations => write!(f, "No implementations found"), GraphErrorType::NoImplementations => write!(f, "No implementations found"),
GraphErrorType::NoConstructor => write!(f, "No construct found for node"), GraphErrorType::NoConstructor => write!(f, "No construct found for node"),
GraphErrorType::ConstructionFailed(error) => write!(f, "Construction failed: {error}"),
GraphErrorType::InvalidImplementations { inputs, error_inputs } => { GraphErrorType::InvalidImplementations { inputs, error_inputs } => {
let format_error = |(index, (found, expected)): &(usize, (Type, Type))| { let format_error = |(index, (found, expected)): &(usize, (Type, Type))| {
let index = index + 1; let index = index + 1;
@@ -634,14 +641,14 @@ pub type GraphErrors = Vec<GraphError>;
/// The `TypingContext` is used to store the types of the nodes indexed by their stable node id. /// The `TypingContext` is used to store the types of the nodes indexed by their stable node id.
#[derive(Default, Clone, dyn_any::DynAny)] #[derive(Default, Clone, dyn_any::DynAny)]
pub struct TypingContext { pub struct TypingContext {
lookup: Cow<'static, HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>>>, lookup: Cow<'static, HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>>>,
inferred: HashMap<NodeId, NodeIOTypes>, inferred: HashMap<NodeId, NodeIOTypes>,
constructor: HashMap<NodeId, NodeConstructor>, constructor: HashMap<NodeId, NodeConstructor>,
} }
impl TypingContext { impl TypingContext {
/// Creates a new `TypingContext` with the given lookup table. /// Creates a new `TypingContext` with the given lookup table.
pub fn new(lookup: &'static HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>>) -> Self { pub fn new(lookup: &'static HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>>) -> Self {
Self { Self {
lookup: Cow::Borrowed(lookup), lookup: Cow::Borrowed(lookup),
..Default::default() ..Default::default()
@@ -685,7 +692,7 @@ impl TypingContext {
// If the node has a value input we can infer the return type from it // If the node has a value input we can infer the return type from it
ConstructionArgs::Value(ref v) => { ConstructionArgs::Value(ref v) => {
// TODO: This should return a reference to the value // TODO: This should return a reference to the value
let types = NodeIOTypes::new(concrete!(Context), Type::Future(Box::new(v.ty())), vec![]); let types = NodeIOTypes::new(concrete!(Context), v.ty(), vec![]);
self.inferred.insert(node_id, types.clone()); self.inferred.insert(node_id, types.clone());
return Ok(types); return Ok(types);
} }
@@ -705,6 +712,7 @@ impl TypingContext {
// Get the node input type from the proto node declaration // Get the node input type from the proto node declaration
let call_argument = &node.call_argument; let call_argument = &node.call_argument;
let impls = self.lookup.get(&node.identifier).ok_or_else(|| vec![GraphError::new(node, GraphErrorType::NoImplementations)])?; let impls = self.lookup.get(&node.identifier).ok_or_else(|| vec![GraphError::new(node, GraphErrorType::NoImplementations)])?;
let candidates: Vec<(NodeIOTypes, NodeConstructor)> = impls.iter().map(|entry| (entry.io.clone(), entry.constructor)).collect();
if let Some(index) = inputs.iter().position(|p| { if let Some(index) = inputs.iter().position(|p| {
matches!(p, matches!(p,
@@ -719,8 +727,6 @@ impl TypingContext {
match (from, to) { match (from, to) {
// Direct comparison of two concrete types. // Direct comparison of two concrete types.
(Type::Concrete(type1), Type::Concrete(type2)) => type1 == type2, (Type::Concrete(type1), Type::Concrete(type2)) => type1 == type2,
// Check inner type for futures
(Type::Future(type1), Type::Future(type2)) => valid_type(type1, type2),
// Direct comparison of two function types. // Direct comparison of two function types.
// Note: in the presence of subtyping, functions are considered on a "greater than or equal to" basis of its function type's generality. // Note: in the presence of subtyping, functions are considered on a "greater than or equal to" basis of its function type's generality.
// That means we compare their types with a contravariant relationship, which means that a more general type signature may be substituted for a more specific type signature. // That means we compare their types with a contravariant relationship, which means that a more general type signature may be substituted for a more specific type signature.
@@ -743,25 +749,24 @@ impl TypingContext {
} }
// List of all implementations that match the input types // List of all implementations that match the input types
let valid_output_types = impls let valid_output_types = candidates
.keys() .iter()
.filter(|node_io| valid_type(&node_io.call_argument, call_argument) && inputs.iter().zip(node_io.inputs.iter()).all(|(p1, p2)| valid_type(p1, p2))) .filter(|(node_io, _)| valid_type(&node_io.call_argument, call_argument) && inputs.iter().zip(node_io.inputs.iter()).all(|(p1, p2)| valid_type(p1, p2)))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// Attempt to substitute generic types with concrete types and save the list of results // Attempt to substitute generic types with concrete types and save the list of results
let substitution_results = valid_output_types let substitution_results = valid_output_types
.iter() .iter()
.map(|node_io| { .map(|(node_io, constructor)| {
let generics_lookup: Result<HashMap<_, _>, _> = collect_generics(node_io) let generics_lookup: Result<HashMap<_, _>, _> = collect_generics(node_io)
.iter() .iter()
.map(|generic| check_generic(node_io, call_argument, &inputs, generic).map(|x| (generic.to_string(), x))) .map(|generic| check_generic(node_io, call_argument, &inputs, generic).map(|x| (generic.to_string(), x)))
.collect(); .collect();
generics_lookup.map(|generics_lookup| { generics_lookup.map(|generics_lookup| {
let orig_node_io = (*node_io).clone(); let mut new_node_io = node_io.clone();
let mut new_node_io = orig_node_io.clone();
replace_generics(&mut new_node_io, &generics_lookup); replace_generics(&mut new_node_io, &generics_lookup);
(new_node_io, orig_node_io) (new_node_io, *constructor)
}) })
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -774,7 +779,7 @@ impl TypingContext {
let convert_node_index_offset = node.original_location.auto_convert_index.unwrap_or(0); let convert_node_index_offset = node.original_location.auto_convert_index.unwrap_or(0);
let mut best_errors = usize::MAX; let mut best_errors = usize::MAX;
let mut error_inputs = Vec::new(); let mut error_inputs = Vec::new();
for node_io in impls.keys() { for (node_io, _) in &candidates {
// For errors on Convert nodes, offset the input index so it correctly corresponds to the node it is connected to. // For errors on Convert nodes, offset the input index so it correctly corresponds to the node it is connected to.
let current_errors = [call_argument] let current_errors = [call_argument]
.into_iter() .into_iter()
@@ -809,36 +814,36 @@ impl TypingContext {
.join("\n"); .join("\n");
Err(vec![GraphError::new(node, GraphErrorType::InvalidImplementations { inputs, error_inputs })]) Err(vec![GraphError::new(node, GraphErrorType::InvalidImplementations { inputs, error_inputs })])
} }
[(node_io, org_nio)] => { [(node_io, constructor)] => {
let node_io = node_io.clone(); let node_io = node_io.clone();
// Save the inferred type // Save the inferred type
self.inferred.insert(node_id, node_io.clone()); self.inferred.insert(node_id, node_io.clone());
self.constructor.insert(node_id, impls[org_nio]); self.constructor.insert(node_id, *constructor);
Ok(node_io) Ok(node_io)
} }
// If two types are available and one of them accepts () an input, always choose that one // If two types are available and one of them accepts () an input, always choose that one
[first, second] => { [first, second] => {
if first.0.call_argument != second.0.call_argument { if first.0.call_argument != second.0.call_argument {
for (node_io, orig_nio) in [first, second] { for (node_io, constructor) in [first, second] {
if node_io.call_argument != concrete!(()) { if node_io.call_argument != concrete!(()) {
continue; continue;
} }
// Save the inferred type // Save the inferred type
self.inferred.insert(node_id, node_io.clone()); self.inferred.insert(node_id, node_io.clone());
self.constructor.insert(node_id, impls[orig_nio]); self.constructor.insert(node_id, *constructor);
return Ok(node_io.clone()); return Ok(node_io.clone());
} }
} }
let inputs = [call_argument].into_iter().chain(&inputs).map(ToString::to_string).collect::<Vec<_>>().join(", "); let inputs = [call_argument].into_iter().chain(&inputs).map(ToString::to_string).collect::<Vec<_>>().join(", ");
let valid = valid_output_types.into_iter().cloned().collect(); let valid = valid_output_types.into_iter().map(|(node_io, _)| node_io.clone()).collect();
Err(vec![GraphError::new(node, GraphErrorType::MultipleImplementations { inputs, valid })]) Err(vec![GraphError::new(node, GraphErrorType::MultipleImplementations { inputs, valid })])
} }
_ => { _ => {
let inputs = [call_argument].into_iter().chain(&inputs).map(ToString::to_string).collect::<Vec<_>>().join(", "); let inputs = [call_argument].into_iter().chain(&inputs).map(ToString::to_string).collect::<Vec<_>>().join(", ");
let valid = valid_output_types.into_iter().cloned().collect(); let valid = valid_output_types.into_iter().map(|(node_io, _)| node_io.clone()).collect();
Err(vec![GraphError::new(node, GraphErrorType::MultipleImplementations { inputs, valid })]) Err(vec![GraphError::new(node, GraphErrorType::MultipleImplementations { inputs, valid })])
} }
} }
@@ -958,6 +963,81 @@ mod test {
); );
} }
#[test]
fn retain_filter_placement_on_source_free_branch() {
let mut network = source_branch_network(vec![1], vec![]);
network.insert_context_nullification_nodes().expect("Error when calling 'insert_context_nullification_nodes'");
let filters = nullification_filters(&network);
assert_eq!(filters.len(), 1, "only the source-free branch gets a filter");
let (filter_id, wrapped, retained) = &filters[0];
assert_eq!(wrapped, "source_b");
assert!(retained.is_empty(), "the source-free branch retains no sources");
let (source_a_id, _) = find_node(&network, "source_a");
let (_, join) = find_node(&network, "join");
let ConstructionArgs::Nodes(join_args) = &join.construction_args else {
panic!("join args must be nodes")
};
assert_eq!(join_args, &vec![source_a_id, *filter_id], "the source branch stays direct, the filter replaces the source-free branch");
}
#[test]
fn diverging_source_sets_filter_each_branch() {
let mut network = source_branch_network(vec![1], vec![2]);
network.insert_context_nullification_nodes().expect("Error when calling 'insert_context_nullification_nodes'");
let mut filters = nullification_filters(&network);
filters.sort_by(|(_, a, _), (_, b, _)| a.cmp(b));
let summary: Vec<_> = filters.iter().map(|(_, wrapped, retained)| (wrapped.as_str(), retained.as_slice())).collect();
assert_eq!(
summary,
vec![("source_a", &[1u64][..]), ("source_b", &[2u64][..])],
"each diverging branch is filtered down to its own source set"
);
}
#[test]
fn matching_source_sets_insert_no_filter() {
let mut network = source_branch_network(vec![1], vec![1]);
network.insert_context_nullification_nodes().expect("Error when calling 'insert_context_nullification_nodes'");
assert!(nullification_filters(&network).is_empty(), "equal branch source sets need no filter");
}
fn find_node<'a>(network: &'a ProtoNetwork, name: &str) -> (NodeId, &'a ProtoNode) {
network
.nodes
.iter()
.find(|(_, node)| node.identifier.as_str() == name)
.map(|(id, node)| (*id, node))
.unwrap_or_else(|| panic!("node {name} not found"))
}
fn nullification_filters(network: &ProtoNetwork) -> Vec<(NodeId, String, Vec<SourceId>)> {
let node = |id: NodeId| &network.nodes[id.0 as usize].1;
network
.nodes
.iter()
.filter(|(_, candidate)| candidate.identifier.as_str() == graphene_core::context_modification::context_modification::IDENTIFIER.as_str())
.map(|(id, candidate)| {
let ConstructionArgs::Nodes(args) = &candidate.construction_args else {
panic!("filter args must be nodes")
};
let ConstructionArgs::Nodes(memoized) = &node(args[0]).construction_args else {
panic!("filter memoize args must be nodes")
};
let ConstructionArgs::Value(value) = &node(args[1]).construction_args else {
panic!("filter payload must be a value")
};
let value::TaggedValue::ContextModification(modification) = &**value else {
panic!("filter payload must be a context modification")
};
(*id, node(memoized[0]).identifier.as_str().to_string(), modification.sources().to_vec())
})
.collect()
}
fn test_network() -> ProtoNetwork { fn test_network() -> ProtoNetwork {
ProtoNetwork { ProtoNetwork {
inputs: vec![NodeId(10)], inputs: vec![NodeId(10)],
@@ -1014,6 +1094,44 @@ mod test {
} }
} }
fn source_branch_network(branch_a_sources: Vec<SourceId>, branch_b_sources: Vec<SourceId>) -> ProtoNetwork {
let branch = |name: &str, sources: Vec<SourceId>| ProtoNode {
identifier: ProtoNodeIdentifier::with_owned_string(name.to_string()),
call_argument: concrete!(()),
construction_args: ConstructionArgs::Nodes(vec![NodeId(0)]),
context_features: ContextDependencies::from_sources(&sources),
..Default::default()
};
ProtoNetwork {
inputs: vec![],
output: NodeId(3),
nodes: [
(
NodeId(0),
ProtoNode {
identifier: ProtoNodeIdentifier::new("value"),
call_argument: concrete!(()),
construction_args: ConstructionArgs::Value(value::TaggedValue::U32(2).into()),
..Default::default()
},
),
(NodeId(1), branch("source_a", branch_a_sources)),
(NodeId(2), branch("source_b", branch_b_sources)),
(
NodeId(3),
ProtoNode {
identifier: ProtoNodeIdentifier::new("join"),
call_argument: concrete!(()),
construction_args: ConstructionArgs::Nodes(vec![NodeId(1), NodeId(2)]),
..Default::default()
},
),
]
.into_iter()
.collect(),
}
}
fn test_network_with_cycles() -> ProtoNetwork { fn test_network_with_cycles() -> ProtoNetwork {
ProtoNetwork { ProtoNetwork {
inputs: vec![NodeId(1)], inputs: vec![NodeId(1)],
+25 -9
View File
@@ -1,7 +1,9 @@
use futures::executor::block_on;
use graph_craft::document::value::{RenderOutputType, TaggedValue, UVec2}; use graph_craft::document::value::{RenderOutputType, TaggedValue, UVec2};
use graph_craft::graphene_compiler::Executor; use graph_craft::graphene_compiler::Executor;
use graphene_std::application_io::{ExportFormat, RenderConfig, TimingInformation}; use graphene_std::application_io::{ExportFormat, RenderConfig, TimingInformation};
use graphene_std::core_types::ops::Convert; use graphene_std::core_types::gpoll::GPoll;
use graphene_std::core_types::ops::ConvertAsync;
use graphene_std::core_types::transform::Footprint; use graphene_std::core_types::transform::Footprint;
use graphene_std::raster_types::{CPU, GPU, Raster}; use graphene_std::raster_types::{CPU, GPU, Raster};
use interpreted_executor::dynamic_executor::DynamicExecutor; use interpreted_executor::dynamic_executor::DynamicExecutor;
@@ -10,6 +12,19 @@ use std::io::Cursor;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::Duration; use std::time::Duration;
fn execute_to_final(executor: &DynamicExecutor, render_config: RenderConfig) -> Result<TaggedValue, Box<dyn Error>> {
match executor.execute(render_config)? {
GPoll::Final(value) => Ok(value),
GPoll::Fallback(boxed) => {
let (value, error) = *boxed;
log::error!("Node graph evaluation reported an error alongside its fallback output: {error:?}");
Ok(value)
}
GPoll::Partial(_) | GPoll::Pending => Err("Node graph evaluation did not complete".into()),
GPoll::Error(error) => Err(format!("Node graph evaluation failed: {error:?}").into()),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileType { pub enum FileType {
Svg, Svg,
@@ -28,9 +43,10 @@ pub fn detect_file_type(path: &Path) -> Result<FileType, String> {
} }
} }
pub async fn export_document( #[allow(clippy::too_many_arguments)]
pub fn export_document(
executor: &DynamicExecutor, executor: &DynamicExecutor,
wgpu_executor: &wgpu_executor::WgpuExecutor, wgpu_executor: wgpu_executor::WgpuExecutorHandle,
output_path: PathBuf, output_path: PathBuf,
file_type: FileType, file_type: FileType,
scale: f64, scale: f64,
@@ -57,7 +73,7 @@ pub async fn export_document(
} }
// Execute the graph // Execute the graph
let result = executor.execute(render_config).await?; let result = execute_to_final(executor, render_config)?;
// Handle the result based on output type // Handle the result based on output type
match result { match result {
@@ -70,7 +86,7 @@ pub async fn export_document(
RenderOutputType::Texture(texture) => { RenderOutputType::Texture(texture) => {
// Convert GPU texture to CPU buffer // Convert GPU texture to CPU buffer
let gpu_raster = Raster::<GPU>::new_gpu(texture); let gpu_raster = Raster::<GPU>::new_gpu(texture);
let cpu_raster: Raster<CPU> = gpu_raster.convert(Footprint::BOUNDLESS, wgpu_executor).await; let cpu_raster: Raster<CPU> = block_on(gpu_raster.convert(Footprint::BOUNDLESS, wgpu_executor.clone()));
let (data, width, height) = cpu_raster.to_flat_u8(); let (data, width, height) = cpu_raster.to_flat_u8();
// Encode and write raster image // Encode and write raster image
@@ -149,9 +165,9 @@ impl AnimationParams {
} }
/// Export an animated GIF by rendering multiple frames at different animation times /// Export an animated GIF by rendering multiple frames at different animation times
pub async fn export_gif( pub fn export_gif(
executor: &DynamicExecutor, executor: &DynamicExecutor,
wgpu_executor: &wgpu_executor::WgpuExecutor, wgpu_executor: wgpu_executor::WgpuExecutorHandle,
output_path: PathBuf, output_path: PathBuf,
scale: f64, scale: f64,
(width, height): (Option<u32>, Option<u32>), (width, height): (Option<u32>, Option<u32>),
@@ -195,14 +211,14 @@ pub async fn export_gif(
} }
// Execute the graph for this frame // Execute the graph for this frame
let result = executor.execute(render_config).await?; let result = execute_to_final(executor, render_config)?;
// Extract RGBA data from result // Extract RGBA data from result
let (data, img_width, img_height) = match result { let (data, img_width, img_height) = match result {
TaggedValue::RenderOutput(output) => match output.data { TaggedValue::RenderOutput(output) => match output.data {
RenderOutputType::Texture(texture) => { RenderOutputType::Texture(texture) => {
let gpu_raster = Raster::<GPU>::new_gpu(texture); let gpu_raster = Raster::<GPU>::new_gpu(texture);
let cpu_raster: Raster<CPU> = gpu_raster.convert(Footprint::BOUNDLESS, wgpu_executor).await; let cpu_raster: Raster<CPU> = block_on(gpu_raster.convert(Footprint::BOUNDLESS, wgpu_executor.clone()));
cpu_raster.to_flat_u8() cpu_raster.to_flat_u8()
} }
RenderOutputType::Buffer { data, width, height } => (data, width, height), RenderOutputType::Buffer { data, width, height } => (data, width, height),
+15 -15
View File
@@ -7,13 +7,13 @@ use document_format::{GddV1, GddV1Layout};
use fern::colors::{Color, ColoredLevelConfig}; use fern::colors::{Color, ColoredLevelConfig};
use futures::executor::block_on; use futures::executor::block_on;
use graph_craft::application_io::EditorPreferences; use graph_craft::application_io::EditorPreferences;
use graph_craft::application_io::resource::ResourceRegistry;
use graph_craft::application_io::{PlatformApplicationIo, PlatformEditorApi}; use graph_craft::application_io::{PlatformApplicationIo, PlatformEditorApi};
use graph_craft::document::*; use graph_craft::document::*;
use graph_craft::graphene_compiler::Compiler; use graph_craft::graphene_compiler::Compiler;
use graph_craft::proto::ProtoNetwork; use graph_craft::proto::ProtoNetwork;
use graph_craft::util::load_network; use graph_craft::util::load_network;
use graphene_std::application_io::{ApplicationIo, NodeGraphUpdateMessage, NodeGraphUpdateSender}; use graphene_std::application_io::{ApplicationIo, NodeGraphUpdateMessage, NodeGraphUpdateSender};
use graphene_std::runtime::{DynGraphRuntime, DynSpawner, GraphRuntime, NoopSpawner, RuntimeHandle};
use interpreted_executor::dynamic_executor::DynamicExecutor; use interpreted_executor::dynamic_executor::DynamicExecutor;
use interpreted_executor::util::wrap_network_in_scope; use interpreted_executor::util::wrap_network_in_scope;
use std::error::Error; use std::error::Error;
@@ -101,8 +101,7 @@ struct GlobalOpts {
verbose: u8, verbose: u8,
} }
#[tokio::main] fn main() -> Result<(), Box<dyn Error>> {
async fn main() -> Result<(), Box<dyn Error>> {
let app = App::parse(); let app = App::parse();
let log_level = app.global_opts.verbose; let log_level = app.global_opts.verbose;
@@ -130,9 +129,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
let gdd = if is_gdd { let gdd = if is_gdd {
let archive = std::fs::read(document_path).map_err(|error| format!("Failed to read document {}: {error}", document_path.display()))?; let archive = std::fs::read(document_path).map_err(|error| format!("Failed to read document {}: {error}", document_path.display()))?;
let container = AnyContainer::Memory(MemoryBackend::new()); let container = AnyContainer::Memory(MemoryBackend::new());
let gdd = document_format::Gdd::open_from_archive(archive.as_ref(), container, GddV1Layout) let gdd = block_on(document_format::Gdd::open_from_archive(archive.as_ref(), container, GddV1Layout)).map_err(|error| format!("Failed to open document: {error}"))?;
.await
.map_err(|error| format!("Failed to open document: {error}"))?;
Some(gdd) Some(gdd)
} else { } else {
None None
@@ -140,7 +137,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
if let Command::ExtractLegacyDoc { ref document } = app.command { if let Command::ExtractLegacyDoc { ref document } = app.command {
let Some(gdd) = &gdd else { return Err("ExtractLegacyDoc requires a .gdd document".into()) }; let Some(gdd) = &gdd else { return Err("ExtractLegacyDoc requires a .gdd document".into()) };
let Some(legacy_doc) = gdd.read_legacy_document().await else { let Some(legacy_doc) = block_on(gdd.read_legacy_document()) else {
return Err("gdd file did not contain a legacy .graphite document".into()); return Err("gdd file did not contain a legacy .graphite document".into());
}; };
let mut new_path = document.clone(); let mut new_path = document.clone();
@@ -153,7 +150,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
// Build the runtime network: from the `.gdd` registry, or by loading a legacy `.graphite` document. // Build the runtime network: from the `.gdd` registry, or by loading a legacy `.graphite` document.
let node_network = match &gdd { let node_network = match &gdd {
Some(gdd) => { Some(gdd) => {
let declarations = gdd.declarations(gdd).await; let declarations = block_on(gdd.declarations(gdd));
let (node_network, _metadata) = gdd.registry().to_runtime_with_metadata(&declarations)?; let (node_network, _metadata) = gdd.registry().to_runtime_with_metadata(&declarations)?;
node_network node_network
} }
@@ -164,7 +161,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
}; };
log::info!("Creating GPU context"); log::info!("Creating GPU context");
let mut application_io = PlatformApplicationIo::new().await; let mut application_io = block_on(PlatformApplicationIo::new());
if let Some(gdd) = &gdd { if let Some(gdd) = &gdd {
application_io.inject_resource_proxy(Box::new(gdd.resource_proxy())); application_io.inject_resource_proxy(Box::new(gdd.resource_proxy()));
} }
@@ -176,16 +173,18 @@ async fn main() -> Result<(), Box<dyn Error>> {
let application_io_for_api = application_io_arc.clone(); let application_io_for_api = application_io_arc.clone();
// Get reference to wgpu executor and clone device handle // Get reference to wgpu executor and clone device handle
let wgpu_executor_ref = application_io_arc.gpu_executor().unwrap(); let wgpu_executor_ref = wgpu_executor::WgpuExecutorHandle(application_io_arc.gpu_executor_arc().unwrap());
let device = wgpu_executor_ref.context().device.clone(); let device = wgpu_executor_ref.context().device.clone();
let preferences = EditorPreferences { let preferences = EditorPreferences {
max_render_region_size: EditorPreferences::default().max_render_region_size, max_render_region_size: EditorPreferences::default().max_render_region_size,
}; };
let graph_runtime: Arc<DynGraphRuntime> = Arc::new(GraphRuntime::new(Box::new(NoopSpawner) as Box<DynSpawner>));
let editor_api = Arc::new(PlatformEditorApi { let editor_api = Arc::new(PlatformEditorApi {
application_io: Some(application_io_for_api), application_io: Some(application_io_for_api),
node_graph_message_sender: Box::new(UpdateLogger {}), node_graph_message_sender: Box::new(UpdateLogger {}),
editor_preferences: Box::new(preferences), editor_preferences: Box::new(preferences),
runtime: RuntimeHandle(graph_runtime.clone()),
}); });
let proto_graph = compile_graph(node_network, editor_api, gdd.as_ref())?; let proto_graph = compile_graph(node_network, editor_api, gdd.as_ref())?;
@@ -218,7 +217,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
let file_type = export::detect_file_type(&output)?; let file_type = export::detect_file_type(&output)?;
// Create executor // Create executor
let executor = create_executor(proto_graph)?; let executor = create_executor(proto_graph, graph_runtime)?;
if fps <= 0. { if fps <= 0. {
return Err("Fps number must be positive".into()); return Err("Fps number must be positive".into());
@@ -227,9 +226,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
// Perform export based on file type // Perform export based on file type
if file_type == export::FileType::Gif { if file_type == export::FileType::Gif {
let animation = export::AnimationParams::new(fps, frames, duration); let animation = export::AnimationParams::new(fps, frames, duration);
export::export_gif(&executor, wgpu_executor_ref, output, scale, (width, height), animation).await?; export::export_gif(&executor, wgpu_executor_ref.clone(), output, scale, (width, height), animation)?;
} else { } else {
export::export_document(&executor, wgpu_executor_ref, output, file_type, scale, (width, height), transparent).await?; export::export_document(&executor, wgpu_executor_ref.clone(), output, file_type, scale, (width, height), transparent)?;
} }
} }
_ => unreachable!("All other commands should be handled before this match statement is run"), _ => unreachable!("All other commands should be handled before this match statement is run"),
@@ -285,7 +284,8 @@ fn compile_graph(network: NodeNetwork, editor_api: Arc<PlatformEditorApi>, gdd:
compiler.compile_single(network).map_err(|x| x.into()) compiler.compile_single(network).map_err(|x| x.into())
} }
fn create_executor(proto_network: ProtoNetwork) -> Result<DynamicExecutor, Box<dyn Error>> { fn create_executor(proto_network: ProtoNetwork, runtime: Arc<DynGraphRuntime>) -> Result<DynamicExecutor, Box<dyn Error>> {
let executor = block_on(DynamicExecutor::new(proto_network)).map_err(|errors| errors.iter().map(|e| format!("{e:?}")).reduce(|acc, e| format!("{acc}\n{e}")).unwrap_or_default())?; let mut executor = DynamicExecutor::new(proto_network).map_err(|errors| errors.iter().map(|e| format!("{e:?}")).reduce(|acc, e| format!("{acc}\n{e}")).unwrap_or_default())?;
executor.set_runtime(runtime);
Ok(executor) Ok(executor)
} }
@@ -1,17 +1,23 @@
use crate::node_registry; use crate::node_registry;
use dyn_any::StaticType; use core_types::arena::Arena;
use core_types::context::{ContextImpl, DynSlot, EvalScope, VarArg, VarArgLink, VarArgSlots};
use core_types::gpoll::GPoll;
use core_types::node::Node;
use core_types::registry::{EdgeHandle, ErasedNode};
use core_types::runtime::{DynGraphRuntime, DynSpawner, GraphRuntime, NoopSpawner};
use graph_craft::Type; use graph_craft::Type;
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graph_craft::document::value::{TaggedValue, UpcastAsRefNode, UpcastNode}; use graph_craft::document::value::TaggedValue;
use graph_craft::graphene_compiler::Executor; use graph_craft::graphene_compiler::Executor;
use graph_craft::proto::{ConstructionArgs, GraphError, LocalFuture, NodeContainer, ProtoNetwork, ProtoNode, SharedNodeContainer, TypeErasedBox, TypingContext}; use graph_craft::proto::{ConstructionArgs, GraphError, ProtoNetwork, ProtoNode, TypingContext};
use graph_craft::proto::{GraphErrorType, GraphErrors}; use graph_craft::proto::{GraphErrorType, GraphErrors};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::error::Error; use std::error::Error;
use std::sync::Arc; use std::sync::{Arc, Mutex, PoisonError};
const ARENA_CAPACITY: usize = 1 << 20;
/// An executor of a node graph that does not require an online compilation server, and instead uses `Box<dyn ...>`. /// An executor of a node graph that does not require an online compilation server, and instead uses `Box<dyn ...>`.
#[derive(Clone)]
pub struct DynamicExecutor { pub struct DynamicExecutor {
output: NodeId, output: NodeId,
/// Stores all of the dynamic node structs. /// Stores all of the dynamic node structs.
@@ -20,6 +26,13 @@ pub struct DynamicExecutor {
typing_context: TypingContext, typing_context: TypingContext,
// This allows us to keep the nodes around for one more frame which is used for introspection // This allows us to keep the nodes around for one more frame which is used for introspection
orphaned_nodes: HashSet<NodeId>, orphaned_nodes: HashSet<NodeId>,
arena: Mutex<Arena>,
runtime: Arc<DynGraphRuntime>,
live_sources: Vec<core_types::SourceId>,
}
fn noop_runtime() -> Arc<DynGraphRuntime> {
Arc::new(GraphRuntime::new(Box::new(NoopSpawner) as Box<DynSpawner>))
} }
impl Default for DynamicExecutor { impl Default for DynamicExecutor {
@@ -29,6 +42,9 @@ impl Default for DynamicExecutor {
tree: Default::default(), tree: Default::default(),
typing_context: TypingContext::new(&node_registry::NODE_REGISTRY), typing_context: TypingContext::new(&node_registry::NODE_REGISTRY),
orphaned_nodes: HashSet::new(), orphaned_nodes: HashSet::new(),
arena: Mutex::new(Arena::new(ARENA_CAPACITY)),
runtime: noop_runtime(),
live_sources: Vec::new(),
} }
} }
} }
@@ -48,23 +64,38 @@ pub struct ResolvedDocumentNodeTypesDelta {
} }
impl DynamicExecutor { impl DynamicExecutor {
pub async fn new(proto_network: ProtoNetwork) -> Result<Self, GraphErrors> { pub fn new(proto_network: ProtoNetwork) -> Result<Self, GraphErrors> {
let mut typing_context = TypingContext::new(&node_registry::NODE_REGISTRY); let mut typing_context = TypingContext::new(&node_registry::NODE_REGISTRY);
typing_context.update(&proto_network)?; typing_context.update(&proto_network)?;
let output = proto_network.output; let output = proto_network.output;
let tree = BorrowTree::new(proto_network, &typing_context).await?; let sources = proto_network.source_ids();
let tree = BorrowTree::new(proto_network, &typing_context)?;
let runtime = noop_runtime();
runtime.retain_sources(&sources);
Ok(Self { Ok(Self {
tree, tree,
output, output,
typing_context, typing_context,
orphaned_nodes: HashSet::new(), orphaned_nodes: HashSet::new(),
arena: Mutex::new(Arena::new(ARENA_CAPACITY)),
runtime,
live_sources: sources,
}) })
} }
pub fn set_runtime(&mut self, runtime: Arc<DynGraphRuntime>) {
runtime.retain_sources(&self.live_sources);
self.runtime = runtime;
}
pub fn take_dirty(&self) -> bool {
self.runtime.take_dirty()
}
/// Updates the existing [`BorrowTree`] to reflect the new [`ProtoNetwork`], reusing nodes where possible. /// Updates the existing [`BorrowTree`] to reflect the new [`ProtoNetwork`], reusing nodes where possible.
#[cfg_attr(debug_assertions, inline(never))] #[cfg_attr(debug_assertions, inline(never))]
pub async fn update(&mut self, proto_network: ProtoNetwork) -> Result<ResolvedDocumentNodeTypesDelta, (ResolvedDocumentNodeTypesDelta, GraphErrors)> { pub fn update(&mut self, proto_network: ProtoNetwork) -> Result<ResolvedDocumentNodeTypesDelta, (ResolvedDocumentNodeTypesDelta, GraphErrors)> {
self.output = proto_network.output; self.output = proto_network.output;
self.typing_context.update(&proto_network).map_err(|e| { self.typing_context.update(&proto_network).map_err(|e| {
// If there is an error then get types that have been resolved before the error // If there is an error then get types that have been resolved before the error
@@ -87,11 +118,10 @@ impl DynamicExecutor {
(ResolvedDocumentNodeTypesDelta { add, remove: Vec::new() }, e) (ResolvedDocumentNodeTypesDelta { add, remove: Vec::new() }, e)
})?; })?;
let (add, orphaned) = self let sources = proto_network.source_ids();
.tree let (add, orphaned) = self.tree.update(proto_network, &self.typing_context).map_err(|e| (ResolvedDocumentNodeTypesDelta::default(), e))?;
.update(proto_network, &self.typing_context) self.runtime.retain_sources(&sources);
.await self.live_sources = sources;
.map_err(|e| (ResolvedDocumentNodeTypesDelta::default(), e))?;
let old_to_remove = core::mem::replace(&mut self.orphaned_nodes, orphaned); let old_to_remove = core::mem::replace(&mut self.orphaned_nodes, orphaned);
let mut remove = Vec::with_capacity(old_to_remove.len() - self.orphaned_nodes.len().min(old_to_remove.len())); let mut remove = Vec::with_capacity(old_to_remove.len() - self.orphaned_nodes.len().min(old_to_remove.len()));
for node_id in old_to_remove { for node_id in old_to_remove {
@@ -135,27 +165,50 @@ impl DynamicExecutor {
} }
} }
impl<I> Executor<I, TaggedValue> for &DynamicExecutor impl<I> Executor<I, GPoll<TaggedValue>> for &DynamicExecutor
where where
I: StaticType + 'static + Send + Sync + std::panic::UnwindSafe, I: VarArg + Send + Sync + std::panic::RefUnwindSafe,
{ {
fn execute(&self, input: I) -> LocalFuture<'_, Result<TaggedValue, Box<dyn Error>>> { fn execute(&self, input: I) -> Result<GPoll<TaggedValue>, Box<dyn Error>> {
Box::pin(async move { let Some(handle) = self.tree.get(self.output) else {
use futures::FutureExt; return Err("Output node not found in executor".into());
};
let result = self.tree.eval_tagged_value(self.output, input); let mut arena = self.arena.lock().unwrap_or_else(PoisonError::into_inner);
let wrapped_result = std::panic::AssertUnwindSafe(result).catch_unwind().await; let result = eval_root(&mut arena, &self.runtime, &input, |ctx| match TaggedValue::from_edge(handle.duplicate(), ctx) {
Ok(poll) => poll.map(Ok),
match wrapped_result { Err(error) => GPoll::Final(Err(error)),
Ok(result) => result.map_err(|e| e.into()), });
Err(e) => { match result {
Box::leak(e); GPoll::Final(value) => Ok(GPoll::Final(value?)),
Err("Node graph execution panicked".into()) GPoll::Partial(value) => Ok(GPoll::Partial(value?)),
} GPoll::Fallback(boxed) => {
let (value, error) = *boxed;
Ok(GPoll::Fallback(Box::new((value?, error))))
} }
}) GPoll::Pending => Ok(GPoll::Pending),
GPoll::Error(error) => Ok(GPoll::Error(error)),
}
} }
} }
pub fn eval_root<S, T>(arena: &mut Arena, runtime: &GraphRuntime<S>, call_argument: DynSlot, eval: impl FnOnce(&ContextImpl) -> GPoll<T>) -> GPoll<T> {
arena.reset();
let generations = runtime.snapshot();
let scope = EvalScope::new(None, None, None, &generations, arena);
let root = ContextImpl::root(&scope);
let link = VarArgLink {
args: VarArgSlots::Single(call_argument),
outer: None,
};
let ctx = root.with_varargs(&link);
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| eval(&ctx))) {
Ok(result) => result,
Err(_) => {
arena.reset();
GPoll::panicked()
}
}
}
pub struct InputMapping {} pub struct InputMapping {}
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -188,39 +241,39 @@ impl std::fmt::Display for IntrospectError {
/// ///
/// # Fields /// # Fields
/// ///
/// * `nodes`: A [`HashMap`] of [`NodeId`]s to tuples of [`SharedNodeContainer`] and [`Path`]. /// * `nodes`: A [`HashMap`] of [`NodeId`]s to tuples of [`EdgeHandle`] and [`Path`].
/// This stores the actual node instances and their associated paths. /// This stores the actual node instances and their associated paths.
/// ///
/// * `source_map`: A [`HashMap`] from [`Path`] to tuples of [`NodeId`] and [`NodeTypes`]. /// * `source_map`: A [`HashMap`] from [`Path`] to tuples of [`NodeId`] and [`NodeTypes`].
/// This maps document paths to node IDs and their associated type information. /// This maps document paths to node IDs and their associated type information.
/// ///
/// A store of the dynamically typed nodes and also the source map. /// A store of the dynamically typed nodes and also the source map.
#[derive(Default, Clone)] #[derive(Default)]
pub struct BorrowTree { pub struct BorrowTree {
/// A hashmap of node IDs and dynamically typed nodes. /// A hashmap of node IDs and dynamically typed nodes.
nodes: HashMap<NodeId, (SharedNodeContainer, Path)>, nodes: HashMap<NodeId, (EdgeHandle, Path)>,
/// A hashmap from the document path to the proto node ID. /// A hashmap from the document path to the proto node ID.
source_map: HashMap<Path, (NodeId, NodeTypes)>, source_map: HashMap<Path, (NodeId, NodeTypes)>,
} }
impl BorrowTree { impl BorrowTree {
pub async fn new(proto_network: ProtoNetwork, typing_context: &TypingContext) -> Result<BorrowTree, GraphErrors> { pub fn new(proto_network: ProtoNetwork, typing_context: &TypingContext) -> Result<BorrowTree, GraphErrors> {
let mut nodes = BorrowTree::default(); let mut nodes = BorrowTree::default();
for (id, node) in proto_network.nodes { for (id, node) in proto_network.nodes {
nodes.push_node(id, node, typing_context).await? nodes.push_node(id, node, typing_context)?
} }
Ok(nodes) Ok(nodes)
} }
/// Pushes new nodes into the tree and return orphaned nodes /// Pushes new nodes into the tree and return orphaned nodes
pub async fn update(&mut self, proto_network: ProtoNetwork, typing_context: &TypingContext) -> Result<(Vec<Path>, HashSet<NodeId>), GraphErrors> { pub fn update(&mut self, proto_network: ProtoNetwork, typing_context: &TypingContext) -> Result<(Vec<Path>, HashSet<NodeId>), GraphErrors> {
let mut old_nodes: HashSet<_> = self.nodes.keys().copied().collect(); let mut old_nodes: HashSet<_> = self.nodes.keys().copied().collect();
let mut new_nodes: Vec<_> = Vec::new(); let mut new_nodes: Vec<_> = Vec::new();
// TODO: Problem: When a passthrough node is connected directly to an export the first input to the passthrough node is not added to the proto network, while the second input is. This means the primary input does not have a type. // TODO: Problem: When a passthrough node is connected directly to an export the first input to the passthrough node is not added to the proto network, while the second input is. This means the primary input does not have a type.
for (id, node) in proto_network.nodes { for (id, node) in proto_network.nodes {
if !self.nodes.contains_key(&id) { if !self.nodes.contains_key(&id) {
new_nodes.push(node.original_location.path.clone().unwrap_or_default().into()); new_nodes.push(node.original_location.path.clone().unwrap_or_default().into());
self.push_node(id, node, typing_context).await?; self.push_node(id, node, typing_context)?;
} else if self.update_source_map(id, typing_context, &node) { } else if self.update_source_map(id, typing_context, &node) {
new_nodes.push(node.original_location.path.clone().unwrap_or_default().into()); new_nodes.push(node.original_location.path.clone().unwrap_or_default().into());
} }
@@ -229,44 +282,33 @@ impl BorrowTree {
Ok((new_nodes, old_nodes)) Ok((new_nodes, old_nodes))
} }
fn node_deps(&self, nodes: &[NodeId]) -> Vec<SharedNodeContainer> { fn node_deps(&self, nodes: &[NodeId]) -> Vec<EdgeHandle> {
nodes.iter().map(|node| self.nodes.get(node).unwrap().0.clone()).collect() nodes.iter().map(|node| self.nodes.get(node).unwrap().0.duplicate()).collect()
} }
fn store_node(&mut self, node: SharedNodeContainer, id: NodeId, path: Path) { fn store_node(&mut self, node: EdgeHandle, id: NodeId, path: Path) {
self.nodes.insert(id, (node, path)); self.nodes.insert(id, (node, path));
} }
/// 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. /// Calls the `Node::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<Arc<dyn std::any::Any + Send + Sync + 'static>, IntrospectError> { pub fn introspect(&self, node_path: &[NodeId]) -> Result<Arc<dyn std::any::Any + Send + Sync + 'static>, IntrospectError> {
let (id, _) = self.source_map.get(node_path).ok_or_else(|| IntrospectError::PathNotFound(node_path.to_vec()))?; 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))?; let (node, _path) = self.nodes.get(id).ok_or(IntrospectError::ProtoNodeNotFound(*id))?;
node.serialize().ok_or(IntrospectError::NoData) node.serialize().ok_or(IntrospectError::NoData)
} }
pub fn get(&self, id: NodeId) -> Option<SharedNodeContainer> { pub fn get(&self, id: NodeId) -> Option<EdgeHandle> {
self.nodes.get(&id).map(|(node, _)| node.clone()) self.nodes.get(&id).map(|(node, _)| node.duplicate())
} }
/// Evaluate the output node of the [`BorrowTree`]. /// Evaluate a node of the [`BorrowTree`], downcasting its edge to the expected output type.
pub async fn eval<'i, I, O>(&'i self, id: NodeId, input: I) -> Option<O> pub fn eval<I, T: 'static>(&self, id: NodeId, input: &I) -> Option<GPoll<T>>
where where
I: StaticType + 'i + Send + Sync, ErasedNode<T>: Node<I, Output = T>,
O: StaticType + 'i,
{ {
let (node, _path) = self.nodes.get(&id).cloned()?; let (node, _path) = self.nodes.get(&id)?;
let output = node.eval(Box::new(input)); let edge = node.duplicate().downcast::<T>().ok()?;
dyn_any::downcast::<O>(output.await).ok().map(|o| *o) Some(edge.eval(input))
}
/// Evaluate the output node of the [`BorrowTree`] and cast it to a tagged value.
/// This ensures that no borrowed data can escape the node graph.
pub async fn eval_tagged_value<I>(&self, id: NodeId, input: I) -> Result<TaggedValue, String>
where
I: StaticType + 'static + Send + Sync + std::panic::UnwindSafe,
{
let (node, _path) = self.nodes.get(&id).cloned().ok_or("Output node not found in executor")?;
let output = node.eval(Box::new(input));
TaggedValue::try_from_any(output.await)
} }
/// Removes a node from the [`BorrowTree`] and returns its associated path. /// Removes a node from the [`BorrowTree`] and returns its associated path.
@@ -293,10 +335,10 @@ impl BorrowTree {
/// use interpreted_executor::node_registry; /// use interpreted_executor::node_registry;
/// ///
/// ///
/// async fn example() -> Result<(), GraphErrors> { /// fn example() -> Result<(), GraphErrors> {
/// let (proto_network, node_id, proto_node) = ProtoNetwork::example(); /// let (proto_network, node_id, proto_node) = ProtoNetwork::example();
/// let typing_context = TypingContext::new(&node_registry::NODE_REGISTRY); /// let typing_context = TypingContext::default();
/// let mut borrow_tree = BorrowTree::new(proto_network, &typing_context).await?; /// let mut borrow_tree = BorrowTree::new(proto_network, &typing_context)?;
/// ///
/// // Assert that the node exists in the BorrowTree /// // Assert that the node exists in the BorrowTree
/// assert!(borrow_tree.get(node_id).is_some(), "Node should exist before removal"); /// assert!(borrow_tree.get(node_id).is_some(), "Node should exist before removal");
@@ -393,30 +435,23 @@ impl BorrowTree {
/// - `Nodes`: Constructs a node using other nodes as dependencies. /// - `Nodes`: Constructs a node using other nodes as dependencies.
/// - Uses the constructor function from the `typing_context` for `Nodes` construction arguments. /// - Uses the constructor function from the `typing_context` for `Nodes` construction arguments.
/// - Returns an error if no constructor is found for the given node ID. /// - Returns an error if no constructor is found for the given node ID.
async fn push_node(&mut self, id: NodeId, proto_node: ProtoNode, typing_context: &TypingContext) -> Result<(), GraphErrors> { fn push_node(&mut self, id: NodeId, proto_node: ProtoNode, typing_context: &TypingContext) -> Result<(), GraphErrors> {
self.update_source_map(id, typing_context, &proto_node); self.update_source_map(id, typing_context, &proto_node);
let path = proto_node.original_location.path.clone().unwrap_or_default(); let path = proto_node.original_location.path.clone().unwrap_or_default();
match &proto_node.construction_args { match &proto_node.construction_args {
ConstructionArgs::Value(value) => { ConstructionArgs::Value(value) => {
let node = if let TaggedValue::EditorApi(api) = &**value { let node = (**value)
let editor_api = UpcastAsRefNode::new(api.clone()); .clone()
let node = Box::new(editor_api) as TypeErasedBox<'_>; .to_edge()
NodeContainer::new(node) .map_err(|error| vec![GraphError::new(&proto_node, GraphErrorType::ConstructionFailed(error))])?;
} else {
let upcasted = UpcastNode::new(value.to_owned());
let node = Box::new(upcasted) as TypeErasedBox<'_>;
NodeContainer::new(node)
};
self.store_node(node, id, path.into()); self.store_node(node, id, path.into());
} }
ConstructionArgs::Inline(_) => unimplemented!("Inline nodes are not supported yet"), ConstructionArgs::Inline(_) => unimplemented!("Inline nodes are not supported yet"),
ConstructionArgs::Nodes(ids) => { ConstructionArgs::Nodes(ids) => {
let ids = ids.to_vec(); let construction_nodes = self.node_deps(ids);
let construction_nodes = self.node_deps(&ids);
let constructor = typing_context.constructor(id).ok_or_else(|| vec![GraphError::new(&proto_node, GraphErrorType::NoConstructor)])?; let constructor = typing_context.constructor(id).ok_or_else(|| vec![GraphError::new(&proto_node, GraphErrorType::NoConstructor)])?;
let node = constructor(construction_nodes).await; let node = constructor(construction_nodes).map_err(|error| vec![GraphError::new(&proto_node, GraphErrorType::ConstructionFailed(format!("{error:?}")))])?;
let node = NodeContainer::new(node);
self.store_node(node, id, path.into()); self.store_node(node, id, path.into());
} }
}; };
@@ -432,17 +467,74 @@ impl BorrowTree {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use core_types::arena::ArenaCell;
use core_types::context::{ExtractFootprint, ExtractVarArgs};
use core_types::runtime::{SourceFuture, Spawner};
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
struct InertSpawner;
impl Spawner for InertSpawner {
fn spawn(&self, _task: SourceFuture) {}
}
#[test]
fn eval_root_builds_the_bare_root_with_the_call_argument_as_vararg_0() {
let mut arena = Arena::new(64);
let runtime = GraphRuntime::new(InertSpawner);
let argument = 21.5f64;
let result = eval_root(&mut arena, &runtime, &argument, |ctx| {
assert!(ctx.try_footprint().is_none(), "the bare root carries no axes");
GPoll::Final(ctx.vararg(0).ok().and_then(|slot| slot.downcast_ref::<f64>()).copied().unwrap_or(0.))
});
assert_eq!(result, GPoll::Final(21.5));
}
#[test]
fn eval_root_resets_the_arena_at_eval_start() {
let mut arena = Arena::new(64);
let runtime = GraphRuntime::new(InertSpawner);
let cell = ArenaCell::new();
eval_root(&mut arena, &runtime, &(), |ctx| {
let (_, weak) = ctx.scope().arena().alloc(5u32).unwrap();
cell.store(weak);
GPoll::Final(())
});
assert!(cell.load(&arena).is_some(), "the introspection window spans until the next eval");
eval_root(&mut arena, &runtime, &(), |ctx| {
assert!(cell.load(ctx.scope().arena()).is_none(), "the reset at eval start reclaims the previous frame");
GPoll::Final(())
});
}
#[test]
fn a_panicking_eval_reports_the_error_and_resets_the_arena() {
let mut arena = Arena::new(64);
let runtime = GraphRuntime::new(InertSpawner);
let cell = ArenaCell::new();
let result: GPoll<()> = eval_root(&mut arena, &runtime, &(), |ctx| {
let (_, weak) = ctx.scope().arena().alloc(5u32).unwrap();
cell.store(weak);
panic!("mid-eval");
});
assert_eq!(result, GPoll::panicked());
assert!(cell.load(&arena).is_none(), "reset-on-panic leaves no stale records");
assert_eq!(eval_root(&mut arena, &runtime, &(), |_| GPoll::Final(7u32)), GPoll::Final(7));
}
#[test] #[test]
fn push_node_sync() { fn push_node_sync() {
let mut tree = BorrowTree::default(); let mut tree = BorrowTree::default();
let val_1_protonode = ProtoNode::value(ConstructionArgs::Value(TaggedValue::U32(2u32).into()), vec![]); let val_1_protonode = ProtoNode::value(ConstructionArgs::Value(TaggedValue::U32(2u32).into()), vec![]);
let context = TypingContext::default(); let context = TypingContext::default();
let future = tree.push_node(NodeId(0), val_1_protonode, &context); tree.push_node(NodeId(0), val_1_protonode, &context).unwrap();
futures::executor::block_on(future).unwrap();
let _node = tree.get(NodeId(0)).unwrap(); let _node = tree.get(NodeId(0)).unwrap();
let result = futures::executor::block_on(tree.eval(NodeId(0), ()));
assert_eq!(result, Some(2u32)); let arena = Arena::new(64);
let generations = [];
let scope = EvalScope::new(None, None, None, &generations, &arena);
let ctx = ContextImpl::root(&scope);
let result: Option<GPoll<u32>> = tree.eval(NodeId(0), &ctx);
assert_eq!(result, Some(GPoll::Final(2)));
} }
} }
+1 -2
View File
@@ -5,7 +5,6 @@ pub mod util;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use core_types::*; use core_types::*;
use futures::executor::block_on;
use graphene_core::ops::passthrough; use graphene_core::ops::passthrough;
#[test] #[test]
@@ -47,6 +46,6 @@ mod tests {
let compiler = Compiler {}; let compiler = Compiler {};
let protograph = compiler.compile_single(network).expect("Graph should be generated"); let protograph = compiler.compile_single(network).expect("Graph should be generated");
let _exec = block_on(DynamicExecutor::new(protograph)).map(|_e| panic!("The network should not type check ")).unwrap_err(); let _exec = DynamicExecutor::new(protograph).map(|_e| panic!("The network should not type check ")).unwrap_err();
} }
} }
@@ -1,10 +1,7 @@
use dyn_any::StaticType;
use glam::{DAffine2, DVec2, IVec2}; use glam::{DAffine2, DVec2, IVec2};
use graph_craft::application_io::PlatformEditorApi; use graph_craft::application_io::PlatformEditorApi;
use graph_craft::document::DocumentNode; use graph_craft::document::DocumentNode;
use graph_craft::document::value::RenderOutput; use graph_craft::document::value::RenderOutput;
use graph_craft::proto::{NodeConstructor, TypeErasedBox};
use graphene_std::any::DynAnyNode;
use graphene_std::application_io::Texture; use graphene_std::application_io::Texture;
use graphene_std::brush::brush_stroke::BrushStroke; use graphene_std::brush::brush_stroke::BrushStroke;
use graphene_std::gradient::GradientStops; use graphene_std::gradient::GradientStops;
@@ -16,19 +13,20 @@ use graphene_std::raster::GPU;
use graphene_std::raster::color::Color; use graphene_std::raster::color::Color;
use graphene_std::raster::*; use graphene_std::raster::*;
use graphene_std::raster::{CPU, Raster}; use graphene_std::raster::{CPU, Raster};
use graphene_std::registry::{ConstructionError, EdgeHandle, ErasedNode, NodeIOTypes, RegistryEntry};
use graphene_std::render_node::RenderIntermediate; use graphene_std::render_node::RenderIntermediate;
use graphene_std::runtime::RuntimeHandle;
use graphene_std::transform::Footprint; use graphene_std::transform::Footprint;
use graphene_std::uuid::NodeId; use graphene_std::uuid::NodeId;
use graphene_std::vector::Vector; use graphene_std::vector::Vector;
use graphene_std::{Artboard, Context, Graphic, NodeIO, NodeIOTypes, ProtoNodeIdentifier, concrete, fn_type_fut, future}; use graphene_std::{Artboard, Context, Graphic, ProtoNodeIdentifier, SourceId, concrete, fn_type};
use node_registry_macros::{async_node, convert_node, into_node}; use node_registry_macros::{async_node, convert_node, into_node};
use std::collections::HashMap; use std::collections::HashMap;
#[cfg(feature = "gpu")] #[cfg(feature = "gpu")]
use wgpu_executor::WgpuExecutor; use wgpu_executor::WgpuExecutorHandle;
// TODO: turn into hashmap fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> { let mut node_types: Vec<(ProtoNodeIdentifier, RegistryEntry)> = vec![
let mut node_types: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![
// ========== // ==========
// INTO NODES // INTO NODES
// ========== // ==========
@@ -92,8 +90,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
convert_node!(from: List<Raster<GPU>>, to: AttributeValueDyn), convert_node!(from: List<Raster<GPU>>, to: AttributeValueDyn),
convert_node!(from: List<Graphic>, to: AttributeValueDyn), convert_node!(from: List<Graphic>, to: AttributeValueDyn),
// into_node!(from: List<Raster<CPU>>, to: List<Raster<SRGBA8>>), // into_node!(from: List<Raster<CPU>>, to: List<Raster<SRGBA8>>),
#[cfg(feature = "gpu")]
into_node!(from: &PlatformEditorApi, to: &WgpuExecutor),
convert_node!(from: DVec2, to: DVec2), convert_node!(from: DVec2, to: DVec2),
convert_node!(from: List<Vector>, to: List<Vector>), convert_node!(from: List<Vector>, to: List<Vector>),
convert_node!(from: DVec2, to: List<Vector>), convert_node!(from: DVec2, to: List<Vector>),
@@ -103,107 +99,107 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
convert_node!(from: IVec2, to: String), convert_node!(from: IVec2, to: String),
convert_node!(from: DAffine2, to: String), convert_node!(from: DAffine2, to: String),
#[cfg(feature = "gpu")] #[cfg(feature = "gpu")]
convert_node!(from: List<Raster<CPU>>, to: List<Raster<CPU>>, converter: &WgpuExecutor), convert_node!(from: List<Raster<CPU>>, to: List<Raster<CPU>>, converter: WgpuExecutorHandle),
#[cfg(feature = "gpu")] #[cfg(feature = "gpu")]
convert_node!(from: List<Raster<CPU>>, to: List<Raster<GPU>>, converter: &WgpuExecutor), convert_node!(from: List<Raster<CPU>>, to: List<Raster<GPU>>, converter: WgpuExecutorHandle),
#[cfg(feature = "gpu")] #[cfg(feature = "gpu")]
convert_node!(from: List<Raster<GPU>>, to: List<Raster<GPU>>, converter: &WgpuExecutor), convert_node!(from: List<Raster<GPU>>, to: List<Raster<GPU>>, converter: WgpuExecutorHandle),
#[cfg(feature = "gpu")] #[cfg(feature = "gpu")]
convert_node!(from: List<Raster<GPU>>, to: List<Raster<CPU>>, converter: &WgpuExecutor), convert_node!(from: List<Raster<GPU>>, to: List<Raster<CPU>>, converter: WgpuExecutorHandle, async),
// ============= // =============
// MONITOR NODES // MONITOR NODES
// ============= // =============
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ()]), 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<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<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<Vector>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => List<Raster<CPU>>]), async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => List<Raster<CPU>>]),
#[cfg(feature = "gpu")] #[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<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<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 => 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 => 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 => String]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => IVec2]), 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 => DVec2]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => DAffine2]), 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 => 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 => bool]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => f64]), 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 => u32]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => u64]), 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 => BlendMode]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Texture]), 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::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::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::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::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::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::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::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 => 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 => 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::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 => 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 => 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<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<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<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<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<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<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<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::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 => 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 => AttributeDyn]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => AttributeValueDyn]), 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 => ListDyn]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Graphic]), 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 => 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 => 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 => 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::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::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::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::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::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::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::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::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::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::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::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::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::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::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::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::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::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::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::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::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::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::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::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::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::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::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::transform::ScaleType]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::InterpolationDistribution]), async_node!(graphene_core::memo::MonitorNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::InterpolationDistribution]),
// Context nullification // Context nullification
#[cfg(feature = "gpu")] #[cfg(feature = "gpu")]
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => &PlatformEditorApi, Context => graphene_std::ContextFeatures]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => std::sync::Arc<PlatformEditorApi>, Context => graphene_std::ContextModification]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RenderIntermediate, Context => graphene_std::ContextFeatures]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RenderIntermediate, Context => graphene_std::ContextModification]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RenderOutput, Context => graphene_std::ContextFeatures]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RenderOutput, Context => graphene_std::ContextModification]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => AttributeDyn, Context => graphene_std::ContextFeatures]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => AttributeDyn, Context => graphene_std::ContextModification]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => AttributeValueDyn, Context => graphene_std::ContextFeatures]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => AttributeValueDyn, Context => graphene_std::ContextModification]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => ListDyn, Context => graphene_std::ContextFeatures]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => ListDyn, Context => graphene_std::ContextModification]),
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => CanvasHandle, Context => graphene_std::ContextFeatures]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => CanvasHandle, Context => graphene_std::ContextModification]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => &PlatformEditorApi, Context => graphene_std::ContextFeatures]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => std::sync::Arc<PlatformEditorApi>, Context => graphene_std::ContextModification]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => &wgpu_executor::WgpuExecutor, Context => graphene_std::ContextFeatures]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuExecutorHandle, Context => graphene_std::ContextModification]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Option<&wgpu_executor::WgpuExecutor>, Context => graphene_std::ContextFeatures]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Option<wgpu_executor::WgpuExecutorHandle>, Context => graphene_std::ContextModification]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuPipelineCache, Context => graphene_std::ContextFeatures]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuPipelineCache, Context => graphene_std::ContextModification]),
// ========== // ==========
// MEMO NODES // MEMO NODES
// ========== // ==========
@@ -239,7 +235,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => DAffine2]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => DAffine2]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Footprint]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Footprint]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => RenderOutput]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => RenderOutput]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => &PlatformEditorApi]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => std::sync::Arc<PlatformEditorApi>]),
#[cfg(feature = "gpu")] #[cfg(feature = "gpu")]
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Raster<GPU>>]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Raster<GPU>>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option<f64>]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option<f64>]),
@@ -251,7 +247,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::text::Font]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::text::Font]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<BrushStroke>]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<BrushStroke>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => DocumentNode]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => DocumentNode]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::ContextFeatures]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::ContextModification]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::Footprint]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::Footprint]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Box<graphene_std::vector::VectorModification>]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Box<graphene_std::vector::VectorModification>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::blending::BlendMode]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::blending::BlendMode]),
@@ -287,8 +283,8 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::ScaleType]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::ScaleType]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::InterpolationDistribution]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::InterpolationDistribution]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => RenderIntermediate]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => RenderIntermediate]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => &wgpu_executor::WgpuExecutor]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuExecutorHandle]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option<&wgpu_executor::WgpuExecutor>]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option<wgpu_executor::WgpuExecutorHandle>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuPipelineCache]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuPipelineCache]),
]; ];
// ============= // =============
@@ -317,15 +313,21 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
.flatten(), .flatten(),
); );
let mut map: HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> = HashMap::new(); let mut map: HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> = HashMap::new();
let insert = |map: &mut HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>>, id: ProtoNodeIdentifier, entry: RegistryEntry| {
let rows = map.entry(id).or_default();
if !rows.iter().any(|row| row.io == entry.io) {
rows.push(entry);
}
};
for (id, entry) in graphene_std::registry::NODE_REGISTRY.lock().unwrap().iter() { for (id, entries) in graphene_std::registry::NODE_REGISTRY.lock().unwrap().iter() {
for (constructor, types) in entry.iter() { for entry in entries {
map.entry(id.clone()).or_default().insert(types.clone(), *constructor); insert(&mut map, id.clone(), entry.clone());
} }
} }
for (id, node_constructor, types) in node_types.into_iter() { for (id, entry) in node_types.into_iter() {
// TODO: this is a hack to remove the newline from the node new_name // TODO: this is a hack to remove the newline from the node new_name
// This occurs for the ChannelMixerNode presumably because of the long name. // This occurs for the ChannelMixerNode presumably because of the long name.
// This might be caused by the stringify! macro // This might be caused by the stringify! macro
@@ -338,43 +340,35 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
new_name = path.to_string(); new_name = path.to_string();
} }
map.entry(ProtoNodeIdentifier::with_owned_string(new_name)).or_default().insert(types.clone(), node_constructor); insert(&mut map, ProtoNodeIdentifier::with_owned_string(new_name), entry);
} }
map map
} }
// TODO: Replace with `core::cell::LazyCell` (<https://doc.rust-lang.org/core/cell/struct.LazyCell.html>) or similar // TODO: Replace with `core::cell::LazyCell` (<https://doc.rust-lang.org/core/cell/struct.LazyCell.html>) or similar
pub static NODE_REGISTRY: once_cell::sync::Lazy<HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>>> = once_cell::sync::Lazy::new(|| node_registry()); pub static NODE_REGISTRY: once_cell::sync::Lazy<HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>>> = once_cell::sync::Lazy::new(node_registry);
mod node_registry_macros { mod node_registry_macros {
macro_rules! async_node { macro_rules! async_node {
// TODO: we currently need to annotate the type here because the compiler would otherwise (correctly)
// TODO: assign a Pin<Box<dyn Future<Output=T>>> type to the node, which is not what we want for now.
//
// This `params` variant of the macro wraps the normal `fn_params` variant and is used as a shorthand for writing `T` instead of `() => T` // 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),*]) => { ($path:ty, input: $input:ty, params: [$($type:ty),*]) => {
async_node!($path, input: $input, fn_params: [ $(() => $type),*]) async_node!($path, input: $input, fn_params: [ $(() => $type),*])
}; };
($path:ty, input: $input:ty, fn_params: [$($arg:ty => $type:ty),*]) => { ($path:ty, input: $input:ty, fn_params: [$first_arg:ty => $first:ty $(, $arg:ty => $type:ty)*]) => {
( (
ProtoNodeIdentifier::new(stringify!($path)), ProtoNodeIdentifier::new(stringify!($path)),
|mut args| { RegistryEntry {
Box::pin(async move { io: NodeIOTypes::new(concrete!($input), concrete!($first), vec![fn_type!($first_arg, $first) $(, fn_type!($arg, $type))*]),
args.reverse(); constructor: |inputs| {
let node = <$path>::new($(graphene_std::any::downcast_node::<$arg, $type>(args.pop().expect("Not enough arguments provided to construct node"))),*); let expected = [stringify!($first) $(, stringify!($type))*].len();
let any: DynAnyNode<$input, _, _> = graphene_std::any::DynAnyNode::new(node); if inputs.len() != expected {
Box::new(any) as TypeErasedBox 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>()?)*);
let node = <$path>::new($( Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedNode<$first>>))
graphene_std::any::PanicNode::<$arg, core::pin::Pin<Box<dyn core::future::Future<Output = $type> + Send>>>::new() },
),*);
let params = vec![$(fn_type_fut!($arg, $type)),*];
let mut node_io = NodeIO::<'_, $input>::to_async_node_io(&node, params);
node_io.call_argument = concrete!(<$input as StaticType>::Static);
node_io
}, },
) )
}; };
@@ -384,31 +378,23 @@ mod node_registry_macros {
(from: $from:ty, to: $to:ty) => { (from: $from:ty, to: $to:ty) => {
( (
ProtoNodeIdentifier::new(concat!["graphene_core::ops::IntoNode<", stringify!($to), ">"]), ProtoNodeIdentifier::new(concat!["graphene_core::ops::IntoNode<", stringify!($to), ">"]),
|mut args| { RegistryEntry {
Box::pin(async move { io: NodeIOTypes::new(concrete!(Context), concrete!($to), vec![fn_type!(Context, $from)]),
let node = graphene_std::ops::IntoNode::new( constructor: |inputs| {
graphene_std::any::downcast_node::<Context, $from>(args.pop().unwrap()), if inputs.len() != 1 {
graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>)), return Err(ConstructionError::Arity { expected: 1, got: inputs.len() });
); }
let any: DynAnyNode<Context, $to, _> = graphene_std::any::DynAnyNode::new(node); let mut inputs = inputs.into_iter();
Box::new(any) as TypeErasedBox let node = graphene_std::ops::IntoNode::<$to, _>::new(inputs.next().unwrap().downcast::<$from>()?);
}) Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedNode<$to>>))
}, },
{
let node = graphene_std::ops::IntoNode::new(
graphene_std::any::PanicNode::<Context, core::pin::Pin<Box<dyn core::future::Future<Output = $from> + Send>>>::new(),
graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>)),
);
let params = vec![fn_type_fut!(Context, $from)];
let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params);
node_io
}, },
) )
}; };
} }
macro_rules! convert_node { macro_rules! convert_node {
(from: $from:ty, to: numbers) => {{ (from: $from:ty, to: numbers) => {{
let x: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![ let x: Vec<(ProtoNodeIdentifier, RegistryEntry)> = vec![
convert_node!(from: $from, to: f32), convert_node!(from: $from, to: f32),
convert_node!(from: $from, to: f64), convert_node!(from: $from, to: f64),
convert_node!(from: $from, to: i8), convert_node!(from: $from, to: i8),
@@ -427,7 +413,7 @@ mod node_registry_macros {
x x
}}; }};
(from: numbers, to: $to:ty) => {{ (from: numbers, to: $to:ty) => {{
let x: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![ let x: Vec<(ProtoNodeIdentifier, RegistryEntry)> = vec![
convert_node!(from: f32, to: $to), convert_node!(from: f32, to: $to),
convert_node!(from: f64, to: $to), convert_node!(from: f64, to: $to),
convert_node!(from: i8, to: $to), convert_node!(from: i8, to: $to),
@@ -448,31 +434,44 @@ mod node_registry_macros {
(from: $from:ty, to: $to:ty) => { (from: $from:ty, to: $to:ty) => {
convert_node!(from: $from, to: $to, converter: ()) convert_node!(from: $from, to: $to, converter: ())
}; };
(from: $from:ty, to: $to:ty, converter: $convert:ty, async) => {
(
ProtoNodeIdentifier::new(concat!["graphene_core::ops::ConvertNode<", stringify!($to), ">"]),
RegistryEntry {
io: NodeIOTypes::new(
concrete!(Context),
concrete!($to),
vec![fn_type!(Context, $from), fn_type!(Context, $convert), fn_type!(Context, RuntimeHandle), fn_type!(Context, SourceId)],
),
constructor: |inputs| {
if inputs.len() != 4 {
return Err(ConstructionError::Arity { expected: 4, got: inputs.len() });
}
let mut inputs = inputs.into_iter();
let node = graphene_std::ops::ConvertAsyncNode::<$to, _, _, _, _>::new(
inputs.next().unwrap().downcast::<$from>()?,
inputs.next().unwrap().downcast::<$convert>()?,
inputs.next().unwrap().downcast::<RuntimeHandle>()?,
inputs.next().unwrap().downcast::<SourceId>()?,
);
Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedNode<$to>>))
},
},
)
};
(from: $from:ty, to: $to:ty, converter: $convert:ty) => { (from: $from:ty, to: $to:ty, converter: $convert:ty) => {
( (
ProtoNodeIdentifier::new(concat!["graphene_core::ops::ConvertNode<", stringify!($to), ">"]), ProtoNodeIdentifier::new(concat!["graphene_core::ops::ConvertNode<", stringify!($to), ">"]),
|mut args| { RegistryEntry {
Box::pin(async move { io: NodeIOTypes::new(concrete!(Context), concrete!($to), vec![fn_type!(Context, $from), fn_type!(Context, $convert)]),
let mut args = args.drain(..); constructor: |inputs| {
let node = graphene_std::ops::ConvertNode::new( if inputs.len() != 2 {
graphene_std::any::downcast_node::<Context, $from>(args.next().expect("Convert node did not get first argument")), return Err(ConstructionError::Arity { expected: 2, got: inputs.len() });
graphene_std::any::downcast_node::<Context, $convert>(args.next().expect("Convert node did not get converter argument")), }
graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>)) let mut inputs = inputs.into_iter();
); let node = graphene_std::ops::ConvertNode::<$to, _, _>::new(inputs.next().unwrap().downcast::<$from>()?, inputs.next().unwrap().downcast::<$convert>()?);
let any: DynAnyNode<Context, $to, _> = graphene_std::any::DynAnyNode::new(node); Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedNode<$to>>))
Box::new(any) as TypeErasedBox },
})
},
{
let node = graphene_std::ops::ConvertNode::new(
graphene_std::any::PanicNode::<Context, core::pin::Pin<Box<dyn core::future::Future<Output = $from> + Send>>>::new(),
graphene_std::any::PanicNode::<Context, core::pin::Pin<Box<dyn core::future::Future<Output = $convert> + Send>>>::new(),
graphene_std::any::FutureWrapperNode::new(graphene_std::value::ClonedNode::new(std::marker::PhantomData::<$to>))
);
let params = vec![fn_type_fut!(Context, $from), fn_type_fut!(Context, $convert)];
let node_io = NodeIO::<'_, Context>::to_async_node_io(&node, params);
node_io
}, },
) )
}; };
+3 -3
View File
@@ -69,12 +69,12 @@ pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc<PlatformEdito
..Default::default() ..Default::default()
}, },
DocumentNode { DocumentNode {
call_argument: concrete!(graphene_std::application_io::RenderConfig), call_argument: concrete!(Context),
inputs: vec![NodeInput::node(NodeId(4), 0)], inputs: vec![NodeInput::node(NodeId(4), 0)],
implementation: DocumentNodeImplementation::ProtoNode(graphene_std::render_node::create_context::IDENTIFIER), implementation: DocumentNodeImplementation::ProtoNode(graphene_std::render_node::create_context::IDENTIFIER),
// We add the extract index annotation here to force the compiler to add a context nullification node before this node so the render context is properly nullified so the render cache node can do its's work // We add the extract index annotation here to force the compiler to add a context nullification node before this node so the render context is properly nullified so the render cache node can do its's work
context_features: graphene_std::ContextDependencies::new( context_features: graphene_std::ContextDependencies::new(
ContextFeatures::INDEX, ContextFeatures::INDEX | ContextFeatures::VARARGS,
ContextFeatures::REAL_TIME | ContextFeatures::ANIMATION_TIME | ContextFeatures::POINTER_POSITION | ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS, ContextFeatures::REAL_TIME | ContextFeatures::ANIMATION_TIME | ContextFeatures::POINTER_POSITION | ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS,
), ),
..Default::default() ..Default::default()
@@ -99,7 +99,7 @@ pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc<PlatformEdito
..Default::default() ..Default::default()
}, },
]; ];
let scope_injections = vec![("editor-api".to_string(), (NodeId(2), concrete!(&PlatformEditorApi)))]; let scope_injections = vec![("editor-api".to_string(), (NodeId(2), concrete!(std::sync::Arc<PlatformEditorApi>)))];
NodeNetwork { NodeNetwork {
exports: vec![NodeInput::node(NodeId(1), 0)], exports: vec![NodeInput::node(NodeId(1), 0)],
@@ -16,6 +16,7 @@ wgpu = ["dep:raster-types", "raster-types/wgpu"]
# Local dependencies # Local dependencies
dyn-any = { workspace = true } dyn-any = { workspace = true }
core-types = { workspace = true } core-types = { workspace = true }
graphene-hash = { workspace = true }
vector-types = { workspace = true } vector-types = { workspace = true }
text-nodes = { workspace = true } text-nodes = { workspace = true }
graphene-resource = { workspace = true } graphene-resource = { workspace = true }
+12 -3
View File
@@ -21,6 +21,9 @@ pub trait ApplicationIo {
fn gpu_executor(&self) -> Option<&Self::Executor> { fn gpu_executor(&self) -> Option<&Self::Executor> {
None None
} }
fn gpu_executor_arc(&self) -> Option<Arc<Self::Executor>> {
None
}
fn load_resource(&self, hash: resource::ResourceHash) -> resource::ResourceFuture<'_>; fn load_resource(&self, hash: resource::ResourceHash) -> resource::ResourceFuture<'_>;
} }
@@ -31,6 +34,10 @@ impl<T: ApplicationIo> ApplicationIo for &T {
(**self).gpu_executor() (**self).gpu_executor()
} }
fn gpu_executor_arc(&self) -> Option<Arc<T::Executor>> {
(**self).gpu_executor_arc()
}
fn load_resource(&self, hash: resource::ResourceHash) -> resource::ResourceFuture<'_> { fn load_resource(&self, hash: resource::ResourceHash) -> resource::ResourceFuture<'_> {
(**self).load_resource(hash) (**self).load_resource(hash)
} }
@@ -54,7 +61,7 @@ pub trait GetEditorPreferences {
fn max_render_region_area(&self) -> u32; fn max_render_region_area(&self) -> u32;
} }
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, graphene_hash::CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ExportFormat { pub enum ExportFormat {
#[default] #[default]
@@ -62,14 +69,14 @@ pub enum ExportFormat {
Raster, Raster,
} }
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny)] #[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, graphene_hash::CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TimingInformation { pub struct TimingInformation {
pub time: f64, pub time: f64,
pub animation_time: Duration, pub animation_time: Duration,
} }
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny)] #[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, graphene_hash::CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RenderConfig { pub struct RenderConfig {
pub viewport: Footprint, pub viewport: Footprint,
@@ -105,6 +112,7 @@ pub struct EditorApi<Io> {
pub node_graph_message_sender: Box<dyn NodeGraphUpdateSender + Send + Sync>, pub node_graph_message_sender: Box<dyn NodeGraphUpdateSender + Send + Sync>,
/// Editor preferences made available to the graph through the `PlatformEditorApi`. /// Editor preferences made available to the graph through the `PlatformEditorApi`.
pub editor_preferences: Box<dyn GetEditorPreferences + Send + Sync>, pub editor_preferences: Box<dyn GetEditorPreferences + Send + Sync>,
pub runtime: core_types::runtime::RuntimeHandle,
} }
impl<Io> Eq for EditorApi<Io> {} impl<Io> Eq for EditorApi<Io> {}
@@ -115,6 +123,7 @@ impl<Io: Default> Default for EditorApi<Io> {
application_io: None, application_io: None,
node_graph_message_sender: Box::new(Logger), node_graph_message_sender: Box::new(Logger),
editor_preferences: Box::new(DummyPreferences), editor_preferences: Box::new(DummyPreferences),
runtime: Default::default(),
} }
} }
} }
+4 -228
View File
@@ -496,185 +496,12 @@ impl ExtractFootprint for () {
} }
} }
// ========================================== // ==============
// EXTRACT TRAIT IMPLS FOR `OwnedContextImpl` // TYPE `Context`
// ========================================== // ==============
impl ArcCtx for OwnedContextImpl {} pub type Context<'a> = ContextImpl<'a>;
impl ExtractFootprint for OwnedContextImpl {
fn try_footprint(&self) -> Option<&Footprint> {
self.footprint.as_ref()
}
}
impl ExtractRealTime for OwnedContextImpl {
fn try_real_time(&self) -> Option<f64> {
self.real_time
}
}
impl ExtractAnimationTime for OwnedContextImpl {
fn try_animation_time(&self) -> Option<f64> {
self.animation_time
}
}
impl ExtractPointerPosition for OwnedContextImpl {
fn try_pointer_position(&self) -> Option<DVec2> {
self.pointer_position
}
}
impl ExtractPosition for OwnedContextImpl {
fn try_position(&self) -> Option<impl Iterator<Item = DVec2>> {
self.position.clone().map(|x| x.into_iter())
}
}
impl ExtractIndex for OwnedContextImpl {
fn try_index(&self) -> Option<impl Iterator<Item = usize>> {
self.index.clone().map(|x| x.into_iter())
}
}
impl ExtractVarArgs for OwnedContextImpl {
fn vararg(&self, index: usize) -> Result<DynRef<'_>, VarArgsResult> {
let Some(ref inner) = self.varargs else {
let Some(ref parent) = self.parent else {
return Err(VarArgsResult::NoVarArgs);
};
return parent.vararg(index);
};
inner.get(index).map(|x| x.as_ref() as DynRef<'_>).ok_or(VarArgsResult::IndexOutOfBounds)
}
fn varargs_len(&self) -> Result<usize, VarArgsResult> {
let Some(ref inner) = self.varargs else {
let Some(ref parent) = self.parent else {
return Err(VarArgsResult::NoVarArgs);
};
return parent.varargs_len();
};
Ok(inner.len())
}
fn hash_varargs(&self, mut hasher: &mut dyn Hasher) {
match (&self.varargs, &self.parent) {
(Some(inner), _) => {
for arg in inner.iter() {
arg.hash(&mut hasher);
}
}
(None, Some(parent)) => {
parent.hash_varargs(hasher);
}
_ => (),
};
}
}
impl CloneVarArgs for Arc<OwnedContextImpl> {
fn arc_clone(&self) -> Option<Arc<dyn ExtractVarArgs + Send + Sync>> {
Some(self.clone())
}
}
// ======================================
// TYPES `Context` AND `OwnedContextImpl`
// ======================================
pub type Context<'a> = Option<Arc<OwnedContextImpl>>;
type DynRef<'a> = &'a (dyn Any + Send + Sync); type DynRef<'a> = &'a (dyn Any + Send + Sync);
type DynBox = Box<dyn AnyHash + Send + Sync>;
#[derive(dyn_any::DynAny)]
pub struct OwnedContextImpl {
parent: Option<Arc<dyn ExtractVarArgs + Sync + Send>>,
footprint: Option<Footprint>,
real_time: Option<f64>,
animation_time: Option<f64>,
pointer_position: Option<DVec2>,
position: Option<Vec<DVec2>>,
// This could be converted into a single enum to save extra bytes
index: Option<Vec<usize>>,
varargs: Option<Arc<[DynBox]>>,
}
impl std::fmt::Debug for OwnedContextImpl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OwnedContextImpl")
.field("parent", &self.parent.as_ref().map(|_| "<Parent>"))
.field("footprint", &self.footprint)
.field("real_time", &self.real_time)
.field("animation_time", &self.animation_time)
.field("pointer_position", &self.pointer_position)
.field("index", &self.index)
.field("varargs_len", &self.varargs.as_ref().map(|x| x.len()))
.finish()
}
}
impl Default for OwnedContextImpl {
#[track_caller]
fn default() -> Self {
Self::empty()
}
}
impl graphene_hash::CacheHash for OwnedContextImpl {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.footprint.cache_hash(state);
self.real_time.cache_hash(state);
self.animation_time.cache_hash(state);
self.pointer_position.cache_hash(state);
self.position.cache_hash(state);
self.index.cache_hash(state);
self.hash_varargs(state);
}
}
impl OwnedContextImpl {
#[track_caller]
pub fn from<T: ExtractAll + CloneVarArgs>(value: T) -> Self {
OwnedContextImpl::from_flags(value, ContextFeatures::all())
}
#[track_caller]
pub fn from_flags<T: ExtractAll + CloneVarArgs>(value: T, bitflags: ContextFeatures) -> Self {
let parent = bitflags
.contains(ContextFeatures::VARARGS)
.then(|| match value.varargs_len() {
Ok(x) if x > 0 => value.arc_clone(),
_ => None,
})
.flatten();
let footprint = bitflags.contains(ContextFeatures::FOOTPRINT).then(|| value.try_footprint().copied()).flatten();
let real_time = bitflags.contains(ContextFeatures::REAL_TIME).then(|| value.try_real_time()).flatten();
let animation_time = bitflags.contains(ContextFeatures::ANIMATION_TIME).then(|| value.try_animation_time()).flatten();
let pointer_position = bitflags.contains(ContextFeatures::POINTER_POSITION).then(|| value.try_pointer_position()).flatten();
let position = bitflags.contains(ContextFeatures::POSITION).then(|| value.try_position()).flatten().map(|x| x.collect());
let index = bitflags.contains(ContextFeatures::INDEX).then(|| value.try_index()).flatten().map(|x| x.collect());
OwnedContextImpl {
parent,
footprint,
real_time,
animation_time,
pointer_position,
position,
index,
varargs: None,
}
}
pub const fn empty() -> Self {
OwnedContextImpl {
parent: None,
footprint: None,
real_time: None,
animation_time: None,
pointer_position: None,
position: None,
index: None,
varargs: None,
}
}
}
pub trait DynHash { pub trait DynHash {
fn dyn_hash(&self, state: &mut dyn Hasher); fn dyn_hash(&self, state: &mut dyn Hasher);
@@ -732,57 +559,6 @@ impl std::fmt::Debug for OwnedSlot {
} }
} }
impl OwnedContextImpl {
pub fn set_footprint(&mut self, footprint: Footprint) {
self.footprint = Some(footprint);
}
pub fn with_footprint(mut self, footprint: Footprint) -> Self {
self.footprint = Some(footprint);
self
}
pub fn with_real_time(mut self, real_time: f64) -> Self {
self.real_time = Some(real_time);
self
}
pub fn with_animation_time(mut self, animation_time: f64) -> Self {
self.animation_time = Some(animation_time);
self
}
pub fn with_pointer_position(mut self, pointer_position: DVec2) -> Self {
self.pointer_position = Some(pointer_position);
self
}
pub fn with_position(mut self, position: DVec2) -> Self {
if let Some(current_position) = &mut self.position {
current_position.insert(0, position);
} else {
self.position = Some(vec![position]);
}
self
}
pub fn with_index(mut self, index: usize) -> Self {
if let Some(current_index) = &mut self.index {
current_index.insert(0, index);
} else {
self.index = Some(vec![index]);
}
self
}
pub fn with_vararg(mut self, value: Box<dyn AnyHash + Send + Sync>) -> Self {
assert!(self.varargs.is_none_or(|value| value.is_empty()));
self.varargs = Some(Arc::new([value]));
self
}
pub fn into_context(self) -> Option<Arc<Self>> {
Some(Arc::new(self))
}
pub fn erase_parent(mut self) -> Self {
self.parent = None;
self
}
}
pub type SourceId = u64; pub type SourceId = u64;
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
@@ -1,17 +0,0 @@
use crate::Node;
use std::marker::PhantomData;
#[derive(Clone)]
pub struct FnNode<T: Fn(I) -> O, I, O>(T, PhantomData<(I, O)>);
impl<'i, T: Fn(I) -> O + 'i, O: 'i, I: 'i> Node<'i, I> for FnNode<T, I, O> {
type Output = O;
fn eval(&'i self, input: I) -> Self::Output {
self.0(input)
}
}
impl<T: Fn(I) -> O, I, O> FnNode<T, I, O> {
pub fn new(f: T) -> Self {
FnNode(f, PhantomData)
}
}
+1 -100
View File
@@ -5,7 +5,6 @@ pub mod bounds;
pub mod consts; pub mod consts;
pub mod context; pub mod context;
pub mod frame_table; pub mod frame_table;
pub mod generic;
pub mod gpoll; pub mod gpoll;
pub mod list; pub mod list;
pub mod math; pub mod math;
@@ -39,113 +38,15 @@ pub use no_std_types::blending;
pub use no_std_types::choice_type; pub use no_std_types::choice_type;
pub use no_std_types::color; pub use no_std_types::color;
pub use no_std_types::shaders; pub use no_std_types::shaders;
pub use node::Node;
pub use num_traits; pub use num_traits;
use std::any::TypeId;
use std::future::Future;
use std::pin::Pin;
#[cfg(feature = "wasm")] #[cfg(feature = "wasm")]
pub use tsify; pub use tsify;
pub use types::Cow; pub use types::Cow;
// pub trait Node: for<'n> NodeIO<'n> {
/// The node trait allows for defining any node. Nodes can only take one call argument input, however they can store references to other nodes inside the struct.
/// See `node-graph/README.md` for information on how to define a new node.
pub trait Node<'i, Input> {
type Output: 'i;
/// Evaluates the node with the single specified input.
fn eval(&'i self, input: Input) -> Self::Output;
/// Resets the node, e.g. the LetNode's cache is set to None.
fn reset(&self) {}
/// Returns the name of the node for diagnostic purposes.
fn node_name(&self) -> &'static str {
std::any::type_name::<Self>()
}
/// Serialize the node which is used for the `introspect` function which can retrieve values from monitor nodes.
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
log::warn!("Node::serialize not implemented for {}", std::any::type_name::<Self>());
None
}
}
mod types; mod types;
pub use types::*; pub use types::*;
pub trait NodeIO<'i, Input>: Node<'i, Input>
where
Self::Output: 'i + StaticTypeSized,
Input: StaticTypeSized,
{
fn input_type(&self) -> TypeId {
TypeId::of::<Input::Static>()
}
fn input_type_name(&self) -> &'static str {
std::any::type_name::<Input>()
}
fn output_type(&self) -> TypeId {
TypeId::of::<<Self::Output as StaticTypeSized>::Static>()
}
fn output_type_name(&self) -> &'static str {
std::any::type_name::<Self::Output>()
}
fn to_node_io(&self, inputs: Vec<Type>) -> NodeIOTypes {
NodeIOTypes {
call_argument: concrete!(<Input as StaticTypeSized>::Static),
return_value: concrete!(<Self::Output as StaticTypeSized>::Static),
inputs,
}
}
fn to_async_node_io(&self, inputs: Vec<Type>) -> NodeIOTypes
where
<Self::Output as Future>::Output: StaticTypeSized,
Self::Output: Future,
{
NodeIOTypes {
call_argument: concrete!(<Input as StaticTypeSized>::Static),
return_value: future!(<<Self::Output as Future>::Output as StaticTypeSized>::Static),
inputs,
}
}
}
impl<'i, N: Node<'i, I>, I> NodeIO<'i, I> for N
where
N::Output: 'i + StaticTypeSized,
I: StaticTypeSized,
{
}
impl<'i, I: 'i, N: Node<'i, I> + ?Sized> Node<'i, I> for &'i N {
type Output = N::Output;
fn eval(&'i self, input: I) -> N::Output {
(*self).eval(input)
}
}
impl<'i, I: 'i, O: 'i, N: Node<'i, I, Output = O> + ?Sized> Node<'i, I> for Box<N> {
type Output = O;
fn eval(&'i self, input: I) -> O {
(**self).eval(input)
}
}
impl<'i, I: 'i, O: 'i, N: Node<'i, I, Output = O> + ?Sized> Node<'i, I> for std::sync::Arc<N> {
type Output = O;
fn eval(&'i self, input: I) -> O {
(**self).eval(input)
}
}
impl<'i, I, O: 'i> Node<'i, I> for Pin<Box<dyn Node<'i, I, Output = O> + 'i>> {
type Output = O;
fn eval(&'i self, input: I) -> O {
(**self).eval(input)
}
}
impl<'i, I, O: 'i> Node<'i, I> for Pin<&'i (dyn NodeIO<'i, I, Output = O> + 'i)> {
type Output = O;
fn eval(&'i self, input: I) -> O {
(**self).eval(input)
}
}
pub trait InputAccessorSource<'a, T>: InputAccessorSourceIdentifier + std::fmt::Debug { pub trait InputAccessorSource<'a, T>: InputAccessorSourceIdentifier + std::fmt::Debug {
fn get_input(&'a self, index: usize) -> Option<&'a T>; fn get_input(&'a self, index: usize) -> Option<&'a T>;
fn set_input(&'a mut self, index: usize, value: T); fn set_input(&'a mut self, index: usize, value: T);
+16 -46
View File
@@ -1,56 +1,26 @@
use crate::Node;
use crate::list::{Attribute, AttributeDyn, AttributeValueDyn, Item, List, ListDyn}; use crate::list::{Attribute, AttributeDyn, AttributeValueDyn, Item, List, ListDyn};
use crate::transform::Footprint; use crate::transform::Footprint;
use glam::DVec2; use glam::DVec2;
use graphene_hash::CacheHash; use graphene_hash::CacheHash;
use std::future::Future;
use std::marker::PhantomData;
// Type
// TODO: Document this
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct TypeNode<N: for<'a> Node<'a, I>, I, O>(pub N, pub PhantomData<(I, O)>);
impl<'i, N, I: 'i, O: 'i> Node<'i, I> for TypeNode<N, I, O>
where
N: for<'n> Node<'n, I, Output = O>,
{
type Output = O;
fn eval(&'i self, input: I) -> Self::Output {
self.0.eval(input)
}
fn reset(&self) {
self.0.reset();
}
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
self.0.serialize()
}
}
impl<'i, N: for<'a> Node<'a, I>, I: 'i> TypeNode<N, I, <N as Node<'i, I>>::Output> {
pub fn new(node: N) -> Self {
Self(node, PhantomData)
}
}
impl<'i, N: for<'a> Node<'a, I> + Clone, I: 'i> Clone for TypeNode<N, I, <N as Node<'i, I>>::Output> {
fn clone(&self) -> Self {
Self(self.0.clone(), self.1)
}
}
impl<'i, N: for<'a> Node<'a, I> + Copy, I: 'i> Copy for TypeNode<N, I, <N as Node<'i, I>>::Output> {}
/// The [`Convert`] trait allows for conversion between Rust primitive numeric types. /// The [`Convert`] trait allows for conversion between Rust primitive numeric types.
/// Because number casting is lossy, we cannot use the normal [`Into`] trait like we do for other types. /// Because number casting is lossy, we cannot use the normal [`Into`] trait like we do for other types.
pub trait Convert<T, C>: Sized { pub trait Convert<T, C>: Sized {
/// Converts this type into the (usually inferred) output type. /// Converts this type into the (usually inferred) output type.
#[must_use] #[must_use]
fn convert(self, footprint: Footprint, converter: C) -> impl Future<Output = T> + Send; fn convert(self, footprint: Footprint, converter: C) -> T;
}
/// The asynchronous counterpart of [`Convert`]; a conversion pair implements exactly one of the two traits.
pub trait ConvertAsync<T, C>: Sized {
#[must_use]
fn convert(self, footprint: Footprint, converter: C) -> crate::runtime::SourceFuture<T>;
} }
impl<T: ToString + Send> Convert<String, ()> for T { impl<T: ToString + Send> Convert<String, ()> for T {
/// Converts this type into a `String` using its `ToString` implementation. /// Converts this type into a `String` using its `ToString` implementation.
#[inline] #[inline]
async fn convert(self, _: Footprint, _converter: ()) -> String { fn convert(self, _: Footprint, _converter: ()) -> String {
self.to_string() self.to_string()
} }
} }
@@ -60,7 +30,7 @@ pub trait ListConvert<U> {
} }
impl<U, T: ListConvert<U> + Send> Convert<List<U>, ()> for List<T> { impl<U, T: ListConvert<U> + Send> Convert<List<U>, ()> for List<T> {
async fn convert(self, _: Footprint, _: ()) -> List<U> { fn convert(self, _: Footprint, _: ()) -> List<U> {
let list: List<U> = self let list: List<U> = self
.into_iter() .into_iter()
.map(|row| { .map(|row| {
@@ -76,7 +46,7 @@ impl<U, T: ListConvert<U> + Send> Convert<List<U>, ()> for List<T> {
/// from any `List<U>` express their signature as `AttributeDyn` and avoid monomorphizing /// from any `List<U>` express their signature as `AttributeDyn` and avoid monomorphizing
/// over `U`; the compiler inserts this convert to bridge concrete-typed graph wires to the dyn input. /// over `U`; the compiler inserts this convert to bridge concrete-typed graph wires to the dyn input.
impl<T: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash + 'static> Convert<AttributeDyn, ()> for List<T> { impl<T: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash + 'static> Convert<AttributeDyn, ()> for List<T> {
async fn convert(self, _: Footprint, _: ()) -> AttributeDyn { fn convert(self, _: Footprint, _: ()) -> AttributeDyn {
let values: Vec<T> = self.into_iter().map(|row| row.into_element()).collect(); let values: Vec<T> = self.into_iter().map(|row| row.into_element()).collect();
AttributeDyn(Box::new(Attribute(values))) AttributeDyn(Box::new(Attribute(values)))
} }
@@ -86,7 +56,7 @@ impl<T: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash
/// (such as `write_attribute`'s value-producing input) be generic over the destination list type /// (such as `write_attribute`'s value-producing input) be generic over the destination list type
/// alone, with the compiler-inserted convert handling each concrete value type at the wire level. /// alone, with the compiler-inserted convert handling each concrete value type at the wire level.
impl<T: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash + 'static> Convert<AttributeValueDyn, ()> for T { impl<T: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash + 'static> Convert<AttributeValueDyn, ()> for T {
async fn convert(self, _: Footprint, _: ()) -> AttributeValueDyn { fn convert(self, _: Footprint, _: ()) -> AttributeValueDyn {
AttributeValueDyn(Box::new(self)) AttributeValueDyn(Box::new(self))
} }
} }
@@ -95,13 +65,13 @@ impl<T: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash
/// only need attribute access (such as the `read_attribute_*` family) take a single `ListDyn` input /// only need attribute access (such as the `read_attribute_*` family) take a single `ListDyn` input
/// instead of monomorphizing over every possible carrier list type. /// instead of monomorphizing over every possible carrier list type.
impl<T: Send> Convert<ListDyn, ()> for List<T> { impl<T: Send> Convert<ListDyn, ()> for List<T> {
async fn convert(self, _: Footprint, _: ()) -> ListDyn { fn convert(self, _: Footprint, _: ()) -> ListDyn {
self.into() self.into()
} }
} }
impl Convert<DVec2, ()> for DVec2 { impl Convert<DVec2, ()> for DVec2 {
async fn convert(self, _: Footprint, _: ()) -> DVec2 { fn convert(self, _: Footprint, _: ()) -> DVec2 {
self self
} }
} }
@@ -115,7 +85,7 @@ pub trait FromAnchorPosition {
// Converts a position into a vector path composed of a single anchor point // Converts a position into a vector path composed of a single anchor point
impl<T: FromAnchorPosition + Send> Convert<List<T>, ()> for DVec2 { impl<T: FromAnchorPosition + Send> Convert<List<T>, ()> for DVec2 {
async fn convert(self, _: Footprint, _: ()) -> List<T> { fn convert(self, _: Footprint, _: ()) -> List<T> {
List::new_from_item(Item::new_from_element(T::from_anchor_position(self))) List::new_from_item(Item::new_from_element(T::from_anchor_position(self)))
} }
} }
@@ -124,7 +94,7 @@ impl<T: FromAnchorPosition + Send> Convert<List<T>, ()> for DVec2 {
macro_rules! impl_convert { macro_rules! impl_convert {
($from:ty, $to:ty) => { ($from:ty, $to:ty) => {
impl Convert<$to, ()> for $from { impl Convert<$to, ()> for $from {
async fn convert(self, _: Footprint, _: ()) -> $to { fn convert(self, _: Footprint, _: ()) -> $to {
self as $to self as $to
} }
} }
@@ -146,7 +116,7 @@ macro_rules! impl_convert {
impl_convert!(usize, $to); impl_convert!(usize, $to);
impl Convert<DVec2, ()> for $to { impl Convert<DVec2, ()> for $to {
async fn convert(self, _: Footprint, _: ()) -> DVec2 { fn convert(self, _: Footprint, _: ()) -> DVec2 {
DVec2::splat(self as f64) DVec2::splat(self as f64)
} }
} }
+428 -208
View File
@@ -1,10 +1,12 @@
use crate::{ContextFeature, Node, NodeIO, NodeIOTypes, ProtoNodeIdentifier, Type, WasmNotSend}; use crate::concrete;
use dyn_any::{DynAny, StaticType}; use crate::context::{Context, ContextImpl};
use crate::node::Node;
use crate::{ContextFeature, ProtoNodeIdentifier, Type, WasmNotSend, WasmNotSync};
use dyn_any::DynAny;
use graphene_hash::CacheHash;
pub use no_std_types::registry::types; pub use no_std_types::registry::types;
use std::collections::HashMap; use std::collections::HashMap;
use std::marker::PhantomData; use std::hash::Hasher;
use std::ops::Deref;
use std::pin::Pin;
use std::sync::{LazyLock, Mutex}; use std::sync::{LazyLock, Mutex};
// Translation struct between macro and definition // Translation struct between macro and definition
@@ -55,239 +57,457 @@ pub enum RegistryValueSource {
None, None,
Default(&'static str), Default(&'static str),
Scope(&'static str), Scope(&'static str),
SourceId,
} }
type NodeRegistry = LazyLock<Mutex<HashMap<ProtoNodeIdentifier, Vec<(NodeConstructor, NodeIOTypes)>>>>; type NodeRegistry = LazyLock<Mutex<HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>>>>;
pub static NODE_REGISTRY: NodeRegistry = LazyLock::new(|| Mutex::new(HashMap::new())); pub static NODE_REGISTRY: NodeRegistry = LazyLock::new(|| Mutex::new(HashMap::new()));
pub static NODE_METADATA: LazyLock<Mutex<HashMap<ProtoNodeIdentifier, NodeMetadata>>> = LazyLock::new(|| Mutex::new(HashMap::new())); pub static NODE_METADATA: LazyLock<Mutex<HashMap<ProtoNodeIdentifier, NodeMetadata>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
pub use crate::NodeIOTypes;
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
pub type DynFuture<'n, T> = Pin<Box<dyn Future<Output = T> + 'n + Send>>; pub type ErasedNode<T> = dyn for<'c> Node<ContextImpl<'c>, Output = T> + Send + Sync;
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
pub type DynFuture<'n, T> = Pin<Box<dyn std::future::Future<Output = T> + 'n>>; pub type ErasedNode<T> = dyn for<'c> Node<ContextImpl<'c>, Output = T>;
pub type LocalFuture<'n, T> = Pin<Box<dyn Future<Output = T> + 'n>>; #[cfg(not(target_family = "wasm"))]
pub type ErasedLendNode<T> = dyn for<'c> Node<ContextImpl<'c>, Output = &'c T> + Send + Sync;
#[cfg(target_family = "wasm")]
pub type ErasedLendNode<T> = dyn for<'c> Node<ContextImpl<'c>, Output = &'c T>;
#[cfg(not(target_family = "wasm"))]
type DynEdge = dyn std::any::Any + Send + Sync;
#[cfg(target_family = "wasm")]
type DynEdge = dyn std::any::Any;
pub fn edge_type<T: 'static>() -> Type {
Type::Fn(Box::new(concrete!(Context)), Box::new(concrete!(T)))
}
pub fn lend_edge_type<T: 'static>() -> Type {
Type::Fn(Box::new(concrete!(Context)), Box::new(Type::Ref(Box::new(concrete!(T)))))
}
pub fn cache_key<C: CacheHash + ?Sized>(ctx: &C) -> u64 {
let mut hasher = graphene_hash::FxHasher64::new();
ctx.cache_hash(&mut hasher);
hasher.finish()
}
#[derive(Debug, PartialEq)]
pub enum ConstructionError {
Arity { expected: usize, got: usize },
Type { expected: Box<Type>, found: Box<Type> },
}
pub struct SharedEdge<N: ?Sized> {
ptr: std::ptr::NonNull<N>,
own: std::sync::Arc<N>,
}
impl<N: ?Sized> SharedEdge<N> {
pub fn new(own: std::sync::Arc<N>) -> Self {
Self {
ptr: std::ptr::NonNull::from(&*own),
own,
}
}
pub fn share(&self) -> Self {
Self { ptr: self.ptr, own: self.own.clone() }
}
}
// SAFETY: `ptr` is derived from the owned Arc and never mutated through, so the edge is exactly as
// thread safe as the payload it shares.
unsafe impl<N: ?Sized + Send + Sync> Send for SharedEdge<N> {}
// SAFETY: as in Send.
unsafe impl<N: ?Sized + Send + Sync> Sync for SharedEdge<N> {}
impl<Input, N> Node<Input> for SharedEdge<N>
where
N: Node<Input> + ?Sized,
{
type Output = N::Output;
fn eval(&self, input: &Input) -> crate::gpoll::GPoll<Self::Output> {
// SAFETY: `own` keeps the payload alive for `self`'s lifetime and Arc
// payloads are address stable.
unsafe { self.ptr.as_ref() }.eval(input)
}
fn extent(&self, input: &Input) -> crate::gpoll::GPoll<crate::gpoll::Extent> {
// SAFETY: as in eval.
unsafe { self.ptr.as_ref() }.extent(input)
}
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
// SAFETY: as in eval.
unsafe { self.ptr.as_ref() }.serialize()
}
fn eval_batch<'a>(&self, input: &'a Input, range: std::ops::Range<u64>, scratch: Option<&'a mut [std::mem::MaybeUninit<Self::Output>]>) -> crate::node::BatchStatus<'a, Self::Output>
where
Input: crate::context::InjectIndex + Copy,
{
// SAFETY: as in eval.
unsafe { self.ptr.as_ref() }.eval_batch(input, range, scratch)
}
}
pub struct EdgeHandle {
node: Box<DynEdge>,
share: fn(&DynEdge) -> Box<DynEdge>,
serialize: fn(&DynEdge) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
ty: Type,
}
impl std::fmt::Debug for EdgeHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EdgeHandle").field("ty", &self.ty).finish_non_exhaustive()
}
}
// SAFETY: wasm is single threaded, so the marker-free payload never actually crosses a thread.
#[cfg(target_family = "wasm")]
unsafe impl Send for EdgeHandle {}
// SAFETY: as in Send.
#[cfg(target_family = "wasm")]
unsafe impl Sync for EdgeHandle {}
impl EdgeHandle {
pub fn new<T: 'static>(node: std::sync::Arc<ErasedNode<T>>) -> Self {
Self::new_erased(node, edge_type::<T>())
}
pub fn new_ref<T: 'static>(node: std::sync::Arc<ErasedLendNode<T>>) -> Self {
Self::new_erased(node, lend_edge_type::<T>())
}
pub fn new_erased<N>(node: std::sync::Arc<N>, ty: Type) -> Self
where
N: ?Sized + 'static + for<'c> Node<ContextImpl<'c>>,
SharedEdge<N>: WasmNotSend + WasmNotSync,
{
Self {
node: Box::new(SharedEdge::new(node)),
share: |edge| Box::new(edge.downcast_ref::<SharedEdge<N>>().expect("share hook matches the stored edge type").share()),
serialize: |edge| Node::<ContextImpl>::serialize(edge.downcast_ref::<SharedEdge<N>>().expect("serialize hook matches the stored edge type")),
ty,
}
}
pub fn ty(&self) -> &Type {
&self.ty
}
pub fn duplicate(&self) -> Self {
Self {
node: (self.share)(&*self.node),
share: self.share,
serialize: self.serialize,
ty: self.ty.clone(),
}
}
pub fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
(self.serialize)(&*self.node)
}
pub fn downcast<T: 'static>(self) -> Result<SharedEdge<ErasedNode<T>>, ConstructionError> {
self.downcast_erased(edge_type::<T>())
}
pub fn downcast_lend<T: 'static>(self) -> Result<SharedEdge<ErasedLendNode<T>>, ConstructionError> {
self.downcast_erased(lend_edge_type::<T>())
}
pub fn downcast_erased<N: ?Sized + 'static>(self, expected: Type) -> Result<SharedEdge<N>, ConstructionError> {
let found = self.ty;
self.node.downcast::<SharedEdge<N>>().map(|edge| *edge).map_err(|_| ConstructionError::Type {
expected: Box::new(expected),
found: Box::new(found),
})
}
}
pub type NodeConstructor = fn(Vec<EdgeHandle>) -> Result<EdgeHandle, ConstructionError>;
#[derive(Clone)]
pub struct RegistryEntry {
pub io: NodeIOTypes,
pub constructor: NodeConstructor,
}
pub fn construct(entry: &RegistryEntry, inputs: Vec<EdgeHandle>) -> Result<EdgeHandle, ConstructionError> {
if inputs.len() != entry.io.inputs.len() {
return Err(ConstructionError::Arity {
expected: entry.io.inputs.len(),
got: inputs.len(),
});
}
for (handle, expected) in inputs.iter().zip(&entry.io.inputs) {
if handle.ty() != expected {
return Err(ConstructionError::Type {
expected: Box::new(expected.clone()),
found: Box::new(handle.ty().clone()),
});
}
}
(entry.constructor)(inputs)
}
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
pub type Any<'n> = Box<dyn DynAny<'n> + 'n + Send>; pub type Any<'n> = Box<dyn DynAny<'n> + 'n + Send>;
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
pub type Any<'n> = Box<dyn DynAny<'n> + 'n>; pub type Any<'n> = Box<dyn DynAny<'n> + 'n>;
pub type FutureAny<'n> = DynFuture<'n, Any<'n>>;
// TODO: is this safe? This is assumed to be send+sync.
#[cfg(not(target_family = "wasm"))]
pub type TypeErasedNode<'n> = dyn for<'i> NodeIO<'i, Any<'i>, Output = FutureAny<'i>> + 'n + Send + Sync;
#[cfg(target_family = "wasm")]
pub type TypeErasedNode<'n> = dyn for<'i> NodeIO<'i, Any<'i>, Output = FutureAny<'i>> + 'n;
pub type TypeErasedPinnedRef<'n> = Pin<&'n TypeErasedNode<'n>>;
pub type TypeErasedRef<'n> = &'n TypeErasedNode<'n>;
pub type TypeErasedBox<'n> = Box<TypeErasedNode<'n>>;
pub type TypeErasedPinned<'n> = Pin<Box<TypeErasedNode<'n>>>;
pub type SharedNodeContainer = std::sync::Arc<NodeContainer>; #[cfg(test)]
mod tests {
use super::*;
use crate::SourceId;
use crate::arena::Arena;
use crate::context::{Ctx, EvalScope, ExtractArena};
use crate::gpoll::GPoll;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
pub type NodeConstructor = fn(Vec<SharedNodeContainer>) -> DynFuture<'static, TypeErasedBox<'static>>; struct CountingNode(AtomicU32);
#[derive(Clone)] impl<Input> Node<Input> for CountingNode {
pub struct NodeContainer { type Output = u32;
#[cfg(feature = "dealloc_nodes")]
pub node: *const TypeErasedNode<'static>,
#[cfg(not(feature = "dealloc_nodes"))]
pub node: TypeErasedRef<'static>,
}
impl Deref for NodeContainer { fn eval(&self, _input: &Input) -> GPoll<u32> {
type Target = TypeErasedNode<'static>; GPoll::Final(self.0.fetch_add(1, Ordering::Relaxed) + 1)
#[cfg(feature = "dealloc_nodes")]
fn deref(&self) -> &Self::Target {
unsafe { &*(self.node) }
#[cfg(not(feature = "dealloc_nodes"))]
self.node
}
#[cfg(not(feature = "dealloc_nodes"))]
fn deref(&self) -> &Self::Target {
self.node
}
}
/// # Safety
/// Marks NodeContainer as Sync. This dissallows the use of threadlocal storage for nodes as this would invalidate references to them.
// TODO: implement this on a higher level wrapper to avoid missuse
#[cfg(feature = "dealloc_nodes")]
unsafe impl Send for NodeContainer {}
#[cfg(feature = "dealloc_nodes")]
unsafe impl Sync for NodeContainer {}
#[cfg(feature = "dealloc_nodes")]
impl Drop for NodeContainer {
fn drop(&mut self) {
unsafe { self.dealloc_unchecked() }
}
}
impl std::fmt::Debug for NodeContainer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NodeContainer").finish()
}
}
impl NodeContainer {
pub fn new(node: TypeErasedBox<'static>) -> SharedNodeContainer {
let node = Box::leak(node);
Self { node }.into()
}
#[cfg(feature = "dealloc_nodes")]
unsafe fn dealloc_unchecked(&mut self) {
unsafe {
drop(Box::from_raw(self.node as *mut TypeErasedNode));
} }
} }
}
/// Boxes the input and downcasts the output. struct ValueNode<T>(T);
/// Wraps around a node taking Box<dyn DynAny> and returning Box<dyn DynAny>
#[derive(Clone)] impl<T: Clone, Input> Node<Input> for ValueNode<T> {
pub struct DowncastBothNode<I, O> { type Output = T;
node: SharedNodeContainer,
_i: PhantomData<I>, fn eval(&self, _input: &Input) -> GPoll<T> {
_o: PhantomData<O>, GPoll::Final(self.0.clone())
} }
impl<'input, O, I> Node<'input, I> for DowncastBothNode<I, O> }
where
O: 'input + StaticType + WasmNotSend, struct LendNode(String);
I: 'input + StaticType + WasmNotSend,
{ impl<'e, Input: Ctx + ExtractArena<ArenaRef = &'e Arena>> Node<Input> for LendNode {
type Output = DynFuture<'input, O>; type Output = &'e String;
#[inline]
#[track_caller] fn eval(&self, input: &Input) -> GPoll<&'e String> {
fn eval(&'input self, input: I) -> Self::Output { match input.arena().alloc(self.0.clone()) {
Some((parked, _)) => GPoll::Final(parked),
None => GPoll::arena_exhausted(),
}
}
}
fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
EvalScope::new(Some(0.5), None, None, generations, arena)
}
#[test]
fn borrow_carrying_value_types_wire_through_the_general_constructor() {
struct SplitBorrow<'c>(&'c str, usize);
struct SplitNode<Node0> {
content: Node0,
}
impl<'e, Input, Node0> Node<Input> for SplitNode<Node0>
where
Input: Ctx,
Node0: Node<Input, Output = &'e String>,
{ {
let node_name = self.node.node_name(); type Output = SplitBorrow<'e>;
let input = Box::new(input);
let future = self.node.eval(input); fn eval(&self, input: &Input) -> GPoll<SplitBorrow<'e>> {
Box::pin(async move { self.content.eval(input).map(|value| SplitBorrow(value, value.len()))
let out = dyn_any::downcast(future.await).unwrap_or_else(|e| panic!("DowncastBothNode wrong output type: {e} in: \n{node_name}")); }
*out
})
} }
}
fn reset(&self) {
self.node.reset();
}
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> { type ErasedSplitEdge = dyn for<'c> Node<ContextImpl<'c>, Output = SplitBorrow<'c>> + Send + Sync;
self.node.serialize()
}
}
impl<I, O> DowncastBothNode<I, O> {
pub const fn new(node: SharedNodeContainer) -> Self {
Self {
node,
_i: PhantomData,
_o: PhantomData,
}
}
}
pub struct FutureWrapperNode<Node> {
node: Node,
}
impl<'i, T: 'i + WasmNotSend, N> Node<'i, T> for FutureWrapperNode<N> let arena = Arena::new(4096);
where let generations = [];
N: Node<'i, T, Output: WasmNotSend> + WasmNotSend, let scope = scope_fixture(&generations, &arena);
{ let ctx = ContextImpl::root(&scope);
type Output = DynFuture<'i, N::Output>;
#[inline(always)]
fn eval(&'i self, input: T) -> Self::Output {
let result = self.node.eval(input);
Box::pin(async move { result })
}
#[inline(always)]
fn reset(&self) {
self.node.reset();
}
#[inline(always)] let lending = EdgeHandle::new_ref(Arc::new(LendNode("held".to_string())) as Arc<ErasedLendNode<String>>);
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> { let upstream = lending.downcast_lend::<String>().unwrap();
self.node.serialize() let node: Arc<ErasedSplitEdge> = Arc::new(SplitNode { content: upstream });
} let handle = EdgeHandle::new_erased(node, concrete!(SplitBorrow<'static>));
} assert_eq!(*handle.ty(), concrete!(SplitBorrow<'static>));
impl<N> FutureWrapperNode<N> { let wired = handle.downcast_erased::<ErasedSplitEdge>(concrete!(SplitBorrow<'static>)).unwrap();
pub const fn new(node: N) -> Self { let GPoll::Final(split) = wired.eval(&ctx) else {
Self { node } panic!("borrow-carrying output must eval through the erased edge");
}
}
pub struct DynAnyNode<I, O, Node> {
node: Node,
_i: PhantomData<I>,
_o: PhantomData<O>,
}
impl<'input, I, O, N> Node<'input, Any<'input>> for DynAnyNode<I, O, N>
where
I: 'input + StaticType + WasmNotSend,
O: 'input + StaticType + WasmNotSend,
N: 'input + Node<'input, I, Output = DynFuture<'input, O>>,
{
type Output = FutureAny<'input>;
#[inline]
fn eval(&'input self, input: Any<'input>) -> Self::Output {
let node_name = std::any::type_name::<N>();
let output = |input| {
let result = self.node.eval(input);
async move { Box::new(result.await) as Any<'input> }
}; };
match dyn_any::downcast(input) { assert_eq!(split.0, "held");
Ok(input) => Box::pin(output(*input)), assert_eq!(split.1, 4);
Err(e) => panic!("DynAnyNode Input, {e} in:\n{node_name}"), }
#[test]
fn derive_ctx_repeat_pushes_index_levels_through_the_erased_edge() {
use crate::context::{DeriveCtx, Derived, ExtractIndex};
struct RepeatNode<Node0> {
content: Node0,
} }
}
fn reset(&self) { impl<C, T, Node0> Node<C> for RepeatNode<Node0>
self.node.reset(); where
} C: Ctx + DeriveCtx,
Node0: for<'x> Node<Derived<'x, C>, Output = T>,
{
type Output = Vec<T>;
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> { fn eval(&self, input: &C) -> GPoll<Vec<T>> {
self.node.serialize() let spilled = input.index_head();
} let mut result = Vec::new();
} for index in 0..3 {
impl<'input, I, O, N> DynAnyNode<I, O, N> let derived = input.promoted(&spilled, index);
where match self.content.eval(&derived) {
I: 'input + StaticType, GPoll::Final(value) => result.push(value),
O: 'input + StaticType, other => return other.map(|_| Vec::new()),
N: 'input + Node<'input, I, Output = DynFuture<'input, O>>, }
{ }
pub const fn new(node: N) -> Self { GPoll::Final(result)
Self { }
node,
_i: PhantomData,
_o: PhantomData,
} }
struct LevelsNode;
impl<Input: ExtractIndex> Node<Input> for LevelsNode {
type Output = Vec<usize>;
fn eval(&self, input: &Input) -> GPoll<Vec<usize>> {
GPoll::Final(input.try_index().map(|levels| levels.collect()).unwrap_or_default())
}
}
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let nested = RepeatNode {
content: RepeatNode { content: LevelsNode },
};
let erased: Box<ErasedNode<Vec<Vec<Vec<usize>>>>> = Box::new(nested);
let GPoll::Final(outer) = erased.eval(&ctx) else {
panic!("nested repeat must evaluate");
};
assert_eq!(outer.len(), 3);
assert_eq!(outer[2][1], vec![1, 2, 0]);
assert_eq!(outer[0][0], vec![0, 0, 0]);
}
#[test]
fn derive_ctx_footprint_replace_reaches_the_content() {
use crate::context::{DeriveCtx, Derived, ExtractFootprint};
use crate::transform::Footprint;
struct ShiftFootprintNode<Node0> {
content: Node0,
}
impl<C, T, Node0> Node<C> for ShiftFootprintNode<Node0>
where
C: Ctx + DeriveCtx + ExtractFootprint,
Node0: for<'x> Node<Derived<'x, C>, Output = T>,
{
type Output = T;
fn eval(&self, input: &C) -> GPoll<T> {
let mut footprint = input.try_footprint().copied().unwrap_or(Footprint::DEFAULT);
footprint.resolution.x += 7;
let derived = input.with_footprint(&footprint);
self.content.eval(&derived)
}
}
struct ResolutionNode;
impl<Input: ExtractFootprint> Node<Input> for ResolutionNode {
type Output = u32;
fn eval(&self, input: &Input) -> GPoll<u32> {
GPoll::Final(input.try_footprint().map(|footprint| footprint.resolution.x).unwrap_or(0))
}
}
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let graph: Box<ErasedNode<u32>> = Box::new(ShiftFootprintNode {
content: ShiftFootprintNode { content: ResolutionNode },
});
assert_eq!(graph.eval(&ctx), GPoll::Final(Footprint::DEFAULT.resolution.x + 14));
}
#[test]
fn construct_checks_arity_and_types() {
fn construct_strlen(args: Vec<EdgeHandle>) -> Result<EdgeHandle, ConstructionError> {
let mut args = args.into_iter();
let value = args.next().ok_or(ConstructionError::Arity { expected: 1, got: 0 })?.downcast::<String>()?;
drop(value);
Ok(EdgeHandle::new(Arc::new(ValueNode(0u32)) as Arc<ErasedNode<u32>>))
}
let entry = RegistryEntry {
io: NodeIOTypes::new(concrete!(Context), concrete!(u32), vec![edge_type::<String>()]),
constructor: construct_strlen,
};
let owned = EdgeHandle::new(Arc::new(ValueNode("typed".to_string())) as Arc<ErasedNode<String>>);
assert!(construct(&entry, vec![owned]).is_ok());
assert_eq!(construct(&entry, vec![]).unwrap_err(), ConstructionError::Arity { expected: 1, got: 0 });
let mistyped = EdgeHandle::new(Arc::new(ValueNode(1.0f64)) as Arc<ErasedNode<f64>>);
assert_eq!(
construct(&entry, vec![mistyped]).unwrap_err(),
ConstructionError::Type {
expected: Box::new(edge_type::<String>()),
found: Box::new(edge_type::<f64>()),
}
);
let lent = EdgeHandle::new_ref(Arc::new(LendNode("typed".to_string())) as Arc<ErasedLendNode<String>>);
assert_eq!(
construct(&entry, vec![lent]).unwrap_err(),
ConstructionError::Type {
expected: Box::new(edge_type::<String>()),
found: Box::new(lend_edge_type::<String>()),
}
);
}
#[test]
fn duplicated_edges_share_one_instance_and_outlive_each_other() {
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let handle = EdgeHandle::new(Arc::new(CountingNode(AtomicU32::new(0))) as Arc<ErasedNode<u32>>);
let duplicate = handle.duplicate();
assert_eq!(*duplicate.ty(), edge_type::<u32>());
let first = handle.downcast::<u32>().unwrap();
let second = duplicate.downcast::<u32>().unwrap();
assert_eq!(first.eval(&ctx), GPoll::Final(1));
assert_eq!(second.eval(&ctx), GPoll::Final(2));
drop(first);
assert_eq!(second.eval(&ctx), GPoll::Final(3));
} }
} }
pub struct PanicNode<I: WasmNotSend, O: WasmNotSend>(PhantomData<I>, PhantomData<O>);
impl<'i, I: 'i + WasmNotSend, O: 'i + WasmNotSend> Node<'i, I> for PanicNode<I, O> {
type Output = O;
fn eval(&'i self, _: I) -> Self::Output {
unimplemented!("This node should never be evaluated")
}
}
impl<I: WasmNotSend, O: WasmNotSend> PanicNode<I, O> {
pub const fn new() -> Self {
Self(PhantomData, PhantomData)
}
}
impl<I: WasmNotSend, O: WasmNotSend> Default for PanicNode<I, O> {
fn default() -> Self {
Self::new()
}
}
// TODO: Evaluate safety
unsafe impl<I: WasmNotSend, O: WasmNotSend> Sync for PanicNode<I, O> {}
@@ -146,3 +146,367 @@ impl<S: Spawner> Runtime for GraphRuntime<S> {
})); }));
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::arena::Arena;
use crate::context::{ContextImpl, Ctx, CtxSnapshot, EvalScope, ExtractFootprint, ExtractVarArgs, VarArgLink, VarArgSlots};
use crate::gpoll::GPoll;
use crate::node::Node;
use crate::transform::Footprint;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU32, Ordering};
#[derive(Default)]
struct MockRuntime {
futures: Mutex<Vec<(SourceId, SourceFuture)>>,
}
impl Runtime for MockRuntime {
fn spawn(&self, source: SourceId, future: SourceFuture) {
self.futures.lock().unwrap().push((source, future));
}
}
impl MockRuntime {
fn drain(&self) -> Vec<SourceId> {
let futures = std::mem::take(&mut *self.futures.lock().unwrap());
let mut task_ctx = std::task::Context::from_waker(std::task::Waker::noop());
futures
.into_iter()
.map(|(source, mut future)| {
assert!(future.as_mut().poll(&mut task_ctx).is_ready());
source
})
.collect()
}
}
#[derive(Default)]
struct CollectSpawner {
tasks: Mutex<Vec<SourceFuture>>,
}
impl Spawner for CollectSpawner {
fn spawn(&self, task: SourceFuture) {
self.tasks.lock().unwrap().push(task);
}
}
impl CollectSpawner {
fn drain(&self) -> usize {
let tasks = std::mem::take(&mut *self.tasks.lock().unwrap());
let mut task_ctx = std::task::Context::from_waker(std::task::Waker::noop());
let count = tasks.len();
for mut task in tasks {
assert!(task.as_mut().poll(&mut task_ctx).is_ready());
}
count
}
}
struct SourceNode<T>(T);
impl<T: Clone, Input> Node<Input> for SourceNode<T> {
type Output = T;
fn eval(&self, _input: &Input) -> GPoll<T> {
GPoll::Final(self.0.clone())
}
}
static SLOW_DOUBLE_RUNS: AtomicU32 = AtomicU32::new(0);
#[node_macro::node(category(""))]
async fn slow_double(_: impl Ctx, value: f64) -> f64 {
SLOW_DOUBLE_RUNS.fetch_add(1, Ordering::Relaxed);
value * 2.
}
fn stand_in(_value: &f64) -> f64 {
-1.
}
#[node_macro::node(category(""), placeholder(stand_in))]
async fn preview_double(_: impl Ctx, value: f64) -> f64 {
value * 2.
}
#[node_macro::node(category(""), placeholder(stand_in), no_partial)]
async fn strict_double(_: impl Ctx, value: f64) -> f64 {
value * 2.
}
#[node_macro::node(category(""))]
async fn snapshot_resolution(ctx: CtxSnapshot, _primary: ()) -> u32 {
ctx.try_footprint().map(|footprint| footprint.resolution.x).unwrap_or(0)
}
#[node_macro::node(category(""))]
async fn snapshot_vararg(ctx: CtxSnapshot, _primary: ()) -> f64 {
ctx.vararg(0).ok().and_then(|slot| slot.downcast_ref::<f64>()).copied().unwrap_or(0.)
}
static STAGED_RUNS: AtomicU32 = AtomicU32::new(0);
#[node_macro::node(category(""))]
fn staged_double(_: impl Ctx, value: f64) -> SourceFuture<f64> {
STAGED_RUNS.fetch_add(1, Ordering::Relaxed);
Box::pin(async move { value * 2. })
}
#[node_macro::node(category(""))]
fn staged_sum(ctx: impl Ctx, value: f64, addend: impl Node<Context<'_>, Output = f64>) -> Result<SourceFuture<f64>, crate::gpoll::Interrupt> {
let addend = addend.eval(ctx)?;
Ok(Box::pin(async move { value + addend }))
}
struct GatedSource(Arc<std::sync::atomic::AtomicBool>, f64);
impl<Input> Node<Input> for GatedSource {
type Output = f64;
fn eval(&self, _input: &Input) -> GPoll<f64> {
match self.0.load(Ordering::Relaxed) {
true => GPoll::Final(self.1),
false => GPoll::Pending,
}
}
}
fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
EvalScope::new(None, None, None, generations, arena)
}
#[test]
fn async_source_spawns_once_and_lands_via_the_slot() {
let arena = Arena::new(64);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let runtime = Arc::new(MockRuntime::default());
let graph = SlowDoubleNode::new(SourceNode(21.0f64), SourceNode(RuntimeHandle(runtime.clone())), SourceNode(7u64));
assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending);
assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending);
assert_eq!(SLOW_DOUBLE_RUNS.load(Ordering::Relaxed), 0);
assert_eq!(runtime.drain(), vec![7]);
assert_eq!(SLOW_DOUBLE_RUNS.load(Ordering::Relaxed), 1);
assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(42.0));
assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(42.0));
assert_eq!(SLOW_DOUBLE_RUNS.load(Ordering::Relaxed), 1);
}
#[test]
fn async_source_reports_the_placeholder_while_in_flight() {
let arena = Arena::new(64);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let runtime = Arc::new(MockRuntime::default());
let graph = PreviewDoubleNode::new(SourceNode(21.0f64), SourceNode(RuntimeHandle(runtime.clone())), SourceNode(1u64));
assert_eq!(Node::eval(&graph, &ctx), GPoll::Partial(-1.0));
runtime.drain();
assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(42.0));
}
#[test]
fn no_partial_maps_the_placeholder_frame_to_pending() {
let arena = Arena::new(64);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let runtime = Arc::new(MockRuntime::default());
let graph = StrictDoubleNode::new(SourceNode(21.0f64), SourceNode(RuntimeHandle(runtime.clone())), SourceNode(2u64));
assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending);
runtime.drain();
assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(42.0));
}
#[test]
fn prologue_runs_sync_and_spawns_once() {
let arena = Arena::new(64);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let runtime = Arc::new(MockRuntime::default());
let graph = StagedDoubleNode::new(SourceNode(21.0f64), SourceNode(RuntimeHandle(runtime.clone())), SourceNode(8u64));
assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending);
assert_eq!(STAGED_RUNS.load(Ordering::Relaxed), 1, "the prologue runs synchronously on the miss");
assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending);
assert_eq!(STAGED_RUNS.load(Ordering::Relaxed), 1, "in flight must not rerun the prologue");
assert_eq!(runtime.drain(), vec![8]);
assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(42.0));
assert_eq!(STAGED_RUNS.load(Ordering::Relaxed), 1);
}
#[test]
fn prologue_interrupt_defers_the_spawn() {
let arena = Arena::new(64);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let gate = Arc::new(std::sync::atomic::AtomicBool::new(false));
let runtime = Arc::new(MockRuntime::default());
let graph = StagedSumNode::new(SourceNode(40.0f64), GatedSource(gate.clone(), 2.0), SourceNode(RuntimeHandle(runtime.clone())), SourceNode(9u64));
assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending);
assert_eq!(runtime.drain(), Vec::<SourceId>::new(), "an interrupted prologue must not spawn or claim the slot");
gate.store(true, Ordering::Relaxed);
assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending);
assert_eq!(runtime.drain(), vec![9]);
assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(42.0));
}
#[test]
fn async_kernels_read_captured_varargs() {
let arena = Arena::new(64);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let root = ContextImpl::root(&scope);
let payload = 21.5f64;
let link = VarArgLink {
args: VarArgSlots::Single(&payload),
outer: None,
};
let ctx = root.with_varargs(&link);
let runtime = Arc::new(MockRuntime::default());
let graph = SnapshotVarargNode::new(SourceNode(()), SourceNode(RuntimeHandle(runtime.clone())), SourceNode(5u64));
assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending);
runtime.drain();
assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(21.5));
}
#[test]
fn async_kernels_read_the_captured_context_snapshot() {
let arena = Arena::new(64);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let root = ContextImpl::root(&scope);
let footprint = Footprint::DEFAULT;
let ctx = root.with_footprint(&footprint);
let runtime = Arc::new(MockRuntime::default());
let graph = SnapshotResolutionNode::new(SourceNode(()), SourceNode(RuntimeHandle(runtime.clone())), SourceNode(3u64));
assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending);
runtime.drain();
assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(Footprint::DEFAULT.resolution.x));
}
#[test]
fn the_epilogue_bumps_the_generation_and_sets_dirty() {
let runtime = GraphRuntime::new(CollectSpawner::default());
runtime.retain_sources(&[7]);
Runtime::spawn(&runtime, 7, Box::pin(async {}));
assert_eq!(runtime.snapshot(), vec![(7, 0)], "no bump before the future completes");
assert!(!runtime.take_dirty());
assert_eq!(runtime.spawner().drain(), 1);
assert_eq!(runtime.snapshot(), vec![(7, 1)]);
assert!(runtime.take_dirty());
assert!(!runtime.take_dirty(), "take_dirty drains the flag");
}
#[test]
fn the_epilogue_notifies_after_setting_dirty() {
let runtime = GraphRuntime::new(CollectSpawner::default());
runtime.retain_sources(&[7]);
let observed_dirty = Arc::new(AtomicBool::new(false));
let dirty_at_notify = Arc::clone(&runtime.dirty);
let observed = Arc::clone(&observed_dirty);
runtime.set_notifier(Arc::new(move || {
observed.store(dirty_at_notify.load(Ordering::Acquire), Ordering::Relaxed);
}));
Runtime::spawn(&runtime, 7, Box::pin(async {}));
assert_eq!(runtime.spawner().drain(), 1);
assert!(observed_dirty.load(Ordering::Relaxed), "the notifier must observe the dirty flag already set");
}
#[test]
fn the_epilogue_of_a_removed_source_does_not_notify() {
let runtime = GraphRuntime::new(CollectSpawner::default());
runtime.retain_sources(&[7]);
let notified = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&notified);
runtime.set_notifier(Arc::new(move || flag.store(true, Ordering::Relaxed)));
Runtime::spawn(&runtime, 7, Box::pin(async {}));
runtime.retain_sources(&[]);
assert_eq!(runtime.spawner().drain(), 1);
assert!(!notified.load(Ordering::Relaxed));
}
#[test]
fn the_epilogue_of_a_removed_source_is_inert() {
let runtime = GraphRuntime::new(CollectSpawner::default());
runtime.retain_sources(&[7]);
Runtime::spawn(&runtime, 7, Box::pin(async {}));
runtime.retain_sources(&[]);
assert_eq!(runtime.spawner().drain(), 1);
assert_eq!(runtime.snapshot(), Vec::<(SourceId, u64)>::new());
assert!(!runtime.take_dirty(), "a removed source must not invalidate");
}
#[test]
fn retain_sources_preserves_live_generations() {
let runtime = GraphRuntime::new(CollectSpawner::default());
runtime.retain_sources(&[7]);
Runtime::spawn(&runtime, 7, Box::pin(async {}));
runtime.spawner().drain();
runtime.retain_sources(&[7, 9]);
assert_eq!(runtime.snapshot(), vec![(7, 1), (9, 0)]);
runtime.retain_sources(&[9]);
assert_eq!(runtime.snapshot(), vec![(9, 0)]);
}
#[node_macro::node(category(""))]
async fn epilogue_double(_: impl Ctx, value: f64) -> f64 {
value * 2.
}
#[test]
fn a_source_slot_lands_through_the_runtime_while_downstream_keys_invalidate() {
let arena = Arena::new(64);
let runtime = Arc::new(GraphRuntime::new(CollectSpawner::default()));
runtime.retain_sources(&[11]);
let graph = EpilogueDoubleNode::new(SourceNode(21.0f64), SourceNode(RuntimeHandle(runtime.clone())), SourceNode(11u64));
let snapshot = runtime.snapshot();
let scope = EvalScope::new(None, None, None, &snapshot, &arena);
let ctx = ContextImpl::root(&scope);
assert_eq!(Node::eval(&graph, &ctx), GPoll::Pending);
assert!(!runtime.take_dirty());
assert_eq!(runtime.spawner().drain(), 1);
assert!(runtime.take_dirty());
let bumped = runtime.snapshot();
assert_eq!(bumped, vec![(11, 1)]);
let bumped_scope = EvalScope::new(None, None, None, &bumped, &arena);
let bumped_ctx = ContextImpl::root(&bumped_scope);
assert_eq!(Node::eval(&graph, &bumped_ctx), GPoll::Final(42.0), "the own-generation-excluded key replays the landed slot");
assert_eq!(runtime.spawner().drain(), 0, "a slot hit must not respawn");
let downstream_key = crate::registry::cache_key(&ContextImpl::root(&scope));
let bumped_downstream_key = crate::registry::cache_key(&ContextImpl::root(&bumped_scope));
assert_ne!(downstream_key, bumped_downstream_key, "unretained keys see the bump");
}
}
+8 -193
View File
@@ -1,103 +1,18 @@
use crate::Node;
use std::cell::{Cell, RefCell, RefMut};
use std::marker::PhantomData;
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct IntNode<const N: u32>;
impl<'i, const N: u32, I> Node<'i, I> for IntNode<N> {
type Output = u32;
#[inline(always)]
fn eval(&'i self, _input: I) -> Self::Output {
N
}
}
#[derive(Default, Debug, Clone, Copy)]
pub struct ValueNode<T>(pub T);
impl<'i, T: 'i, I> Node<'i, I> for ValueNode<T> {
type Output = &'i T;
#[inline(always)]
fn eval(&'i self, _input: I) -> Self::Output {
&self.0
}
}
impl<T> ValueNode<T> {
pub const fn new(value: T) -> ValueNode<T> {
ValueNode(value)
}
}
impl<T> From<T> for ValueNode<T> {
fn from(value: T) -> Self {
ValueNode::new(value)
}
}
#[derive(Default, Debug, Clone, Copy)]
pub struct AsRefNode<T: AsRef<U>, U>(pub T, PhantomData<U>);
impl<'i, T: 'i + AsRef<U>, U: 'i> Node<'i, ()> for AsRefNode<T, U> {
type Output = &'i U;
#[inline(always)]
fn eval(&'i self, _input: ()) -> Self::Output {
self.0.as_ref()
}
}
impl<T: AsRef<U>, U> AsRefNode<T, U> {
pub const fn new(value: T) -> AsRefNode<T, U> {
AsRefNode(value, PhantomData)
}
}
#[derive(Default, Debug, Clone)]
pub struct RefCellMutNode<T>(pub RefCell<T>);
impl<'i, T: 'i> Node<'i, ()> for RefCellMutNode<T> {
type Output = RefMut<'i, T>;
#[inline(always)]
fn eval(&'i self, _input: ()) -> Self::Output {
self.0.borrow_mut()
}
}
impl<T> RefCellMutNode<T> {
pub const fn new(value: T) -> RefCellMutNode<T> {
RefCellMutNode(RefCell::new(value))
}
}
#[derive(Default)]
pub struct OnceCellNode<T>(pub Cell<T>);
impl<'i, T: Default + 'i, I> Node<'i, I> for OnceCellNode<T> {
type Output = T;
#[inline(always)]
fn eval(&'i self, _input: I) -> Self::Output {
self.0.replace(T::default())
}
}
impl<T> OnceCellNode<T> {
pub const fn new(value: T) -> OnceCellNode<T> {
OnceCellNode(Cell::new(value))
}
}
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct ClonedNode<T: Clone>(pub T); pub struct ClonedNode<T: Clone>(pub T);
impl<'i, T: Clone + 'i, I> Node<'i, I> for ClonedNode<T> { impl<T: Clone, Input> crate::node::Node<Input> for ClonedNode<T> {
type Output = T; type Output = T;
#[inline(always)]
fn eval(&'i self, _input: I) -> Self::Output { fn eval(&self, _input: &Input) -> crate::gpoll::GPoll<T> {
self.0.clone() crate::gpoll::GPoll::Final(self.0.clone())
} }
} }
pub fn value_edge<T: Clone + crate::WasmNotSend + crate::WasmNotSync + 'static>(value: T) -> crate::registry::EdgeHandle {
crate::registry::EdgeHandle::new(std::sync::Arc::new(ClonedNode(value)) as std::sync::Arc<crate::registry::ErasedNode<T>>)
}
impl<T: Clone> ClonedNode<T> { impl<T: Clone> ClonedNode<T> {
pub const fn new(value: T) -> ClonedNode<T> { pub const fn new(value: T) -> ClonedNode<T> {
ClonedNode(value) ClonedNode(value)
@@ -109,103 +24,3 @@ impl<T: Clone> From<T> for ClonedNode<T> {
ClonedNode::new(value) ClonedNode::new(value)
} }
} }
#[derive(Clone, Copy)]
/// The DebugClonedNode logs every time it is evaluated.
/// This is useful for debugging.
pub struct DebugClonedNode<T: Clone>(pub T);
impl<'i, T: Clone + 'i> Node<'i, ()> for DebugClonedNode<T> {
type Output = T;
#[inline(always)]
fn eval(&'i self, _input: ()) -> Self::Output {
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
log::debug!("DebugClonedNode::eval");
self.0.clone()
}
}
impl<T: Clone> DebugClonedNode<T> {
pub const fn new(value: T) -> DebugClonedNode<T> {
DebugClonedNode(value)
}
}
#[derive(Clone, Copy)]
pub struct CopiedNode<T: Copy>(pub T);
impl<'i, T: Copy + 'i, I> Node<'i, I> for CopiedNode<T> {
type Output = T;
#[inline(always)]
fn eval(&'i self, _input: I) -> Self::Output {
self.0
}
}
impl<T: Copy> CopiedNode<T> {
pub const fn new(value: T) -> CopiedNode<T> {
CopiedNode(value)
}
}
#[derive(Default)]
pub struct DefaultNode<T>(PhantomData<T>);
impl<'i, T: Default + 'i, I> Node<'i, I> for DefaultNode<T> {
type Output = T;
fn eval(&'i self, _input: I) -> Self::Output {
T::default()
}
}
impl<T> DefaultNode<T> {
pub fn new() -> Self {
Self(PhantomData)
}
}
#[repr(C)]
/// Return the unit value
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct ForgetNode;
impl<'i, T: 'i> Node<'i, T> for ForgetNode {
type Output = ();
fn eval(&'i self, _input: T) -> Self::Output {}
}
impl ForgetNode {
pub const fn new() -> Self {
ForgetNode
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_int_node() {
let node = IntNode::<5>;
assert_eq!(node.eval(()), 5);
}
#[test]
fn test_value_node() {
let node = ValueNode::new(5);
assert_eq!(node.eval(()), &5);
let type_erased = &node as &dyn for<'a> Node<'a, (), Output = &'a i32>;
assert_eq!(type_erased.eval(()), &5);
}
#[test]
fn test_default_node() {
let node = DefaultNode::<u32>::new();
assert_eq!(node.eval(42), 0);
}
#[test]
#[allow(clippy::unit_cmp)]
fn test_unit_node() {
let node = ForgetNode::new();
assert_eq!(node.eval(()), ());
}
}
+18 -7
View File
@@ -9,17 +9,16 @@ use crate::texture_cache::TextureCache;
use anyhow::Result; use anyhow::Result;
use core_types::Color; use core_types::Color;
use core_types::color::SRGBA8; use core_types::color::SRGBA8;
use futures::lock::Mutex;
use glam::UVec2; use glam::UVec2;
use graphene_application_io::{ApplicationIo, EditorApi}; use graphene_application_io::{ApplicationIo, EditorApi};
use raster_types::Texture; use raster_types::Texture;
use std::sync::Arc; use std::sync::Arc;
use std::sync::Mutex;
use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene}; use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
use wgpu::{Origin3d, TextureAspect}; use wgpu::{Origin3d, TextureAspect};
pub use context::Context as WgpuContext; pub use context::Context as WgpuContext;
pub use context::ContextBuilder as WgpuContextBuilder; pub use context::ContextBuilder as WgpuContextBuilder;
pub use pipeline::AsyncPipeline as AsyncWgpuPipeline;
pub use pipeline::Pipeline as WgpuPipeline; pub use pipeline::Pipeline as WgpuPipeline;
pub use pipeline::PipelineCache as WgpuPipelineCache; pub use pipeline::PipelineCache as WgpuPipelineCache;
pub use rendering::RenderContext; pub use rendering::RenderContext;
@@ -61,6 +60,18 @@ impl std::fmt::Debug for WgpuExecutor {
} }
} }
/// Owned Arc handle carrying the executor as an ordinary wire value.
#[derive(Clone, Debug)]
pub struct WgpuExecutorHandle(pub std::sync::Arc<WgpuExecutor>);
impl std::ops::Deref for WgpuExecutorHandle {
type Target = WgpuExecutor;
fn deref(&self) -> &WgpuExecutor {
&self.0
}
}
impl<'a, T: ApplicationIo<Executor = WgpuExecutor>> From<&'a EditorApi<T>> for &'a WgpuExecutor { impl<'a, T: ApplicationIo<Executor = WgpuExecutor>> From<&'a EditorApi<T>> for &'a WgpuExecutor {
fn from(editor_api: &'a EditorApi<T>) -> Self { fn from(editor_api: &'a EditorApi<T>) -> Self {
editor_api.application_io.as_ref().unwrap().gpu_executor().unwrap() editor_api.application_io.as_ref().unwrap().gpu_executor().unwrap()
@@ -68,8 +79,8 @@ impl<'a, T: ApplicationIo<Executor = WgpuExecutor>> From<&'a EditorApi<T>> for &
} }
impl WgpuExecutor { impl WgpuExecutor {
pub async fn render_vello_scene(&self, scene: &Scene, size: UVec2, context: &RenderContext, background: Option<Color>) -> Result<Texture> { pub fn render_vello_scene(&self, scene: &Scene, size: UVec2, context: &RenderContext, background: Option<Color>) -> Result<Texture> {
let texture = self.request_texture(size).await; let texture = self.request_texture(size);
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
@@ -82,7 +93,7 @@ impl WgpuExecutor {
}; };
{ {
let mut renderer = self.inner.vello_renderer.lock().await; let mut renderer = self.inner.vello_renderer.lock().unwrap();
for (image_brush, texture) in context.resource_overrides.iter() { for (image_brush, texture) in context.resource_overrides.iter() {
let texture_view = wgpu::TexelCopyTextureInfoBase { let texture_view = wgpu::TexelCopyTextureInfoBase {
texture: (**texture).clone(), texture: (**texture).clone(),
@@ -109,8 +120,8 @@ impl WgpuExecutor {
pipeline.init::<P>(self); pipeline.init::<P>(self);
} }
pub async fn request_texture(&self, size: UVec2) -> Texture { pub fn request_texture(&self, size: UVec2) -> Texture {
self.inner.texture_cache.lock().await.request_texture(&self.context().device, size) self.inner.texture_cache.lock().unwrap().request_texture(&self.context().device, size)
} }
} }
@@ -1,42 +1,16 @@
use dyn_any::DynAny; use dyn_any::DynAny;
use std::any::Any; use std::any::Any;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, OnceLock}; use std::sync::{Arc, OnceLock};
use crate::WgpuExecutor; use crate::WgpuExecutor;
pub type PipelineFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
pub trait Pipeline: Any + Send + Sync + Sized { pub trait Pipeline: Any + Send + Sync + Sized {
type Args<'a>; type Args<'a>;
type Out: Send; type Out: Send;
fn create(executor: &WgpuExecutor) -> Self; fn create(executor: &WgpuExecutor) -> Self;
fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> PipelineFuture<'a, Self::Out>; fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> Self::Out;
}
pub trait AsyncPipeline: Any + Send + Sync + Sized {
type Args<'a>;
type Out: Send;
fn create(executor: &WgpuExecutor) -> Self;
fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> impl Future<Output = Self::Out> + Send + 'a;
}
impl<P: AsyncPipeline> Pipeline for P {
type Args<'a> = <P as AsyncPipeline>::Args<'a>;
type Out = <P as AsyncPipeline>::Out;
fn create(executor: &WgpuExecutor) -> Self {
<P as AsyncPipeline>::create(executor)
}
fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> PipelineFuture<'a, Self::Out> {
Box::pin(<P as AsyncPipeline>::run(self, executor, args))
}
} }
#[derive(Default, Clone, DynAny)] #[derive(Default, Clone, DynAny)]
@@ -51,13 +25,13 @@ impl PipelineCache {
self.pipeline.get_or_init(|| Box::new(P::create(executor))); self.pipeline.get_or_init(|| Box::new(P::create(executor)));
} }
pub async fn run<P: Pipeline>(&self, args: &P::Args<'_>) -> P::Out { pub fn run<P: Pipeline>(&self, args: &P::Args<'_>) -> P::Out {
let executor = self.executor.get().expect("PipelineCache not initialized"); let executor = self.executor.get().expect("PipelineCache not initialized");
let entry = self.pipeline.get().expect("PipelineCache not initialized"); let entry = self.pipeline.get().expect("PipelineCache not initialized");
let pipeline = (&**entry) let pipeline = (**entry)
.downcast_ref::<P>() .downcast_ref::<P>()
.unwrap_or_else(|| panic!("PipelineCache type mismatch: run::<{}>() but init used a different pipeline type", std::any::type_name::<P>(),)); .unwrap_or_else(|| panic!("PipelineCache type mismatch: run::<{}>() but init used a different pipeline type", std::any::type_name::<P>(),));
pipeline.run(executor, args).await pipeline.run(executor, args)
} }
} }
@@ -2,10 +2,10 @@ use crate::WgpuContext;
use crate::shader_runtime::{FULLSCREEN_VERTEX_SHADER_NAME, ShaderRuntime}; use crate::shader_runtime::{FULLSCREEN_VERTEX_SHADER_NAME, ShaderRuntime};
use core_types::list::{Item, List}; use core_types::list::{Item, List};
use core_types::shaders::buffer_struct::BufferStruct; use core_types::shaders::buffer_struct::BufferStruct;
use futures::lock::Mutex;
use raster_types::{GPU, Raster}; use raster_types::{GPU, Raster};
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Mutex;
use wgpu::util::{BufferInitDescriptor, DeviceExt}; use wgpu::util::{BufferInitDescriptor, DeviceExt};
use wgpu::{ use wgpu::{
BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, Buffer, BufferBinding, BufferBindingType, BufferUsages, ColorTargetState, Face, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, Buffer, BufferBinding, BufferBindingType, BufferUsages, ColorTargetState, Face,
@@ -33,8 +33,8 @@ impl PerPixelAdjustShaderRuntime {
} }
impl ShaderRuntime { impl ShaderRuntime {
pub async fn run_per_pixel_adjust<T: BufferStruct>(&self, shaders: &Shaders<'_>, textures: List<Raster<GPU>>, args: Option<&T>) -> List<Raster<GPU>> { pub fn run_per_pixel_adjust<T: BufferStruct>(&self, shaders: &Shaders<'_>, textures: List<Raster<GPU>>, args: Option<&T>) -> List<Raster<GPU>> {
let mut cache = self.per_pixel_adjust.pipeline_cache.lock().await; let mut cache = self.per_pixel_adjust.pipeline_cache.lock().unwrap();
let pipeline = cache let pipeline = cache
.entry(shaders.fragment_shader_name.to_owned()) .entry(shaders.fragment_shader_name.to_owned())
.or_insert_with(|| PerPixelAdjustGraphicsPipeline::new(&self.context, shaders)); .or_insert_with(|| PerPixelAdjustGraphicsPipeline::new(&self.context, shaders));
@@ -1,9 +1,10 @@
use crate::WgpuExecutor; use crate::WgpuExecutorHandle;
use core_types::Color; use core_types::Color;
use core_types::Ctx; use core_types::Ctx;
use core_types::color::SRGBA8; use core_types::color::SRGBA8;
use core_types::list::{Item, List}; use core_types::list::{Item, List};
use core_types::ops::Convert; use core_types::ops::{Convert, ConvertAsync};
use core_types::runtime::SourceFuture;
use core_types::transform::Footprint; use core_types::transform::Footprint;
use raster_types::Image; use raster_types::Image;
use raster_types::{CPU, GPU, Raster}; use raster_types::{CPU, GPU, Raster};
@@ -38,6 +39,52 @@ fn upload_to_texture(device: &wgpu::Device, queue: &wgpu::Queue, image: &Raster<
) )
} }
/// Passthrough conversion for GPU `List`s - no conversion needed
impl Convert<List<Raster<GPU>>, WgpuExecutorHandle> for List<Raster<GPU>> {
fn convert(self, _: Footprint, _converter: WgpuExecutorHandle) -> List<Raster<GPU>> {
self
}
}
/// Converts a `List<Raster<CPU>>` to `List<Raster<GPU>>` by uploading each image to a texture
impl Convert<List<Raster<GPU>>, WgpuExecutorHandle> for List<Raster<CPU>> {
fn convert(self, _: Footprint, executor: WgpuExecutorHandle) -> List<Raster<GPU>> {
let device = &executor.context().device;
let queue = executor.context().queue.lock();
let list = self
.into_iter()
.map(|row| {
let (image, attributes) = row.into_parts();
let texture = upload_to_texture(device, &queue, &image);
Item::from_parts(Raster::new_gpu(texture), attributes)
})
.collect();
queue.submit([]);
list
}
}
/// Converts single CPU raster to GPU by uploading to texture
impl Convert<Raster<GPU>, WgpuExecutorHandle> for Raster<CPU> {
fn convert(self, _: Footprint, executor: WgpuExecutorHandle) -> Raster<GPU> {
let device = &executor.context().device;
let queue = executor.context().queue.lock();
let texture = upload_to_texture(device, &queue, &self);
queue.submit([]);
Raster::new_gpu(texture)
}
}
/// Passthrough conversion for CPU `List`s - no conversion needed
impl Convert<List<Raster<CPU>>, WgpuExecutorHandle> for List<Raster<CPU>> {
fn convert(self, _: Footprint, _converter: WgpuExecutorHandle) -> List<Raster<CPU>> {
self
}
}
/// Converts a Raster<GPU> texture to Raster<CPU> by downloading the underlying texture data. /// Converts a Raster<GPU> texture to Raster<CPU> by downloading the underlying texture data.
/// ///
/// Assumptions: /// Assumptions:
@@ -142,57 +189,11 @@ impl RasterGpuToRasterCpuConverter {
} }
} }
/// Passthrough conversion for GPU `List`s - no conversion needed
impl<'i> Convert<List<Raster<GPU>>, &'i WgpuExecutor> for List<Raster<GPU>> {
async fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> List<Raster<GPU>> {
self
}
}
/// Converts a `List<Raster<CPU>>` to `List<Raster<GPU>>` by uploading each image to a texture
impl<'i> Convert<List<Raster<GPU>>, &'i WgpuExecutor> for List<Raster<CPU>> {
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List<Raster<GPU>> {
let device = &executor.context().device;
let queue = executor.context().queue.lock();
let list = self
.into_iter()
.map(|row| {
let (image, attributes) = row.into_parts();
let texture = upload_to_texture(device, &queue, &image);
Item::from_parts(Raster::new_gpu(texture), attributes)
})
.collect();
queue.submit([]);
list
}
}
/// Converts single CPU raster to GPU by uploading to texture
impl<'i> Convert<Raster<GPU>, &'i WgpuExecutor> for Raster<CPU> {
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster<GPU> {
let device = &executor.context().device;
let queue = executor.context().queue.lock();
let texture = upload_to_texture(device, &queue, &self);
queue.submit([]);
Raster::new_gpu(texture)
}
}
/// Passthrough conversion for CPU `List`s - no conversion needed
impl<'i> Convert<List<Raster<CPU>>, &'i WgpuExecutor> for List<Raster<CPU>> {
async fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> List<Raster<CPU>> {
self
}
}
/// Converts a `List<Raster<GPU>>` to `List<Raster<CPU>>` by downloading texture data in one go then asynchronously maps all buffers and processes the results. /// Converts a `List<Raster<GPU>>` to `List<Raster<CPU>>` by downloading texture data in one go then asynchronously maps all buffers and processes the results.
impl<'i> Convert<List<Raster<CPU>>, &'i WgpuExecutor> for List<Raster<GPU>> { impl ConvertAsync<List<Raster<CPU>>, WgpuExecutorHandle> for List<Raster<GPU>> {
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List<Raster<CPU>> { fn convert(self, _: Footprint, executor: WgpuExecutorHandle) -> SourceFuture<List<Raster<CPU>>> {
let device = &executor.context().device; let device = executor.context().device.clone();
let queue = &executor.context().queue; let queue = executor.context().queue.lock();
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("batch_texture_download_encoder"), label: Some("batch_texture_download_encoder"),
@@ -203,48 +204,50 @@ impl<'i> Convert<List<Raster<CPU>>, &'i WgpuExecutor> for List<Raster<GPU>> {
for row in self { for row in self {
let (element, attributes) = row.into_parts(); let (element, attributes) = row.into_parts();
converters.push(RasterGpuToRasterCpuConverter::new(device, &mut encoder, element)); converters.push(RasterGpuToRasterCpuConverter::new(&device, &mut encoder, element));
rows_meta.push(Item::from_parts((), attributes)); rows_meta.push(Item::from_parts((), attributes));
} }
queue.submit([encoder.finish()]); queue.submit([encoder.finish()]);
let mut map_futures = Vec::new(); Box::pin(async move {
for converter in converters { let mut map_futures = Vec::new();
map_futures.push(converter.convert(device)); for converter in converters {
} map_futures.push(converter.convert(&device));
}
let map_results = futures::future::try_join_all(map_futures) let map_results = futures::future::try_join_all(map_futures)
.await .await
.map_err(|_| "Failed to receive map result") .map_err(|_| "Failed to receive map result")
.expect("Buffer mapping communication failed"); .expect("Buffer mapping communication failed");
map_results map_results
.into_iter() .into_iter()
.zip(rows_meta) .zip(rows_meta)
.map(|(element, row)| { .map(|(element, row)| {
let (_, attributes) = row.into_parts(); let (_, attributes) = row.into_parts();
Item::from_parts(element, attributes) Item::from_parts(element, attributes)
}) })
.collect() .collect()
})
} }
} }
/// Converts single GPU raster to CPU by downloading texture data /// Converts single GPU raster to CPU by downloading texture data
impl<'i> Convert<Raster<CPU>, &'i WgpuExecutor> for Raster<GPU> { impl ConvertAsync<Raster<CPU>, WgpuExecutorHandle> for Raster<GPU> {
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster<CPU> { fn convert(self, _: Footprint, executor: WgpuExecutorHandle) -> SourceFuture<Raster<CPU>> {
let device = &executor.context().device; let device = executor.context().device.clone();
let queue = &executor.context().queue; let queue = executor.context().queue.lock();
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("single_texture_download_encoder"), label: Some("single_texture_download_encoder"),
}); });
let converter = RasterGpuToRasterCpuConverter::new(device, &mut encoder, self); let converter = RasterGpuToRasterCpuConverter::new(&device, &mut encoder, self);
queue.submit([encoder.finish()]); queue.submit([encoder.finish()]);
converter.convert(device).await.expect("Failed to download texture data") Box::pin(async move { converter.convert(&device).await.expect("Failed to download texture data") })
} }
} }
@@ -252,10 +255,10 @@ impl<'i> Convert<Raster<CPU>, &'i WgpuExecutor> for Raster<GPU> {
/// ///
/// Accepts either individual raster data or a `List` of raster elements and converts it to the GPU format using the WgpuExecutor's device and queue. /// Accepts either individual raster data or a `List` of raster elements and converts it to the GPU format using the WgpuExecutor's device and queue.
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
pub async fn upload_texture<'a: 'n, T: Convert<List<Raster<GPU>>, &'a WgpuExecutor>>( pub fn upload_texture<T: Convert<List<Raster<GPU>>, WgpuExecutorHandle>>(
_: impl Ctx, _: impl Ctx,
#[implementations(List<Raster<CPU>>, List<Raster<GPU>>)] input: T, #[implementations(List<Raster<CPU>>, List<Raster<GPU>>)] input: T,
executor: &'a WgpuExecutor, executor: WgpuExecutorHandle,
) -> List<Raster<GPU>> { ) -> List<Raster<GPU>> {
input.convert(Footprint::DEFAULT, executor).await input.convert(Footprint::DEFAULT, executor)
} }
File diff suppressed because it is too large Load Diff
+139 -3
View File
@@ -56,6 +56,14 @@ pub(crate) struct NodeFnAttributes {
pub(crate) memoize: bool, pub(crate) memoize: bool,
/// Whether this node provides a scope /// Whether this node provides a scope
pub(crate) inject_scope: bool, pub(crate) inject_scope: bool,
/// Function producing a stand-in value while an async source node's real value is in flight
pub(crate) placeholder: Option<Path>,
/// Function overriding the generated `extent` method
pub(crate) extent: Option<Path>,
/// Function overriding the generated `eval_batch` method
pub(crate) batch: Option<Path>,
/// Whether partial upstream values are mapped to `Pending` instead of flowing into this node
pub(crate) no_partial: bool,
} }
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
@@ -63,7 +71,8 @@ pub enum ParsedValueSource {
#[default] #[default]
None, None,
Default(TokenStream2), Default(TokenStream2),
Scope(Expr), Scope(Box<Expr>),
SourceId,
} }
// #[widget(ParsedWidgetOverride::Hidden)] // #[widget(ParsedWidgetOverride::Hidden)]
@@ -311,6 +320,10 @@ impl Parse for NodeFnAttributes {
let mut serialize = None; let mut serialize = None;
let mut memoize = false; let mut memoize = false;
let mut inject_scope = false; let mut inject_scope = false;
let mut placeholder = None;
let mut extent = None;
let mut batch = None;
let mut no_partial = false;
let content = input; let content = input;
// let content; // let content;
@@ -453,13 +466,63 @@ impl Parse for NodeFnAttributes {
} }
inject_scope = true; inject_scope = true;
} }
// Function producing a stand-in value for an async source node while the spawned future is in flight.
// The node reports `Partial` with the stand-in until the real value lands; without a placeholder it reports `Pending`.
//
// Example usage:
// #[node_macro::node(..., placeholder(empty_image), ...)]
"placeholder" => {
let meta = meta.require_list()?;
if placeholder.is_some() {
return Err(Error::new_spanned(meta, "Multiple 'placeholder' attributes are not allowed"));
}
let parsed_path: Path = meta
.parse_args()
.map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'placeholder', e.g., placeholder(empty_image)"))?;
placeholder = Some(parsed_path);
}
// Function overriding the generated `extent` method, replacing the default meet over the node's inputs.
//
// Example usage:
// #[node_macro::node(..., extent(my_extent), ...)]
"extent" => {
let meta = meta.require_list()?;
if extent.is_some() {
return Err(Error::new_spanned(meta, "Multiple 'extent' attributes are not allowed"));
}
let parsed_path: Path = meta.parse_args().map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'extent', e.g., extent(my_extent)"))?;
extent = Some(parsed_path);
}
// Function overriding the generated `eval_batch` method, replacing the trait's per-lane spec loop.
//
// Example usage:
// #[node_macro::node(..., batch(my_batch), ...)]
"batch" => {
let meta = meta.require_list()?;
if batch.is_some() {
return Err(Error::new_spanned(meta, "Multiple 'batch' attributes are not allowed"));
}
let parsed_path: Path = meta.parse_args().map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'batch', e.g., batch(my_batch)"))?;
batch = Some(parsed_path);
}
// Instructs the generated eval to report `Pending` instead of passing partial upstream values into this node.
//
// Example usage:
// #[node_macro::node(..., no_partial, ...)]
"no_partial" => {
let path = meta.require_path_only()?;
if no_partial {
return Err(Error::new_spanned(path, "Multiple 'no_partial' attributes are not allowed"));
}
no_partial = true;
}
_ => { _ => {
return Err(Error::new_spanned( return Err(Error::new_spanned(
meta, meta,
indoc!( indoc!(
r#" r#"
Unsupported attribute in `node`. Unsupported attribute in `node`.
Supported attributes are 'category', 'name', 'path', 'skip_impl', 'properties', 'cfg', 'shader_node', 'serialize', 'memoize', and 'inject_scope'. Supported attributes are 'category', 'name', 'path', 'skip_impl', 'properties', 'cfg', 'shader_node', 'serialize', 'memoize', 'inject_scope', 'placeholder', 'extent', 'batch', and 'no_partial'.
Example usage: Example usage:
#[node_macro::node(..., name("Test Node"), ...)] #[node_macro::node(..., name("Test Node"), ...)]
"# "#
@@ -493,6 +556,10 @@ impl Parse for NodeFnAttributes {
serialize, serialize,
memoize, memoize,
inject_scope, inject_scope,
placeholder,
extent,
batch,
no_partial,
}) })
} }
} }
@@ -723,7 +790,7 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
let value_source = match (default_value, scope) { let value_source = match (default_value, scope) {
(Some(_), Some(_)) => return Err(Error::new_spanned(&pat_ident, "Cannot have both `default` and `scope` attributes")), (Some(_), Some(_)) => return Err(Error::new_spanned(&pat_ident, "Cannot have both `default` and `scope` attributes")),
(Some(default_value), _) => ParsedValueSource::Default(default_value), (Some(default_value), _) => ParsedValueSource::Default(default_value),
(_, Some(scope)) => ParsedValueSource::Scope(scope), (_, Some(scope)) => ParsedValueSource::Scope(Box::new(scope)),
_ => ParsedValueSource::None, _ => ParsedValueSource::None,
}; };
@@ -931,6 +998,10 @@ pub fn new_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Result<TokenS
let crate_ident = CrateIdent::default(); let crate_ident = CrateIdent::default();
let mut parsed_node = parse_node_fn(attr, item.clone()).map_err(|e| Error::new(e.span(), format!("Failed to parse node function:\n{e}")))?; let mut parsed_node = parse_node_fn(attr, item.clone()).map_err(|e| Error::new(e.span(), format!("Failed to parse node function:\n{e}")))?;
parsed_node.replace_impl_trait_in_input(); parsed_node.replace_impl_trait_in_input();
if parsed_node.is_async || crate::codegen::is_source_kernel(&parsed_node.output_type) {
let core_types = crate_ident.gcore()?.clone();
parsed_node.inject_async_source_fields(&core_types);
}
crate::validation::validate_node_fn(&parsed_node).map_err(|e| Error::new(e.span(), format!("Validation error:\n{e}")))?; crate::validation::validate_node_fn(&parsed_node).map_err(|e| Error::new(e.span(), format!("Validation error:\n{e}")))?;
generate_node_code(&crate_ident, &parsed_node).map_err(|e| Error::new(e.span(), format!("Failed to generate node code:\n{e}"))) generate_node_code(&crate_ident, &parsed_node).map_err(|e| Error::new(e.span(), format!("Failed to generate node code:\n{e}")))
} }
@@ -958,6 +1029,43 @@ impl ParsedNodeFn {
self.input.pat_ident.ident = Ident::new("__ctx", self.input.pat_ident.ident.span()); self.input.pat_ident.ident = Ident::new("__ctx", self.input.pat_ident.ident.span());
} }
} }
pub fn inject_async_source_fields(&mut self, core_types: &TokenStream2) {
let hidden_field = |name: &str, ty: Type, value_source: ParsedValueSource| ParsedField {
pat_ident: PatIdent {
attrs: Vec::new(),
by_ref: None,
mutability: None,
ident: Ident::new(name, proc_macro2::Span::call_site()),
subpat: None,
},
name: None,
description: String::new(),
widget_override: ParsedWidgetOverride::Hidden,
ty: ParsedFieldType::Regular(RegularParsedField {
ty,
exposed: false,
value_source,
number_soft_min: None,
number_soft_max: None,
number_hard_min: None,
number_hard_max: None,
number_mode_range: false,
implementations: Default::default(),
gpu_image: false,
}),
number_display_decimal_places: None,
number_step: None,
unit: None,
is_data_field: false,
};
self.fields.push(hidden_field(
"_runtime",
parse_quote!(#core_types::runtime::RuntimeHandle),
ParsedValueSource::Scope(Box::new(parse_quote!("graphene_std::runtime::RuntimeNode"))),
));
self.fields.push(hidden_field("_source", parse_quote!(#core_types::SourceId), ParsedValueSource::SourceId));
}
} }
#[cfg(test)] #[cfg(test)]
@@ -1082,6 +1190,10 @@ mod tests {
serialize: None, serialize: None,
memoize: false, memoize: false,
inject_scope: false, inject_scope: false,
placeholder: None,
extent: None,
batch: None,
no_partial: false,
}, },
fn_name: Ident::new("add", Span::call_site()), fn_name: Ident::new("add", Span::call_site()),
struct_name: Ident::new("Add", Span::call_site()), struct_name: Ident::new("Add", Span::call_site()),
@@ -1152,6 +1264,10 @@ mod tests {
serialize: None, serialize: None,
memoize: false, memoize: false,
inject_scope: false, inject_scope: false,
placeholder: None,
extent: None,
batch: None,
no_partial: false,
}, },
fn_name: Ident::new("transform", Span::call_site()), fn_name: Ident::new("transform", Span::call_site()),
struct_name: Ident::new("Transform", Span::call_site()), struct_name: Ident::new("Transform", Span::call_site()),
@@ -1236,6 +1352,10 @@ mod tests {
serialize: None, serialize: None,
memoize: false, memoize: false,
inject_scope: false, inject_scope: false,
placeholder: None,
extent: None,
batch: None,
no_partial: false,
}, },
fn_name: Ident::new("circle", Span::call_site()), fn_name: Ident::new("circle", Span::call_site()),
struct_name: Ident::new("Circle", Span::call_site()), struct_name: Ident::new("Circle", Span::call_site()),
@@ -1302,6 +1422,10 @@ mod tests {
serialize: None, serialize: None,
memoize: false, memoize: false,
inject_scope: false, inject_scope: false,
placeholder: None,
extent: None,
batch: None,
no_partial: false,
}, },
fn_name: Ident::new("levels", Span::call_site()), fn_name: Ident::new("levels", Span::call_site()),
struct_name: Ident::new("Levels", Span::call_site()), struct_name: Ident::new("Levels", Span::call_site()),
@@ -1380,6 +1504,10 @@ mod tests {
serialize: None, serialize: None,
memoize: false, memoize: false,
inject_scope: false, inject_scope: false,
placeholder: None,
extent: None,
batch: None,
no_partial: false,
}, },
fn_name: Ident::new("add", Span::call_site()), fn_name: Ident::new("add", Span::call_site()),
struct_name: Ident::new("Add", Span::call_site()), struct_name: Ident::new("Add", Span::call_site()),
@@ -1461,6 +1589,10 @@ mod tests {
serialize: None, serialize: None,
memoize: false, memoize: false,
inject_scope: false, inject_scope: false,
placeholder: None,
extent: None,
batch: None,
no_partial: false,
}, },
fn_name: Ident::new("load_image", Span::call_site()), fn_name: Ident::new("load_image", Span::call_site()),
struct_name: Ident::new("LoadImage", Span::call_site()), struct_name: Ident::new("LoadImage", Span::call_site()),
@@ -1527,6 +1659,10 @@ mod tests {
serialize: None, serialize: None,
memoize: false, memoize: false,
inject_scope: false, inject_scope: false,
placeholder: None,
extent: None,
batch: None,
no_partial: false,
}, },
fn_name: Ident::new("custom_node", Span::call_site()), fn_name: Ident::new("custom_node", Span::call_site()),
struct_name: Ident::new("CustomNode", Span::call_site()), struct_name: Ident::new("CustomNode", Span::call_site()),
@@ -146,7 +146,7 @@ impl PerPixelAdjustCodegen<'_> {
ParamType::Uniform => quote!(uniform.#ident), ParamType::Uniform => quote!(uniform.#ident),
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let context = quote!(()); let context = quote!(&());
let entry_point_mod = &self.entry_point_mod; let entry_point_mod = &self.entry_point_mod;
let entry_point_name = &self.entry_point_name_ident; let entry_point_name = &self.entry_point_name_ident;
@@ -231,9 +231,9 @@ impl PerPixelAdjustCodegen<'_> {
description: "".to_string(), description: "".to_string(),
widget_override: Default::default(), widget_override: Default::default(),
ty: ParsedFieldType::Regular(RegularParsedField { ty: ParsedFieldType::Regular(RegularParsedField {
ty: parse_quote!(&'a WgpuExecutor), ty: parse_quote!(std::sync::Arc<WgpuExecutor>),
exposed: true, exposed: true,
value_source: ParsedValueSource::Scope(parse_quote!("graphene_std::platform_application_io::WgpuExecutorNode")), value_source: ParsedValueSource::Scope(Box::new(parse_quote!("graphene_std::platform_application_io::WgpuExecutorArcNode"))),
number_soft_min: None, number_soft_min: None,
number_soft_max: None, number_soft_max: None,
number_hard_min: None, number_hard_min: None,
@@ -287,7 +287,7 @@ impl PerPixelAdjustCodegen<'_> {
wgsl_shader: crate::WGSL_SHADER, wgsl_shader: crate::WGSL_SHADER,
fragment_shader_name: super::#entry_point_name, fragment_shader_name: super::#entry_point_name,
has_uniform: #has_uniform, has_uniform: #has_uniform,
}, #gpu_image, #uniform_buffer).await }, #gpu_image, #uniform_buffer)
} }
}; };
@@ -305,7 +305,7 @@ impl PerPixelAdjustCodegen<'_> {
fn_name: self.shader_node_mod.clone(), fn_name: self.shader_node_mod.clone(),
struct_name: format_ident!("{}", self.shader_node_mod.to_string().to_case(Case::Pascal)), struct_name: format_ident!("{}", self.shader_node_mod.to_string().to_case(Case::Pascal)),
mod_name: self.shader_node_mod.clone(), mod_name: self.shader_node_mod.clone(),
fn_generics: vec![parse_quote!('a: 'n)], fn_generics: Vec::new(),
where_clause: None, where_clause: None,
input: Input { input: Input {
pat_ident: self.parsed.input.pat_ident.clone(), pat_ident: self.parsed.input.pat_ident.clone(),
@@ -314,7 +314,7 @@ impl PerPixelAdjustCodegen<'_> {
context_features: self.parsed.input.context_features.clone(), context_features: self.parsed.input.context_features.clone(),
}, },
output_type: raster_gpu, output_type: raster_gpu,
is_async: true, is_async: false,
fields, fields,
body, body,
description: self.parsed.description.clone(), description: self.parsed.description.clone(),
+34
View File
@@ -11,6 +11,7 @@ pub fn validate_node_fn(parsed: &ParsedNodeFn) -> syn::Result<()> {
validate_primary_input_expose, validate_primary_input_expose,
validate_min_max, validate_min_max,
validate_range_slider_bounds, validate_range_slider_bounds,
validate_async_source,
]; ];
for validator in validators { for validator in validators {
@@ -20,6 +21,39 @@ pub fn validate_node_fn(parsed: &ParsedNodeFn) -> syn::Result<()> {
Ok(()) Ok(())
} }
fn validate_async_source(parsed: &ParsedNodeFn) {
let snapshot_ctx = matches!(&parsed.input.ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "CtxSnapshot"));
let future_kernel = crate::codegen::is_source_kernel(&parsed.output_type);
if parsed.is_async && future_kernel {
emit_error!(
parsed.output_type.span(),
"an `async fn` kernel already is the async part; returning `SourceFuture` is the sync-prologue form, so drop the `async` keyword or return the value directly"
);
return;
}
if !parsed.is_async {
if snapshot_ctx {
emit_error!(
parsed.input.pat_ident.span(),
"`CtxSnapshot` is the async source context; synchronous nodes take `impl Ctx` and read through extract bounds"
);
}
if !future_kernel {
return;
}
}
if parsed.is_async {
for field in &parsed.fields {
if matches!(field.ty, ParsedFieldType::Node(_)) {
emit_error!(
field.pat_ident.span(),
"`async fn` source nodes cannot take `impl Node` inputs: the spawned future outlives any borrow of the graph, so it cannot evaluate other nodes; use the sync-prologue form (return `SourceFuture`) to evaluate lazy inputs before spawning"
);
}
}
}
}
fn validate_min_max(parsed: &ParsedNodeFn) { fn validate_min_max(parsed: &ParsedNodeFn) {
for field in &parsed.fields { for field in &parsed.fields {
if let ParsedField { if let ParsedField {
+27 -40
View File
@@ -1,17 +1,14 @@
use crate::brush_cache::BrushCache; use crate::brush_cache::BrushCache;
use crate::brush_stroke::{BrushStroke, BrushStyle}; use crate::brush_stroke::{BrushStroke, BrushStyle};
use core_types::Ctx;
use core_types::blending::BlendMode; use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::color::{Alpha, Color, Pixel, Sample}; use core_types::color::{Alpha, Color, Pixel, Sample};
use core_types::generic::FnNode;
use core_types::list::{Item, List}; use core_types::list::{Item, List};
use core_types::math::bbox::{AxisAlignedBbox, Bbox}; use core_types::math::bbox::{AxisAlignedBbox, Bbox};
use core_types::registry::FutureWrapperNode;
use core_types::transform::Transform; use core_types::transform::Transform;
use core_types::uuid::NodeId; use core_types::uuid::NodeId;
use core_types::value::ClonedNode;
use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM}; use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM};
use core_types::{Ctx, Node};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use raster_nodes::blending_nodes::blend_colors; use raster_nodes::blending_nodes::blend_colors;
use raster_nodes::std_nodes::{empty_image, extend_image_to_bounds}; use raster_nodes::std_nodes::{empty_image, extend_image_to_bounds};
@@ -63,7 +60,7 @@ impl<P: Pixel + Alpha> Sample for BrushStampGenerator<P> {
/// The feather exponent is calculated from hardness to determine edge softness. /// The feather exponent is calculated from hardness to determine edge softness.
/// Used internally to create the brush texture before stamping it repeatedly along a stroke path. /// Used internally to create the brush texture before stamping it repeatedly along a stroke path.
#[node_macro::node(category(""), skip_impl)] #[node_macro::node(category(""), skip_impl)]
fn brush_stamp_generator(#[unit(" px")] diameter: f64, color: Color, hardness: f64, flow: f64) -> BrushStampGenerator<Color> { fn brush_stamp_generator(_: impl Ctx, #[unit(" px")] diameter: f64, color: Color, hardness: f64, flow: f64) -> BrushStampGenerator<Color> {
// Diameter // Diameter
let radius = diameter / 2.; let radius = diameter / 2.;
@@ -83,9 +80,9 @@ fn brush_stamp_generator(#[unit(" px")] diameter: f64, color: Color, hardness: f
/// Used to efficiently paint brush strokes. Applies the same texture repeatedly at different positions with proper blending and boundary handling. /// Used to efficiently paint brush strokes. Applies the same texture repeatedly at different positions with proper blending and boundary handling.
#[node_macro::node(category(""), skip_impl)] #[node_macro::node(category(""), skip_impl)]
fn blit<BlendFn>(mut target: List<Raster<CPU>>, texture: Raster<CPU>, positions: Vec<DVec2>, blend_mode: BlendFn) -> List<Raster<CPU>> fn blit<BlendFn>(_: impl Ctx, mut target: List<Raster<CPU>>, texture: Raster<CPU>, positions: Vec<DVec2>, blend_mode: BlendFn) -> List<Raster<CPU>>
where where
BlendFn: for<'any_input> Node<'any_input, (Color, Color), Output = Color>, BlendFn: Fn(Color, Color) -> Color,
{ {
if positions.is_empty() { if positions.is_empty() {
return target; return target;
@@ -125,7 +122,7 @@ where
for x in blit_area_offset.x..blit_area_offset.x + blit_area_dimensions.x { for x in blit_area_offset.x..blit_area_offset.x + blit_area_dimensions.x {
let src_pixel = texture.data[texture_index(x, y)]; let src_pixel = texture.data[texture_index(x, y)];
let dst_pixel = &mut element.data_mut().data[target_index(x + clamp_start.x, y + clamp_start.y)]; let dst_pixel = &mut element.data_mut().data[target_index(x + clamp_start.x, y + clamp_start.y)];
*dst_pixel = blend_mode.eval((src_pixel, *dst_pixel)); *dst_pixel = blend_mode(src_pixel, *dst_pixel);
} }
} }
} }
@@ -134,10 +131,10 @@ where
target target
} }
pub async fn create_brush_texture(brush_style: &BrushStyle) -> Raster<CPU> { pub fn create_brush_texture(brush_style: &BrushStyle) -> Raster<CPU> {
let stamp = brush_stamp_generator(brush_style.diameter, brush_style.color, brush_style.hardness, brush_style.flow); let stamp = brush_stamp_generator(&(), brush_style.diameter, brush_style.color, brush_style.hardness, brush_style.flow);
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(brush_style.diameter), 0., -DVec2::splat(brush_style.diameter / 2.)); let transform = DAffine2::from_scale_angle_translation(DVec2::splat(brush_style.diameter), 0., -DVec2::splat(brush_style.diameter / 2.));
let blank_texture = empty_image((), transform, List::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap_or_default(); let blank_texture = empty_image(&(), transform, List::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap_or_default();
let image = blend_stamp_closure(stamp, blank_texture, |a, b| blend_colors(a, b, BlendMode::Normal, 1.)); let image = blend_stamp_closure(stamp, blank_texture, |a, b| blend_colors(a, b, BlendMode::Normal, 1.));
image.into_element() image.into_element()
@@ -188,7 +185,7 @@ pub fn blend_with_mode(background: Item<Raster<CPU>>, foreground: Item<Raster<CP
/// Generates the brush strokes painted with the Brush tool as a raster image. /// Generates the brush strokes painted with the Brush tool as a raster image.
/// If an input image is supplied, strokes are drawn on top of it, expanding bounds as needed. /// If an input image is supplied, strokes are drawn on top of it, expanding bounds as needed.
#[node_macro::node(category("Raster"))] #[node_macro::node(category("Raster"))]
async fn brush( fn brush(
_: impl Ctx, _: impl Ctx,
/// Optional raster content that may be drawn onto. /// Optional raster content that may be drawn onto.
mut background: List<Raster<CPU>>, mut background: List<Raster<CPU>>,
@@ -224,7 +221,7 @@ async fn brush(
let mut brush_plan = cache.compute_brush_plan(list_item, &draw_strokes); let mut brush_plan = cache.compute_brush_plan(list_item, &draw_strokes);
// TODO: Find a way to handle more than one item // TODO: Find a way to handle more than one item
let Some(mut actual_image) = extend_image_to_bounds((), List::new_from_item(brush_plan.background), background_bounds).into_iter().next() else { let Some(mut actual_image) = extend_image_to_bounds(&(), List::new_from_item(brush_plan.background), background_bounds).into_iter().next() else {
return List::new(); return List::new();
}; };
@@ -234,7 +231,7 @@ async fn brush(
// TODO: apply rotation from layer to stamp for non-rotationally-symmetric brushes. // TODO: apply rotation from layer to stamp for non-rotationally-symmetric brushes.
let mut brush_texture = cache.get_cached_brush(&stroke.style); let mut brush_texture = cache.get_cached_brush(&stroke.style);
if brush_texture.is_none() { if brush_texture.is_none() {
let tex = create_brush_texture(&stroke.style).await; let tex = create_brush_texture(&stroke.style);
cache.store_brush(stroke.style.clone(), tex.clone()); cache.store_brush(stroke.style.clone(), tex.clone());
brush_texture = Some(tex); brush_texture = Some(tex);
} }
@@ -255,21 +252,14 @@ async fn brush(
let stroke_origin_in_layer = bbox.start - snap_offset - DVec2::splat(stroke.style.diameter / 2.); let stroke_origin_in_layer = bbox.start - snap_offset - DVec2::splat(stroke.style.diameter / 2.);
let stroke_to_layer = DAffine2::from_translation(stroke_origin_in_layer) * DAffine2::from_scale(stroke_size); let stroke_to_layer = DAffine2::from_translation(stroke_origin_in_layer) * DAffine2::from_scale(stroke_size);
let normal_blend = FnNode::new(|(a, b)| blend_colors(a, b, BlendMode::Normal, 1.));
let blit_node = BlitNode::new(
FutureWrapperNode::new(ClonedNode::new(brush_texture)),
FutureWrapperNode::new(ClonedNode::new(positions)),
FutureWrapperNode::new(ClonedNode::new(normal_blend)),
);
let blit_target = if idx == 0 { let blit_target = if idx == 0 {
let target = core::mem::take(&mut brush_plan.first_stroke_texture); let target = core::mem::take(&mut brush_plan.first_stroke_texture);
extend_image_to_bounds((), List::new_from_item(target), stroke_to_layer) extend_image_to_bounds(&(), List::new_from_item(target), stroke_to_layer)
} else { } else {
empty_image((), stroke_to_layer, List::new_from_element(Color::TRANSPARENT)) empty_image(&(), stroke_to_layer, List::new_from_element(Color::TRANSPARENT))
// EmptyImageNode::new(CopiedNode::new(stroke_to_layer), CopiedNode::new(Color::TRANSPARENT)).eval(())
}; };
let list = blit_node.eval(blit_target).await; let list = blit(&(), blit_target, brush_texture, positions, |a, b| blend_colors(a, b, BlendMode::Normal, 1.));
assert_eq!(list.len(), 1); assert_eq!(list.len(), 1);
list.into_iter().next().unwrap_or_default() list.into_iter().next().unwrap_or_default()
}; };
@@ -291,7 +281,7 @@ async fn brush(
for stroke in trace.into_iter().map(|row| row.into_element()) { for stroke in trace.into_iter().map(|row| row.into_element()) {
let mut brush_texture = cache.get_cached_brush(&stroke.style); let mut brush_texture = cache.get_cached_brush(&stroke.style);
if brush_texture.is_none() { if brush_texture.is_none() {
let tex = create_brush_texture(&stroke.style).await; let tex = create_brush_texture(&stroke.style);
cache.store_brush(stroke.style.clone(), tex.clone()); cache.store_brush(stroke.style.clone(), tex.clone());
brush_texture = Some(tex); brush_texture = Some(tex);
} }
@@ -305,17 +295,15 @@ async fn brush(
_ => BlendMode::Restore, _ => BlendMode::Restore,
}; };
let blend_params = FnNode::new(move |(a, b)| blend_colors(a, b, mask_blend_mode, 1.)); erase_restore_mask = blit(&(), List::new_from_item(erase_restore_mask), brush_texture, positions, move |a, b| {
let blit_node = BlitNode::new( blend_colors(a, b, mask_blend_mode, 1.)
FutureWrapperNode::new(ClonedNode::new(brush_texture)), })
FutureWrapperNode::new(ClonedNode::new(positions)), .into_iter()
FutureWrapperNode::new(ClonedNode::new(blend_params)), .next()
); .unwrap_or_default();
erase_restore_mask = blit_node.eval(List::new_from_item(erase_restore_mask)).await.into_iter().next().unwrap_or_default();
} }
let blend_params = FnNode::new(|(a, b)| blend_colors(a, b, BlendMode::MultiplyAlpha, 1.)); actual_image = blend_image_closure(erase_restore_mask, actual_image, |a, b| blend_colors(a, b, BlendMode::MultiplyAlpha, 1.));
actual_image = blend_image_closure(erase_restore_mask, actual_image, |a, b| blend_params.eval((a, b)));
} }
let transform: DAffine2 = actual_image.attribute_cloned_or_default(ATTR_TRANSFORM); let transform: DAffine2 = actual_image.attribute_cloned_or_default(ATTR_TRANSFORM);
@@ -410,16 +398,16 @@ mod test {
#[test] #[test]
fn test_brush_texture() { fn test_brush_texture() {
let size = 20.; let size = 20.;
let image = brush_stamp_generator(size, Color::BLACK, 100., 100.); let image = brush_stamp_generator(&(), size, Color::BLACK, 100., 100.);
assert_eq!(image.transform(), DAffine2::from_scale_angle_translation(DVec2::splat(size.ceil()), 0., -DVec2::splat(size / 2.))); assert_eq!(image.transform(), DAffine2::from_scale_angle_translation(DVec2::splat(size.ceil()), 0., -DVec2::splat(size / 2.)));
// center pixel should be BLACK // center pixel should be BLACK
assert_eq!(image.sample(DVec2::splat(0.), DVec2::ONE), Some(Color::BLACK)); assert_eq!(image.sample(DVec2::splat(0.), DVec2::ONE), Some(Color::BLACK));
} }
#[tokio::test] #[test]
async fn test_brush_output_size() { fn test_brush_output_size() {
let image = brush( let image = brush(
(), &(),
&BrushCache::default(), &BrushCache::default(),
List::new_from_element(Raster::new_cpu(Image::<Color>::default())), List::new_from_element(Raster::new_cpu(Image::<Color>::default())),
List::new_from_element(BrushStroke { List::new_from_element(BrushStroke {
@@ -433,8 +421,7 @@ mod test {
blend_mode: BlendMode::Normal, blend_mode: BlendMode::Normal,
}, },
}), }),
) );
.await;
assert_eq!(image.element(0).unwrap().width, 20); assert_eq!(image.element(0).unwrap().width, 20);
} }
} }
+14 -13
View File
@@ -1,6 +1,7 @@
use core_types::gpoll::GPoll;
use core_types::list::List; use core_types::list::List;
use core_types::transform::Footprint; use core_types::transform::Footprint;
use core_types::{CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl}; use core_types::{CacheHash, Color, Context, Ctx, DeriveCtx, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphic_types::vector_types::GradientStops; use graphic_types::vector_types::GradientStops;
use graphic_types::{Artboard, Graphic, Vector}; use graphic_types::{Artboard, Graphic, Vector};
@@ -61,8 +62,8 @@ fn animation_time(
} }
#[node_macro::node(category("Debug"))] #[node_macro::node(category("Debug"))]
async fn quantize_real_time<T>( fn quantize_real_time<T>(
ctx: impl Ctx + ExtractAll + CloneVarArgs, ctx: impl Ctx + ExtractRealTime + DeriveCtx,
#[implementations( #[implementations(
Context -> bool, Context -> bool,
Context -> u32, Context -> u32,
@@ -84,11 +85,11 @@ async fn quantize_real_time<T>(
Context -> List<f64>, Context -> List<f64>,
Context -> (), Context -> (),
)] )]
value: impl Node<'n, Context<'static>, Output = T>, value: impl Node<Context<'_>, Output = T>,
#[default(1)] #[default(1)]
#[unit("sec")] #[unit("sec")]
quantum: f64, quantum: f64,
) -> T { ) -> GPoll<T> {
let time = ctx.try_real_time().unwrap_or_default(); let time = ctx.try_real_time().unwrap_or_default();
let time = time / 1000.; let time = time / 1000.;
let mut quantized_time = (time * quantum.recip()).round() / quantum.recip(); let mut quantized_time = (time * quantum.recip()).round() / quantum.recip();
@@ -96,13 +97,13 @@ async fn quantize_real_time<T>(
quantized_time = time; quantized_time = time;
} }
let quantized_time = quantized_time * 1000.; let quantized_time = quantized_time * 1000.;
let new_context = OwnedContextImpl::from(ctx).with_real_time(quantized_time); let scope = ctx.scope().with_real_time(Some(quantized_time));
value.eval(Some(new_context.into())).await value.eval(&ctx.with_scope(&scope))
} }
#[node_macro::node(category("Debug"))] #[node_macro::node(category("Debug"))]
async fn quantize_animation_time<T>( fn quantize_animation_time<T>(
ctx: impl Ctx + ExtractAll + CloneVarArgs, ctx: impl Ctx + ExtractAnimationTime + DeriveCtx,
#[implementations( #[implementations(
Context -> bool, Context -> bool,
Context -> u32, Context -> u32,
@@ -124,18 +125,18 @@ async fn quantize_animation_time<T>(
Context -> List<f64>, Context -> List<f64>,
Context -> (), Context -> (),
)] )]
value: impl Node<'n, Context<'static>, Output = T>, value: impl Node<Context<'_>, Output = T>,
#[default(1)] #[default(1)]
#[unit("sec")] #[unit("sec")]
quantum: f64, quantum: f64,
) -> T { ) -> GPoll<T> {
let time = ctx.try_animation_time().unwrap_or_default(); let time = ctx.try_animation_time().unwrap_or_default();
let mut quantized_time = (time * quantum.recip()).round() / quantum.recip(); let mut quantized_time = (time * quantum.recip()).round() / quantum.recip();
if !quantized_time.is_finite() { if !quantized_time.is_finite() {
quantized_time = time; quantized_time = time;
} }
let new_context = OwnedContextImpl::from(ctx).with_animation_time(quantized_time); let scope = ctx.scope().with_animation_time(Some(quantized_time));
value.eval(Some(new_context.into())).await value.eval(&ctx.with_scope(&scope))
} }
/// Produces the current position of the user's pointer within the document canvas. /// Produces the current position of the user's pointer within the document canvas.
+2 -2
View File
@@ -47,7 +47,7 @@ fn read_gradient(ctx: impl Ctx + ExtractVarArgs) -> List<GradientStops> {
} }
#[node_macro::node(category("Context"), path(core_types::vector))] #[node_macro::node(category("Context"), path(core_types::vector))]
async fn read_position( fn read_position(
ctx: impl Ctx + ExtractPosition, ctx: impl Ctx + ExtractPosition,
_primary: (), _primary: (),
/// The number of nested loops to traverse outwards (from the innermost loop) to get the position from. The most upstream loop is level 0, and downstream loops add levels. /// The number of nested loops to traverse outwards (from the innermost loop) to get the position from. The most upstream loop is level 0, and downstream loops add levels.
@@ -64,7 +64,7 @@ async fn read_position(
/// ///
/// Nested loops can enable 2D or higher-dimensional iteration by using the *Loop Level* parameter to read the index from outer levels of loops. /// Nested loops can enable 2D or higher-dimensional iteration by using the *Loop Level* parameter to read the index from outer levels of loops.
#[node_macro::node(category("Context"), path(core_types::vector))] #[node_macro::node(category("Context"), path(core_types::vector))]
async fn read_index( fn read_index(
ctx: impl Ctx + ExtractIndex, ctx: impl Ctx + ExtractIndex,
_primary: (), _primary: (),
/// The number of nested loops to traverse outwards (from the innermost loop) to get the index from. The most upstream loop is level 0, and downstream loops add levels. /// The number of nested loops to traverse outwards (from the innermost loop) to get the index from. The most upstream loop is level 0, and downstream loops add levels.
@@ -1,9 +1,10 @@
use core::f64; use core::f64;
use core_types::context::{CloneVarArgs, Context, ContextFeatures, Ctx, ExtractAll}; use core_types::Color;
use core_types::context::{Context, ContextModification, Ctx, DeriveCtx};
use core_types::gpoll::GPoll;
use core_types::list::{AttributeDyn, AttributeValueDyn, List, ListDyn}; use core_types::list::{AttributeDyn, AttributeValueDyn, List, ListDyn};
use core_types::transform::Footprint; use core_types::transform::Footprint;
use core_types::uuid::NodeId; use core_types::uuid::NodeId;
use core_types::{Color, OwnedContextImpl};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphic_types::vector_types::GradientStops; use graphic_types::vector_types::GradientStops;
use graphic_types::{Artboard, Graphic, Vector}; use graphic_types::{Artboard, Graphic, Vector};
@@ -12,8 +13,8 @@ use raster_types::{CPU, GPU, Raster};
/// Filters out what should be unused components of the context based on the specified requirements. /// Filters out what should be unused components of the context based on the specified requirements.
/// This node is inserted by the compiler to "zero out" unused context components. /// This node is inserted by the compiler to "zero out" unused context components.
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn context_modification<T>( fn context_modification<T>(
ctx: impl Ctx + CloneVarArgs + ExtractAll, ctx: impl Ctx + DeriveCtx,
/// The data to pass through, evaluated with the stripped down context. /// The data to pass through, evaluated with the stripped down context.
#[implementations( #[implementations(
Context -> (), Context -> (),
@@ -41,80 +42,10 @@ async fn context_modification<T>(
Context -> AttributeValueDyn, Context -> AttributeValueDyn,
Context -> ListDyn, Context -> ListDyn,
)] )]
value: impl Node<Context<'static>, Output = T>, value: impl Node<Context<'_>, Output = T>,
/// The parts of the context to keep when evaluating the input value. All other parts are nullified. /// The parts of the context to keep when evaluating the input value. All other parts are nullified.
features_to_keep: ContextFeatures, modification: ContextModification,
) -> T { ) -> GPoll<T> {
let new_context = OwnedContextImpl::from_flags(ctx, features_to_keep); let scope = ctx.scope().nullified(modification.features, Some(&modification.sources));
value.eval(&ctx.nullified(modification.features, &scope))
value.eval(Some(new_context.into())).await
}
#[cfg(test)]
mod tests {
use super::*;
use core_types::graphene_hash::CacheHash;
use core_types::transform::Footprint;
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
/// Verifies that nullified context fields don't affect the cache hash — only the kept features matter.
#[test]
fn test_nullified_context_hash_stability() {
use core_types::Context;
use std::sync::Arc;
let original_ctx: Context = Some(Arc::new(
OwnedContextImpl::empty()
.with_footprint(Footprint::default())
.with_index(1)
.with_real_time(10.5)
.with_vararg(Box::new("test"))
.with_animation_time(20.25),
));
// A second context with different values for the nullified fields
let changed_ctx: Context = Some(Arc::new(
OwnedContextImpl::empty()
.with_footprint(Footprint::default())
.with_index(2)
.with_real_time(999.9)
.with_vararg(Box::new("test"))
.with_animation_time(888.8),
));
// Nullify everything — both should hash the same regardless of their field values
let features_to_keep = ContextFeatures::empty();
let nullified1 = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), features_to_keep);
let nullified2 = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), features_to_keep);
let mut hasher1 = DefaultHasher::new();
nullified1.cache_hash(&mut hasher1);
let mut hasher2 = DefaultHasher::new();
nullified2.cache_hash(&mut hasher2);
assert_eq!(
hasher1.finish(),
hasher2.finish(),
"Hash of nullified context should remain stable regardless of input changes when features are nullified"
);
// Keep only footprint and varargs — both have the same footprint and vararg, so hash should still match
let partial_features = ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS;
let partial1 = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), partial_features);
let partial2 = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), partial_features);
let mut hasher3 = DefaultHasher::new();
partial1.cache_hash(&mut hasher3);
let mut hasher4 = DefaultHasher::new();
partial2.cache_hash(&mut hasher4);
assert_eq!(
hasher3.finish(),
hasher4.finish(),
"Hash should be stable when keeping only footprint and varargs and their values are the same"
);
}
} }
+244 -33
View File
@@ -1,54 +1,265 @@
use core_types::WasmNotSend; use core_types::arena::{Arena, ArenaCell};
use core_types::context::{Ctx, CtxSnapshot, DeriveCtx, ExtractAll};
use core_types::frame_table::{FrameTable, Lookup};
use core_types::gpoll::{Extent, Finality, GPoll, Interrupt};
use core_types::graphene_hash::CacheHash; use core_types::graphene_hash::CacheHash;
use core_types::memo::*; use core_types::memo::*;
use std::hash::DefaultHasher; use core_types::node::Node;
use std::hash::Hasher; use core_types::registry::cache_key;
use std::sync::Arc; use std::sync::Arc;
use std::sync::Mutex; use std::sync::Mutex;
/// Helps speed up repeated renders in a computationally-heavy part of the node graph. /// Helps speed up repeated renders in a computationally-heavy part of the node graph.
/// ///
/// Stores the last evaluated data that flowed through this node and immediately returns that data on subsequent renders if the context has not changed. /// Stores the last evaluated data that flowed through this node and immediately returns that data on subsequent renders if the context has not changed.
#[node_macro::node(category("General"), path(graphene_core::memo), skip_impl)] #[node_macro::node(category("General"), path(graphene_core::memo), skip_impl, extent(memoize_extent))]
async fn memoize<I: CacheHash + Send + 'n, T: Clone + WasmNotSend>(input: I, #[data] cache: Arc<Mutex<Option<(u64, T)>>>, content: impl Node<I, Output = T>) -> T { fn memoize<I: CacheHash, T: Clone>(input: I, #[data] cache: Arc<Mutex<Option<(u64, T, Finality)>>>, content: impl Node<I, Output = T>) -> GPoll<T> {
// Caches the output of a given node called with a specific input. let key = cache_key(&input);
// if let Some((hash, value, finality)) = cache.lock().unwrap().as_ref()
// A cache miss occurs when the Option is None. In this case, the node evaluates the inner node and memoizes (stores) the result. && *hash == key
// {
// A cache hit occurs when the Option is Some and has a stored hash matching the hash of the call argument. In this case, the node returns the cached value without re-evaluating the inner node. return match finality {
// Finality::AllFinal => GPoll::Final(value.clone()),
// Currently, only one input-output pair is cached. Subsequent calls with different inputs will overwrite the previous cache. Finality::Partial => GPoll::Partial(value.clone()),
};
let mut hasher = DefaultHasher::new();
input.cache_hash(&mut hasher);
let hash = hasher.finish();
if let Some(data) = cache.lock().as_ref().unwrap().as_ref().and_then(|data| (data.0 == hash).then_some(data.1.clone())) {
return data;
} }
let result = content.eval(input);
let value = content.eval(input).await; match &result {
*cache.lock().unwrap() = Some((hash, value.clone())); GPoll::Final(value) => *cache.lock().unwrap() = Some((key, value.clone(), Finality::AllFinal)),
value GPoll::Partial(value) => *cache.lock().unwrap() = Some((key, value.clone(), Finality::Partial)),
GPoll::Pending | GPoll::Fallback(_) | GPoll::Error(_) => {}
}
result
} }
type MonitorValue<I, T> = Arc<Mutex<Option<Arc<IORecord<I, T>>>>>; fn memoize_extent<C, T, NodeContent>(node: &MemoizeNode<T, NodeContent>, ctx: &C) -> GPoll<Extent>
where
T: Clone,
NodeContent: Node<C, Output = T>,
{
node.content.extent(ctx)
}
#[node_macro::node(category(""), path(graphene_core::memo), skip_impl, extent(frame_memo_extent))]
fn frame_memo<'e, T: Clone + 'static>(ctx: impl Ctx + CacheHash + ExtractArena<'e>, #[data] cell: ArenaCell<FrameTable<T, 32>>, content: impl Node<Context<'_>, Output = T>) -> GPoll<&'e T> {
let arena = ctx.arena();
let table = match cell.load(arena) {
Some(table) => table,
None => match arena.alloc(FrameTable::new()) {
Some((table, weak)) => {
cell.store(weak);
table
}
None => return park(arena, content.eval(ctx)),
},
};
match table.lookup(cache_key(ctx)) {
Lookup::Hit(Finality::AllFinal, value) => GPoll::Final(value),
Lookup::Hit(Finality::Partial, value) => GPoll::Partial(value),
Lookup::Vacant(slot) => match content.eval(ctx) {
GPoll::Final(value) => GPoll::Final(slot.publish(value, Finality::AllFinal)),
GPoll::Partial(value) => GPoll::Partial(slot.publish(value, Finality::Partial)),
unpublishable => {
slot.release();
park(arena, unpublishable)
}
},
Lookup::Full => park(arena, content.eval(ctx)),
}
}
fn frame_memo_extent<C, T, NodeContent>(node: &FrameMemoNode<T, NodeContent>, ctx: &C) -> GPoll<Extent>
where
T: Clone + 'static,
NodeContent: Node<C, Output = T>,
{
node.content.extent(ctx)
}
pub fn park<T>(arena: &Arena, result: GPoll<T>) -> GPoll<&T> {
match result {
GPoll::Final(value) => match arena.alloc(value) {
Some((parked, _)) => GPoll::Final(parked),
None => GPoll::arena_exhausted(),
},
GPoll::Partial(value) => match arena.alloc(value) {
Some((parked, _)) => GPoll::Partial(parked),
None => GPoll::arena_exhausted(),
},
GPoll::Fallback(boxed) => {
let (value, error) = *boxed;
match arena.alloc(value) {
Some((parked, _)) => GPoll::Fallback(Box::new((parked, error))),
None => GPoll::arena_exhausted(),
}
}
GPoll::Pending => GPoll::Pending,
GPoll::Error(error) => GPoll::Error(error),
}
}
type MonitorValue<T> = Arc<Mutex<Option<Arc<IORecord<CtxSnapshot, T>>>>>;
/// 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.
#[node_macro::node(category(""), path(graphene_core::memo), serialize(serialize_monitor), properties("monitor_properties"), skip_impl)] #[node_macro::node(category(""), path(graphene_core::memo), serialize(serialize_monitor), properties("monitor_properties"), skip_impl)]
async fn monitor<I: Clone + 'static + Send + Sync, T: Clone + 'static + Send + Sync>( fn monitor<T: Clone + 'static + Send + Sync>(
input: I, ctx: impl Ctx + DeriveCtx + ExtractAll,
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
#[data] #[data]
io: MonitorValue<I, T>, io: MonitorValue<T>,
content: impl Node<I, Output = T>, content: impl Node<Context<'_>, Output = T>,
) -> T { ) -> Result<T, Interrupt> {
let output = content.eval(input.clone()).await; let output = content.eval(&ctx.derived())?;
*io.lock().unwrap() = Some(Arc::new(IORecord { input, output: output.clone() })); *io.lock().unwrap() = Some(Arc::new(IORecord {
output input: CtxSnapshot::capture(ctx),
output: output.clone(),
}));
Ok(output)
} }
fn serialize_monitor<I: Clone + 'static + Send + Sync, T: Clone + 'static + Send + Sync>(io: &MonitorValue<I, T>) -> Option<Arc<dyn std::any::Any + Send + Sync>> { fn serialize_monitor<T: Clone + 'static + Send + Sync>(io: &MonitorValue<T>) -> Option<Arc<dyn std::any::Any + Send + Sync>> {
let io = io.lock().unwrap(); let io = io.lock().unwrap();
io.as_ref().map(|output| output.clone() as Arc<dyn std::any::Any + Send + Sync>) io.as_ref().map(|output| output.clone() as Arc<dyn std::any::Any + Send + Sync>)
} }
#[cfg(test)]
mod tests {
use super::*;
use core_types::SourceId;
use core_types::Type;
use core_types::concrete;
use core_types::context::{ContextImpl, EvalScope};
use core_types::registry::{EdgeHandle, ErasedLendNode, ErasedNode};
use std::sync::atomic::{AtomicU32, Ordering};
struct CountingNode(AtomicU32);
impl<Input> Node<Input> for CountingNode {
type Output = u32;
fn eval(&self, _input: &Input) -> GPoll<u32> {
GPoll::Final(self.0.fetch_add(1, Ordering::Relaxed) + 1)
}
}
struct PartialCountingNode(AtomicU32);
impl<Input> Node<Input> for PartialCountingNode {
type Output = u32;
fn eval(&self, _input: &Input) -> GPoll<u32> {
GPoll::Partial(self.0.fetch_add(1, Ordering::Relaxed) + 1)
}
}
struct ValueNode<T>(T);
impl<T: Clone, Input> Node<Input> for ValueNode<T> {
type Output = T;
fn eval(&self, _input: &Input) -> GPoll<T> {
GPoll::Final(self.0.clone())
}
}
fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
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<ErasedNode<u32>>);
assert!(handle.serialize().is_none(), "no record before the first eval");
let edge = handle.duplicate().downcast::<u32>().unwrap();
assert_eq!(edge.eval(&ctx), GPoll::Final(11));
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);
}
#[test]
fn memoize_caches_across_evals() {
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let memoized = MemoizeNode::new(CountingNode(AtomicU32::new(0)));
assert_eq!(memoized.eval(&ctx), GPoll::Final(1));
assert_eq!(memoized.eval(&ctx), GPoll::Final(1));
}
#[test]
fn memo_invalidates_on_generation_bump() {
let arena = Arena::new(1024);
let source: SourceId = 7;
let before = [(source, 1)];
let after = [(source, 2)];
let scope_before = scope_fixture(&before, &arena);
let scope_after = scope_fixture(&after, &arena);
let memoized = MemoizeNode::new(CountingNode(AtomicU32::new(0)));
assert_eq!(memoized.eval(&ContextImpl::root(&scope_before)), GPoll::Final(1));
assert_eq!(memoized.eval(&ContextImpl::root(&scope_before)), GPoll::Final(1));
assert_eq!(memoized.eval(&ContextImpl::root(&scope_after)), GPoll::Final(2));
}
#[test]
fn memo_replays_partiality_on_hit() {
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let memoized = MemoizeNode::new(PartialCountingNode(AtomicU32::new(0)));
assert_eq!(memoized.eval(&ctx), GPoll::Partial(1));
assert_eq!(memoized.eval(&ctx), GPoll::Partial(1));
}
#[test]
fn memoized_edges_stack_and_rewire() {
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let edge = EdgeHandle::new(Arc::new(CountingNode(AtomicU32::new(0))) as Arc<ErasedNode<u32>>);
let memoized = EdgeHandle::new(Arc::new(MemoizeNode::new(edge.downcast::<u32>().unwrap())) as Arc<ErasedNode<u32>>);
let stacked = MemoizeNode::new(memoized.downcast::<u32>().unwrap());
assert_eq!(stacked.eval(&ctx), GPoll::Final(1));
assert_eq!(stacked.eval(&ctx), GPoll::Final(1));
}
#[test]
fn frame_memo_turns_an_owned_edge_into_a_lending_edge() {
let arena = Arena::new(4096);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let edge = EdgeHandle::new(Arc::new(ValueNode("lent out".to_string())) as Arc<ErasedNode<String>>);
let lending = EdgeHandle::new_ref(Arc::new(FrameMemoNode::new(edge.downcast::<String>().unwrap())) as Arc<ErasedLendNode<String>>);
assert_eq!(*lending.ty(), core_types::registry::lend_edge_type::<String>());
let node = lending.downcast_lend::<String>().unwrap();
let GPoll::Final(first) = node.eval(&ctx) else {
panic!("lend must fill the frame table and lend");
};
let GPoll::Final(second) = node.eval(&ctx) else {
panic!("second eval must lend the published value");
};
assert_eq!(first, "lent out");
assert!(std::ptr::eq(first, second));
}
}
+12 -8
View File
@@ -1,9 +1,8 @@
use core_types::{Ctx, ExtractFootprint, ops::Convert, transform::Footprint}; use core_types::ExtractAll;
use core_types::runtime::SourceFuture;
use core_types::{Ctx, ops::Convert, ops::ConvertAsync, transform::Footprint};
use std::marker::PhantomData; use std::marker::PhantomData;
// Re-export TypeNode from core-types for convenience
pub use core_types::ops::TypeNode;
/// Passes-through the input value without changing it. This is useful for rerouting wires for organization purposes. /// Passes-through the input value without changing it. This is useful for rerouting wires for organization purposes.
#[node_macro::node(category("General"), skip_impl)] #[node_macro::node(category("General"), skip_impl)]
fn passthrough<'i, T: 'i + Send>(_: impl Ctx, content: T) -> T { fn passthrough<'i, T: 'i + Send>(_: impl Ctx, content: T) -> T {
@@ -11,13 +10,18 @@ fn passthrough<'i, T: 'i + Send>(_: impl Ctx, content: T) -> T {
} }
#[node_macro::node(category(""), skip_impl)] #[node_macro::node(category(""), skip_impl)]
fn into<'i, T: 'i + Send + Into<O>, O: 'i + Send>(_: impl Ctx, value: T, _out_ty: PhantomData<O>) -> O { fn into<T: Send + Into<O>, O: Send>(_: impl Ctx, value: T, #[data] _out_ty: PhantomData<O>) -> O {
value.into() value.into()
} }
#[node_macro::node(category(""), skip_impl)] #[node_macro::node(category(""), skip_impl)]
async fn convert<'i, T: 'i + Send + Convert<O, C>, O: 'i + Send, C: 'i + Send>(ctx: impl Ctx + ExtractFootprint, value: T, converter: C, _out_ty: PhantomData<O>) -> O { fn convert<T: Send + Convert<O, C>, O: Send, C: Send>(ctx: impl Ctx + ExtractAll, value: T, converter: C, #[data] _out_ty: PhantomData<O>) -> O {
value.convert(*ctx.try_footprint().unwrap_or(&Footprint::DEFAULT), converter).await value.convert(*ctx.try_footprint().unwrap_or(&Footprint::DEFAULT), converter)
}
#[node_macro::node(category(""), skip_impl)]
fn convert_async<T: Send + ConvertAsync<O, C>, O: Send + 'static, C: Send>(ctx: impl Ctx + ExtractAll, value: T, converter: C, #[data] _out_ty: PhantomData<O>) -> SourceFuture<O> {
value.convert(*ctx.try_footprint().unwrap_or(&Footprint::DEFAULT), converter)
} }
#[cfg(test)] #[cfg(test)]
@@ -26,6 +30,6 @@ mod test {
#[test] #[test]
pub fn passthrough_node() { pub fn passthrough_node() {
assert_eq!(passthrough((), &4), &4); assert_eq!(passthrough(&(), &4), &4);
} }
} }
+10 -14
View File
@@ -1,6 +1,7 @@
use core_types::gpoll::Interrupt;
use core_types::list::{Item, List}; use core_types::list::{Item, List};
use core_types::transform::TransformMut; use core_types::transform::TransformMut;
use core_types::{ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_LOCATION, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl}; use core_types::{ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_LOCATION, Color, Context, Ctx, DeriveCtx, ModifyFootprint};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphic_types::graphic::{Graphic, IntoGraphicList}; use graphic_types::graphic::{Graphic, IntoGraphicList};
use graphic_types::{Artboard, Vector}; use graphic_types::{Artboard, Vector};
@@ -9,8 +10,8 @@ use vector_types::GradientStops;
/// Constructs a single-element `Artboard[]` with the given content and metadata stored as row attributes. /// Constructs a single-element `Artboard[]` with the given content and metadata stored as row attributes.
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
pub async fn create_artboard<T: IntoGraphicList>( pub fn create_artboard<T: IntoGraphicList>(
ctx: impl ExtractAll + CloneVarArgs + Ctx, ctx: impl Ctx + DeriveCtx + ModifyFootprint,
/// Graphics to include within the artboard. /// Graphics to include within the artboard.
#[implementations( #[implementations(
Context -> List<Graphic>, Context -> List<Graphic>,
@@ -22,7 +23,7 @@ pub async fn create_artboard<T: IntoGraphicList>(
Context -> List<GradientStops>, Context -> List<GradientStops>,
Context -> DAffine2, Context -> DAffine2,
)] )]
content: impl Node<Context<'static>, Output = T>, content: impl Node<Context<'_>, Output = T>,
/// Coordinate of the top-left corner of the artboard within the document. /// Coordinate of the top-left corner of the artboard within the document.
location: DVec2, location: DVec2,
/// Width and height of the artboard within the document. /// Width and height of the artboard within the document.
@@ -32,14 +33,9 @@ pub async fn create_artboard<T: IntoGraphicList>(
/// Whether to cut off the contained content that extends outside the artboard, or keep it visible. /// Whether to cut off the contained content that extends outside the artboard, or keep it visible.
#[default(true)] #[default(true)]
clip: bool, clip: bool,
) -> List<Artboard> { ) -> Result<List<Artboard>, Interrupt> {
let footprint = ctx.try_footprint().copied(); let translated = ctx.modify_footprint(|footprint| footprint.translate(location));
let mut new_ctx = OwnedContextImpl::from(ctx); let content = content.eval(&translated.ctx())?.into_graphic_list();
if let Some(mut footprint) = footprint {
footprint.translate(location);
new_ctx = new_ctx.with_footprint(footprint);
}
let content = content.eval(new_ctx.into_context()).await.into_graphic_list();
// Normalize so `location` is the top-left corner and `dimensions` are positive (allowing negative input // Normalize so `location` is the top-left corner and `dimensions` are positive (allowing negative input
// dimensions to represent dragging from the opposite corner). Compute the corner using the raw signed // dimensions to represent dragging from the opposite corner). Compute the corner using the raw signed
@@ -50,11 +46,11 @@ pub async fn create_artboard<T: IntoGraphicList>(
let background = background.element(0).copied().unwrap_or(Color::WHITE); let background = background.element(0).copied().unwrap_or(Color::WHITE);
// Name is not stored here, it's resolved live from the parent layer's display name // Name is not stored here, it's resolved live from the parent layer's display name
List::new_from_item( Ok(List::new_from_item(
Item::new_from_element(Artboard::new(content)) Item::new_from_element(Artboard::new(content))
.with_attribute(ATTR_LOCATION, normalized_location) .with_attribute(ATTR_LOCATION, normalized_location)
.with_attribute(ATTR_DIMENSIONS, normalized_dimensions) .with_attribute(ATTR_DIMENSIONS, normalized_dimensions)
.with_attribute(ATTR_BACKGROUND, background) .with_attribute(ATTR_BACKGROUND, background)
.with_attribute(ATTR_CLIP, clip), .with_attribute(ATTR_CLIP, clip),
) ))
} }
+30 -26
View File
@@ -1,8 +1,9 @@
use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::gpoll::Interrupt;
use core_types::list::{AttributeDyn, AttributeValueDyn, Item, List, ListDyn}; use core_types::list::{AttributeDyn, AttributeValueDyn, Item, List, ListDyn};
use core_types::registry::types::{Angle, SignedInteger}; use core_types::registry::types::{Angle, SignedInteger};
use core_types::uuid::NodeId; use core_types::uuid::NodeId;
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, AnyHash, BlendMode, CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl}; use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, AnyHash, BlendMode, CacheHash, Color, Context, Ctx, DeriveCtx};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphic_types::graphic::{Graphic, IntoGraphicList}; use graphic_types::graphic::{Graphic, IntoGraphicList};
use graphic_types::{Artboard, Vector}; use graphic_types::{Artboard, Vector};
@@ -108,8 +109,8 @@ pub fn extract_element<T: Clone + Default + Send + Sync + 'static>(
} }
#[node_macro::node(category("General"))] #[node_macro::node(category("General"))]
async fn map<Item: AnyHash + Send + Sync + CacheHash>( fn map<Item: AnyHash + Clone + Send + Sync + CacheHash>(
ctx: impl Ctx + CloneVarArgs + ExtractAll, ctx: impl Ctx + DeriveCtx,
#[implementations( #[implementations(
List<Graphic>, List<Graphic>,
List<Vector>, List<Vector>,
@@ -127,23 +128,24 @@ async fn map<Item: AnyHash + Send + Sync + CacheHash>(
Context -> List<GradientStops>, Context -> List<GradientStops>,
Context -> List<String>, Context -> List<String>,
)] )]
mapped: impl Node<Context<'static>, Output = List<Item>>, mapped: impl Node<Context<'_>, Output = List<Item>>,
) -> List<Item> { ) -> Result<List<Item>, Interrupt> {
let spilled = ctx.index_head();
let mut rows = List::new(); let mut rows = List::new();
for (i, row) in content.into_iter().enumerate() { for (i, row) in content.into_iter().enumerate() {
let owned_ctx = OwnedContextImpl::from(ctx.clone()); let item = List::new_from_item(row);
let owned_ctx = owned_ctx.with_vararg(Box::new(List::new_from_item(row))).with_index(i); let scoped = ctx.push_vararg(&item);
let list = mapped.eval(owned_ctx.into_context()).await; let list = mapped.eval(&scoped.ctx().promoted(&spilled, i as u64))?;
rows.extend(list); rows.extend(list);
} }
rows Ok(rows)
} }
#[node_macro::node(category("General"))] #[node_macro::node(category("General"))]
async fn mirror<T: 'n + Send + Clone>( fn mirror<T: Send + Clone>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Graphic>, List<Graphic>,
@@ -229,8 +231,8 @@ pub fn path_of_subgraph(_: impl Ctx, node_path: List<NodeId>) -> List<NodeId> {
/// The value is type-erased into an `AttributeValueDyn` by an auto-inserted convert node, so this node only /// The value is type-erased into an `AttributeValueDyn` by an auto-inserted convert node, so this node only
/// monomorphizes over `T` instead of the cartesian product `(T, U)`. /// monomorphizes over `T` instead of the cartesian product `(T, U)`.
#[node_macro::node(category("Attributes: Write"))] #[node_macro::node(category("Attributes: Write"))]
async fn write_attribute<T: AnyHash + Clone + Send + Sync + CacheHash>( fn write_attribute<T: AnyHash + Clone + Send + Sync + CacheHash>(
ctx: impl ExtractAll + CloneVarArgs + Ctx, ctx: impl Ctx + DeriveCtx,
/// The `List` to set the named attribute on (one value per item). /// The `List` to set the named attribute on (one value per item).
#[implementations( #[implementations(
List<Artboard>, List<Artboard>,
@@ -252,15 +254,17 @@ async fn write_attribute<T: AnyHash + Clone + Send + Sync + CacheHash>(
name: String, name: String,
/// The node that produces the attribute value for each item. Called once per item with the item's index in context. /// The node that produces the attribute value for each item. Called once per item with the item's index in context.
#[implementations(Context -> AttributeValueDyn)] #[implementations(Context -> AttributeValueDyn)]
value: impl Node<'n, Context<'static>, Output = AttributeValueDyn>, value: impl Node<Context<'_>, Output = AttributeValueDyn>,
) -> List<T> { ) -> Result<List<T>, Interrupt> {
let spilled = ctx.index_head();
for index in 0..content.len() { for index in 0..content.len() {
let row = content.clone_item(index).expect("index is within bounds"); let row = content.clone_item(index).expect("index is within bounds");
let owned_ctx = OwnedContextImpl::from(ctx.clone()).with_vararg(Box::new(List::new_from_item(row))).with_index(index); let item = List::new_from_item(row);
let v = value.eval(owned_ctx.into_context()).await; let scoped = ctx.push_vararg(&item);
let v = value.eval(&scoped.ctx().promoted(&spilled, index as u64))?;
content.set_attribute_value_dyn(&name, index, v); content.set_attribute_value_dyn(&name, index, v);
} }
content Ok(content)
} }
/// Sets a named attribute on the primary list, with each value taken from the corresponding item's element in the source list (paired by index, wrapping if the source has fewer items). /// Sets a named attribute on the primary list, with each value taken from the corresponding item's element in the source list (paired by index, wrapping if the source has fewer items).
@@ -497,7 +501,7 @@ fn read_attribute_raster(
/// Joins two `List`s of the same type, extending the base `List` with the items from the new `List`. /// Joins two `List`s of the same type, extending the base `List` with the items from the new `List`.
#[node_macro::node(category("General"))] #[node_macro::node(category("General"))]
pub async fn extend<T: 'n + Send + Clone>( pub fn extend<T: Send + Clone>(
_: impl Ctx, _: impl Ctx,
/// The `List` whose items will appear at the start of the extended `List`. /// The `List` whose items will appear at the start of the extended `List`.
#[implementations(List<Artboard>, List<Graphic>, List<Vector>, List<String>, List<Raster<CPU>>, List<Raster<GPU>>, List<Color>, List<GradientStops>)] #[implementations(List<Artboard>, List<Graphic>, List<Vector>, List<String>, List<Raster<CPU>>, List<Raster<GPU>>, List<Color>, List<GradientStops>)]
@@ -517,7 +521,7 @@ pub async fn extend<T: 'n + Send + Clone>(
/// Performs an obsolete function as part of a migration from an older document format. /// Performs an obsolete function as part of a migration from an older document format.
/// Users are advised to delete this node and replace it with a new one. /// Users are advised to delete this node and replace it with a new one.
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
pub async fn legacy_layer_extend<T: 'n + Send + Clone>( pub fn legacy_layer_extend<T: Send + Clone>(
_: impl Ctx, _: impl Ctx,
#[implementations(List<Artboard>, List<Graphic>, List<Vector>, List<String>, List<Raster<CPU>>, List<Raster<GPU>>, List<Color>, List<GradientStops>)] base: List<T>, #[implementations(List<Artboard>, List<Graphic>, List<Vector>, List<String>, List<Raster<CPU>>, List<Raster<GPU>>, List<Color>, List<GradientStops>)] base: List<T>,
#[expose] #[expose]
@@ -544,7 +548,7 @@ pub async fn legacy_layer_extend<T: 'n + Send + Clone>(
/// Nests the input graphical content in a wrapper graphic. This essentially "groups" the input. /// Nests the input graphical content in a wrapper graphic. This essentially "groups" the input.
/// The inverse of this node is 'Flatten Graphic'. /// The inverse of this node is 'Flatten Graphic'.
#[node_macro::node(category("General"))] #[node_macro::node(category("General"))]
pub async fn wrap_graphic<T: Into<Graphic> + 'n>( pub fn wrap_graphic<T: Into<Graphic>>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Graphic>, List<Graphic>,
@@ -565,7 +569,7 @@ pub async fn wrap_graphic<T: Into<Graphic> + 'n>(
/// Converts a list of graphical content into a `Graphic[]` by placing it into an element of a new wrapper `Graphic[]`. /// Converts a list of graphical content into a `Graphic[]` by placing it into an element of a new wrapper `Graphic[]`.
/// If it is already a `Graphic[]`, it is not wrapped again. Use the 'Wrap Graphic' node if wrapping is always desired. /// If it is already a `Graphic[]`, it is not wrapped again. Use the 'Wrap Graphic' node if wrapping is always desired.
#[node_macro::node(category("General"))] #[node_macro::node(category("General"))]
pub async fn to_graphic<T: IntoGraphicList>( pub fn to_graphic<T: IntoGraphicList>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Graphic>, List<Graphic>,
@@ -583,7 +587,7 @@ pub async fn to_graphic<T: IntoGraphicList>(
/// Removes a level of nesting from a `Graphic[]`, or all nesting if "Fully Flatten" is enabled. /// Removes a level of nesting from a `Graphic[]`, or all nesting if "Fully Flatten" is enabled.
#[node_macro::node(category("General"))] #[node_macro::node(category("General"))]
pub async fn flatten_graphic(_: impl Ctx, content: List<Graphic>, fully_flatten: bool) -> List<Graphic> { pub fn flatten_graphic(_: impl Ctx, content: List<Graphic>, fully_flatten: bool) -> List<Graphic> {
// TODO: Avoid mutable reference, instead return a new List<Graphic>? // TODO: Avoid mutable reference, instead return a new List<Graphic>?
fn flatten_list(output_graphic_list: &mut List<Graphic>, current_graphic_list: List<Graphic>, fully_flatten: bool, recursion_depth: usize) { fn flatten_list(output_graphic_list: &mut List<Graphic>, current_graphic_list: List<Graphic>, fully_flatten: bool, recursion_depth: usize) {
for index in 0..current_graphic_list.len() { for index in 0..current_graphic_list.len() {
@@ -620,7 +624,7 @@ pub async fn flatten_graphic(_: impl Ctx, content: List<Graphic>, fully_flatten:
/// Converts a `Graphic[]` into a `Vector[]` by deeply flattening any vector content it contains, and discarding any non-vector content. /// Converts a `Graphic[]` into a `Vector[]` by deeply flattening any vector content it contains, and discarding any non-vector content.
#[node_macro::node(category("Vector"))] #[node_macro::node(category("Vector"))]
pub async fn flatten_vector<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> { pub fn flatten_vector<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> {
let graphic_list = content.into_graphic_list(); let graphic_list = content.into_graphic_list();
let mut output: List<Vector> = graphic_list.clone().into_flattened_list(); let mut output: List<Vector> = graphic_list.clone().into_flattened_list();
@@ -653,19 +657,19 @@ pub async fn flatten_vector<T: IntoGraphicList>(_: impl Ctx, #[implementations(L
/// Converts a `Graphic[]` into a `Raster[]` by deeply flattening any raster content it contains, and discarding any non-raster content. /// Converts a `Graphic[]` into a `Raster[]` by deeply flattening any raster content it contains, and discarding any non-raster content.
#[node_macro::node(category("Raster"))] #[node_macro::node(category("Raster"))]
pub async fn flatten_raster<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Raster<CPU>>)] content: T) -> List<Raster<CPU>> { pub fn flatten_raster<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Raster<CPU>>)] content: T) -> List<Raster<CPU>> {
content.into_flattened_list() content.into_flattened_list()
} }
/// Converts a `Graphic[]` into a `Color[]` by deeply flattening any color content it contains, and discarding any non-color content. /// Converts a `Graphic[]` into a `Color[]` by deeply flattening any color content it contains, and discarding any non-color content.
#[node_macro::node(category("General"))] #[node_macro::node(category("General"))]
pub async fn flatten_color<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Color>)] content: T) -> List<Color> { pub fn flatten_color<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Color>)] content: T) -> List<Color> {
content.into_flattened_list() content.into_flattened_list()
} }
/// Converts a `Graphic[]` into a `GradientStops[]` by deeply flattening any gradient content it contains, and discarding any non-gradient content. /// Converts a `Graphic[]` into a `GradientStops[]` by deeply flattening any gradient content it contains, and discarding any non-gradient content.
#[node_macro::node(category("General"))] #[node_macro::node(category("General"))]
pub async fn flatten_gradient<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<GradientStops>)] content: T) -> List<GradientStops> { pub fn flatten_gradient<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<GradientStops>)] content: T) -> List<GradientStops> {
content.into_flattened_list() content.into_flattened_list()
} }
-27
View File
@@ -1,27 +0,0 @@
use core_types::NodeIO;
use core_types::WasmNotSend;
pub use core_types::registry::{DowncastBothNode, DynAnyNode, FutureWrapperNode, PanicNode};
pub use core_types::{Node, generic, ops};
use dyn_any::StaticType;
pub use graph_craft::proto::{Any, NodeContainer, TypeErasedBox, TypeErasedNode};
use graph_craft::proto::{FutureAny, SharedNodeContainer};
pub trait IntoTypeErasedNode<'n> {
fn into_type_erased(self) -> TypeErasedBox<'n>;
}
impl<'n, N: 'n> IntoTypeErasedNode<'n> for N
where
N: for<'i> NodeIO<'i, Any<'i>, Output = FutureAny<'i>> + Sync + WasmNotSend,
{
fn into_type_erased(self) -> TypeErasedBox<'n> {
Box::new(self)
}
}
pub fn input_node<O: StaticType>(n: SharedNodeContainer) -> DowncastBothNode<(), O> {
downcast_node(n)
}
pub fn downcast_node<I: StaticType, O: StaticType>(n: SharedNodeContainer) -> DowncastBothNode<I, O> {
DowncastBothNode::new(n)
}
+1 -1
View File
@@ -1,9 +1,9 @@
pub mod any;
pub mod platform_application_io; pub mod platform_application_io;
pub mod render_background; pub mod render_background;
pub mod render_cache; pub mod render_cache;
pub mod render_node; pub mod render_node;
pub mod render_pixel_preview; pub mod render_pixel_preview;
pub mod runtime;
pub mod text; pub mod text;
pub use blending_nodes; pub use blending_nodes;
pub use brush_nodes as brush; pub use brush_nodes as brush;
@@ -3,9 +3,11 @@ use base64::Engine;
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
use canvas_utils::{Canvas, CanvasHandle}; use canvas_utils::{Canvas, CanvasHandle};
use core_types::color::SRGBA8; use core_types::color::SRGBA8;
use core_types::gpoll::GPoll;
use core_types::list::{Item, List}; use core_types::list::{Item, List};
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
use core_types::math::bbox::Bbox; use core_types::math::bbox::Bbox;
use core_types::runtime::SourceFuture;
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
use core_types::transform::Footprint; use core_types::transform::Footprint;
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
@@ -137,7 +139,7 @@ fn image_to_bytes(_: impl Ctx, image: List<Raster<CPU>>) -> List<u8> {
/// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue. /// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue.
#[node_macro::node(category("Web Request"))] #[node_macro::node(category("Web Request"))]
async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[name("URL")] url: String) -> Arc<[u8]> { async fn load_resource(_: impl Ctx, _primary: (), #[name("URL")] url: String) -> Arc<[u8]> {
let placeholder = || -> Arc<[u8]> { Arc::from(Vec::<u8>::new()) }; let placeholder = || -> Arc<[u8]> { Arc::from(Vec::<u8>::new()) };
let response = match reqwest::Client::new().get(&url).send().await { let response = match reqwest::Client::new().get(&url).send().await {
@@ -185,14 +187,14 @@ fn decode_image(_: impl Ctx, data: Arc<[u8]>) -> List<Raster<CPU>> {
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn create_canvas(_: impl Ctx) -> CanvasHandle { fn create_canvas(_: impl Ctx) -> CanvasHandle {
CanvasHandle::new() CanvasHandle::new()
} }
/// Renders a view of the input graphic within an area defined by the *Footprint*. /// Renders a view of the input graphic within an area defined by the *Footprint*.
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn rasterize<T: WasmNotSend + Clone + 'n>( async fn rasterize<T: WasmNotSend + Clone>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Vector>, List<Vector>,
@@ -262,29 +264,37 @@ where
} }
#[node_macro::node(category(""), inject_scope)] #[node_macro::node(category(""), inject_scope)]
pub async fn editor_api<'a: 'n>(_: impl Ctx, #[scope("editor-api")] editor_api: &'a PlatformEditorApi) -> &'a PlatformEditorApi { pub fn editor_api(_: impl Ctx, #[scope("editor-api")] editor_api: Arc<PlatformEditorApi>) -> Arc<PlatformEditorApi> {
editor_api editor_api
} }
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
pub async fn resource<'a: 'n>(_: impl Ctx, hash: ResourceHash, #[scope(editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi) -> Resource { pub fn resource(_: impl Ctx, hash: ResourceHash, #[scope(editor_api::IDENTIFIER)] editor_api: Arc<PlatformEditorApi>) -> SourceFuture<GPoll<Resource>> {
let application_io = editor_api.application_io.as_ref().expect("ApplicationIo must be available when using resources"); let application_io = editor_api.application_io.clone();
application_io.load_resource(hash).await.unwrap_or_else(|| { Box::pin(async move {
panic!("Resource {hash} not found"); let Some(application_io) = application_io else {
return GPoll::error("ApplicationIo not available");
};
match application_io.load_resource(hash).await {
Some(resource) => GPoll::Final(resource),
None => GPoll::error("resource not found"),
}
}) })
} }
#[node_macro::node(category(""), inject_scope)] #[node_macro::node(category(""), inject_scope)]
pub async fn wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi) -> &'a ::wgpu_executor::WgpuExecutor { pub fn wgpu_executor(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Arc<PlatformEditorApi>) -> ::wgpu_executor::WgpuExecutorHandle {
editor_api ::wgpu_executor::WgpuExecutorHandle(
.application_io editor_api
.as_ref() .application_io
.expect("ApplicationIo not not available") .as_ref()
.gpu_executor() .expect("ApplicationIo not not available")
.expect("GPU executor not available") .gpu_executor_arc()
.expect("GPU executor not available"),
)
} }
#[node_macro::node(category(""), inject_scope)] #[node_macro::node(category(""), inject_scope)]
pub async fn try_wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi) -> Option<&'a ::wgpu_executor::WgpuExecutor> { pub fn try_wgpu_executor(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Arc<PlatformEditorApi>) -> Option<::wgpu_executor::WgpuExecutorHandle> {
editor_api.application_io.as_ref()?.gpu_executor() editor_api.application_io.as_ref()?.gpu_executor_arc().map(::wgpu_executor::WgpuExecutorHandle)
} }
+13 -19
View File
@@ -9,14 +9,10 @@ use graphic_types::raster_types::Texture;
use rendering::{RenderParams, SvgRender, SvgRenderOutput}; use rendering::{RenderParams, SvgRender, SvgRenderOutput};
use std::fmt::Write; use std::fmt::Write;
use wgpu::util::DeviceExt; use wgpu::util::DeviceExt;
use wgpu_executor::{AsyncWgpuPipeline, WgpuExecutor, WgpuPipelineCache}; use wgpu_executor::{WgpuExecutor, WgpuPipeline, WgpuPipelineCache};
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn render_background<'a: 'n>( fn render_background<'a>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, #[scope(composite_background_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache, data: RenderOutput) -> RenderOutput {
ctx: impl Ctx + ExtractFootprint + ExtractVarArgs,
#[scope(composite_background_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache,
data: RenderOutput,
) -> RenderOutput {
let footprint = ctx.footprint(); let footprint = ctx.footprint();
let render_params = ctx let render_params = ctx
.vararg(0) .vararg(0)
@@ -35,14 +31,12 @@ async fn render_background<'a: 'n>(
let data = match foreground_data { let data = match foreground_data {
RenderOutputType::Texture(foreground_texture) => { RenderOutputType::Texture(foreground_texture) => {
let doc_to_screen = render_params.footprint.transform.as_affine2(); let doc_to_screen = render_params.footprint.transform.as_affine2();
let blended = pipeline let blended = pipeline.run::<CompositeBackground>(&CompositeBackgroundArgs {
.run::<CompositeBackground>(&CompositeBackgroundArgs { foreground: foreground_texture.as_ref(),
foreground: foreground_texture.as_ref(), backgrounds: &metadata.backgrounds,
backgrounds: &metadata.backgrounds, document_to_screen: doc_to_screen,
document_to_screen: doc_to_screen, zoom: render_params.viewport_zoom.to_f32(),
zoom: render_params.viewport_zoom.to_f32(), });
})
.await;
RenderOutputType::Texture(blended) RenderOutputType::Texture(blended)
} }
@@ -121,9 +115,9 @@ async fn render_background<'a: 'n>(
} }
#[node_macro::node(category(""), inject_scope)] #[node_macro::node(category(""), inject_scope)]
async fn composite_background_pipeline<'a: 'n>( fn composite_background_pipeline(
_ctx: impl Ctx, _ctx: impl Ctx,
#[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<&'a WgpuExecutor>, #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<wgpu_executor::WgpuExecutorHandle>,
#[data] pipeline: WgpuPipelineCache, #[data] pipeline: WgpuPipelineCache,
) -> WgpuPipelineCache { ) -> WgpuPipelineCache {
if let Some(executor) = executor { if let Some(executor) = executor {
@@ -148,7 +142,7 @@ pub struct CompositeBackgroundArgs<'a> {
zoom: f32, zoom: f32,
} }
impl AsyncWgpuPipeline for CompositeBackground { impl WgpuPipeline for CompositeBackground {
type Args<'a> = CompositeBackgroundArgs<'a>; type Args<'a> = CompositeBackgroundArgs<'a>;
type Out = Texture; type Out = Texture;
@@ -331,7 +325,7 @@ impl AsyncWgpuPipeline for CompositeBackground {
} }
} }
async fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> Self::Out { fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> Self::Out {
let &CompositeBackgroundArgs { let &CompositeBackgroundArgs {
foreground, foreground,
backgrounds, backgrounds,
@@ -340,7 +334,7 @@ impl AsyncWgpuPipeline for CompositeBackground {
} = args; } = args;
let foreground_size = foreground.size(); let foreground_size = foreground.size();
let output = executor.request_texture(UVec2::new(foreground_size.width, foreground_size.height)).await; let output = executor.request_texture(UVec2::new(foreground_size.width, foreground_size.height));
if zoom <= 0. { if zoom <= 0. {
return output; return output;
+48 -70
View File
@@ -1,8 +1,9 @@
//! Tile-based render caching for efficient viewport panning. //! Tile-based render caching for efficient viewport panning.
use core_types::gpoll::Interrupt;
use core_types::math::bbox::AxisAlignedBbox; use core_types::math::bbox::AxisAlignedBbox;
use core_types::transform::{Footprint, RenderQuality, Transform}; use core_types::transform::{Footprint, RenderQuality, Transform};
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl}; use core_types::{Ctx, DeriveCtx, ExtractAll};
use glam::{DAffine2, DVec2, IVec2, UVec2}; use glam::{DAffine2, DVec2, IVec2, UVec2};
use graph_craft::application_io::PlatformEditorApi; use graph_craft::application_io::PlatformEditorApi;
use graph_craft::document::value::{RenderOutput, RenderOutputType}; use graph_craft::document::value::{RenderOutput, RenderOutputType};
@@ -11,7 +12,6 @@ use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams};
use std::collections::HashSet; use std::collections::HashSet;
use std::hash::Hash; use std::hash::Hash;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use wgpu_executor::WgpuExecutor;
pub const TILE_SIZE: u32 = 256; pub const TILE_SIZE: u32 = 256;
pub const MAX_CACHE_MEMORY_BYTES: usize = 512 * 1024 * 1024; pub const MAX_CACHE_MEMORY_BYTES: usize = 512 * 1024 * 1024;
@@ -321,25 +321,23 @@ fn flood_fill(start: &TileCoord, tile_set: &HashSet<TileCoord>, visited: &mut Ha
} }
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
pub async fn render_output_cache<'a: 'n>( pub fn render_output_cache(
ctx: impl Ctx + ExtractAll + CloneVarArgs + ExtractRealTime + ExtractAnimationTime + ExtractPointerPosition + Sync, ctx: impl Ctx + ExtractAll + DeriveCtx,
#[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<&'a WgpuExecutor>, #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<wgpu_executor::WgpuExecutorHandle>,
#[scope(crate::platform_application_io::editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi, #[scope(crate::platform_application_io::editor_api::IDENTIFIER)] editor_api: std::sync::Arc<PlatformEditorApi>,
data: impl Node<Context<'static>, Output = RenderOutput> + Send + Sync, data: impl Node<Context<'_>, Output = RenderOutput>,
#[data] tile_cache: TileCache, #[data] tile_cache: TileCache,
) -> RenderOutput { ) -> Result<RenderOutput, Interrupt> {
let footprint = ctx.footprint(); let footprint = *ctx.footprint();
let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::<RenderParams>()) else { let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::<RenderParams>()) else {
log::warn!("render_output_cache: missing or invalid render params, falling back to direct render"); log::warn!("render_output_cache: missing or invalid render params, falling back to direct render");
let context = OwnedContextImpl::from(ctx.clone()).with_footprint(*footprint); return data.eval(&ctx.derived());
return data.eval(context.into_context()).await;
}; };
// Fall back to direct render for non-Vello or zero-size viewports // Fall back to direct render for non-Vello or zero-size viewports
let physical_resolution = footprint.resolution; let physical_resolution = footprint.resolution;
if !matches!(render_params.render_output_type, RenderOutputTypeRequest::Vello) || physical_resolution.x == 0 || physical_resolution.y == 0 { if !matches!(render_params.render_output_type, RenderOutputTypeRequest::Vello) || physical_resolution.x == 0 || physical_resolution.y == 0 {
let context = OwnedContextImpl::from(ctx.clone()).with_footprint(*footprint).with_vararg(Box::new(render_params.clone())); return data.eval(&ctx.derived());
return data.eval(context.into_context()).await;
} }
let zoom = footprint.scale_magnitudes().x; let zoom = footprint.scale_magnitudes().x;
@@ -375,8 +373,38 @@ pub async fn render_output_cache<'a: 'n>(
if missing_region.tiles.is_empty() { if missing_region.tiles.is_empty() {
continue; continue;
} }
let region = render_missing_region(missing_region, |ctx| data.eval(ctx), ctx.clone(), render_params, &footprint.transform, &device_origin_offset).await; let min_tile = missing_region.tiles.iter().fold(IVec2::new(i32::MAX, i32::MAX), |acc, t| acc.min(IVec2::new(t.x, t.y)));
new_regions.push(region); let max_tile = missing_region.tiles.iter().fold(IVec2::new(i32::MIN, i32::MIN), |acc, t| acc.max(IVec2::new(t.x, t.y)));
let tile_count = (max_tile - min_tile) + IVec2::ONE;
let region_pixel_size = (tile_count * TILE_SIZE as i32).as_uvec2();
let tile_global_offset = min_tile.as_dvec2() * TILE_SIZE as f64 + device_origin_offset;
let region_transform = DAffine2::from_translation(-tile_global_offset) * footprint.transform;
let region_footprint = Footprint {
transform: region_transform,
resolution: region_pixel_size,
quality: RenderQuality::Full,
};
let mut result = data.eval(&ctx.with_footprint(&region_footprint))?;
let RenderOutputType::Texture(texture) = result.data else {
unreachable!("render_output_cache: expected texture output from Vello render");
};
result.metadata.apply_transform(region_transform.inverse());
let memory_size = (region_pixel_size.x * region_pixel_size.y) as usize * BYTES_PER_PIXEL;
new_regions.push(CachedRegion {
texture,
texture_size: region_pixel_size,
tiles: missing_region.tiles.clone(),
metadata: result.metadata,
last_access: 0,
memory_size,
});
} }
tile_cache.store_regions(new_regions.clone()); tile_cache.store_regions(new_regions.clone());
@@ -385,68 +413,18 @@ pub async fn render_output_cache<'a: 'n>(
// If no regions, fall back to direct render // If no regions, fall back to direct render
if all_regions.is_empty() { if all_regions.is_empty() {
let context = OwnedContextImpl::from(ctx.clone()).with_footprint(*footprint).with_vararg(Box::new(render_params.clone())); return data.eval(&ctx.derived());
return data.eval(context.into_context()).await;
} }
let executor = executor.expect("GPU executor not available"); let executor = executor.expect("GPU executor not available");
let output_texture = executor.request_texture(physical_resolution).await; let output_texture = executor.request_texture(physical_resolution);
let combined_metadata = composite_cached_regions(&all_regions, &output_texture, &device_origin_offset, &footprint.transform, executor); let combined_metadata = composite_cached_regions(&all_regions, &output_texture, &device_origin_offset, &footprint.transform, &executor);
RenderOutput { Ok(RenderOutput {
data: RenderOutputType::Texture(output_texture), data: RenderOutputType::Texture(output_texture),
metadata: combined_metadata, metadata: combined_metadata,
} })
}
async fn render_missing_region<F, Fut>(
region: &RenderRegion,
render_fn: F,
ctx: impl Ctx + ExtractAll + CloneVarArgs,
render_params: &RenderParams,
viewport_transform: &DAffine2,
viewport_origin_offset: &DVec2,
) -> CachedRegion
where
F: Fn(Context<'static>) -> Fut,
Fut: std::future::Future<Output = RenderOutput>,
{
let min_tile = region.tiles.iter().fold(IVec2::new(i32::MAX, i32::MAX), |acc, t| acc.min(IVec2::new(t.x, t.y)));
let max_tile = region.tiles.iter().fold(IVec2::new(i32::MIN, i32::MIN), |acc, t| acc.max(IVec2::new(t.x, t.y)));
let tile_count = (max_tile - min_tile) + IVec2::ONE;
let region_pixel_size = (tile_count * TILE_SIZE as i32).as_uvec2();
let tile_global_offset = min_tile.as_dvec2() * TILE_SIZE as f64 + *viewport_origin_offset;
let region_transform = DAffine2::from_translation(-tile_global_offset) * *viewport_transform;
let region_footprint = Footprint {
transform: region_transform,
resolution: region_pixel_size,
quality: RenderQuality::Full,
};
let region_params = render_params.clone();
let region_ctx = OwnedContextImpl::from(ctx).with_footprint(region_footprint).with_vararg(Box::new(region_params)).into_context();
let mut result = render_fn(region_ctx).await;
let RenderOutputType::Texture(texture) = result.data else {
unreachable!("render_missing_region: expected texture output from Vello render");
};
let pixel_to_document = region_transform.inverse();
result.metadata.apply_transform(pixel_to_document);
let memory_size = (region_pixel_size.x * region_pixel_size.y) as usize * BYTES_PER_PIXEL;
CachedRegion {
texture,
texture_size: region_pixel_size,
tiles: region.tiles.clone(),
metadata: result.metadata,
last_access: 0,
memory_size,
}
} }
fn composite_cached_regions( fn composite_cached_regions(
+102 -30
View File
@@ -1,7 +1,7 @@
use core_types::gpoll::Interrupt;
use core_types::list::List; use core_types::list::List;
use core_types::transform::{Footprint, Transform}; use core_types::transform::{Footprint, Transform};
use core_types::{CloneVarArgs, ExtractAll, ExtractVarArgs}; use core_types::{Color, Context, Ctx, DeriveCtx, ExtractFootprint, ExtractVarArgs, VarArgLink, VarArgSlots, WasmNotSend};
use core_types::{Color, Context, Ctx, ExtractFootprint, OwnedContextImpl, WasmNotSend};
use graph_craft::document::value::{RenderOutput, RenderOutputType}; use graph_craft::document::value::{RenderOutput, RenderOutputType};
use graphene_application_io::{ExportFormat, RenderConfig}; use graphene_application_io::{ExportFormat, RenderConfig};
use graphic_types::raster_types::{CPU, Raster}; use graphic_types::raster_types::{CPU, Raster};
@@ -9,7 +9,7 @@ use graphic_types::{Artboard, Graphic, Vector};
use rendering::{Render, RenderMetadata, RenderOutputType as RenderOutputTypeRequest, RenderParams, SvgRender, SvgRenderOutput}; use rendering::{Render, RenderMetadata, RenderOutputType as RenderOutputTypeRequest, RenderParams, SvgRender, SvgRenderOutput};
use std::sync::Arc; use std::sync::Arc;
use vector_types::GradientStops; use vector_types::GradientStops;
use wgpu_executor::{RenderContext, WgpuExecutor}; use wgpu_executor::RenderContext;
#[derive(Clone, dyn_any::DynAny)] #[derive(Clone, dyn_any::DynAny)]
pub enum RenderIntermediateType { pub enum RenderIntermediateType {
@@ -23,8 +23,8 @@ pub struct RenderIntermediate {
} }
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + Sync>( fn render_intermediate<T: 'static + Render + WasmNotSend + Send + Sync>(
ctx: impl Ctx + ExtractVarArgs + ExtractAll + CloneVarArgs, ctx: impl Ctx + ExtractVarArgs + DeriveCtx,
#[implementations( #[implementations(
Context -> List<Artboard>, Context -> List<Artboard>,
Context -> List<Graphic>, Context -> List<Graphic>,
@@ -34,21 +34,19 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
Context -> List<GradientStops>, Context -> List<GradientStops>,
Context -> List<String>, Context -> List<String>,
)] )]
data: impl Node<Context<'static>, Output = T>, data: impl Node<Context<'_>, Output = T>,
) -> RenderIntermediate { ) -> Result<RenderIntermediate, Interrupt> {
let data = data.eval(&ctx.derived())?;
let render_params = ctx let render_params = ctx
.vararg(0) .vararg(0)
.expect("Did not find var args") .expect("Did not find var args")
.downcast_ref::<RenderParams>() .downcast_ref::<RenderParams>()
.expect("Downcasting render params yielded invalid type"); .expect("Downcasting render params yielded invalid type");
let ctx = OwnedContextImpl::from(ctx.clone()).into_context();
let data = data.eval(ctx).await;
let footprint = Footprint::default(); let footprint = Footprint::default();
let mut metadata = RenderMetadata::default(); let mut metadata = RenderMetadata::default();
data.collect_metadata(&mut metadata, footprint, None); data.collect_metadata(&mut metadata, footprint, None);
match &render_params.render_output_type { Ok(match &render_params.render_output_type {
RenderOutputTypeRequest::Vello => { RenderOutputTypeRequest::Vello => {
let mut scene = vello::Scene::new(); let mut scene = vello::Scene::new();
@@ -70,13 +68,13 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
metadata, metadata,
} }
} }
} })
} }
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn render<'a: 'n>( fn render(
ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, ctx: impl Ctx + ExtractFootprint + ExtractVarArgs,
#[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<&'a WgpuExecutor>, #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<wgpu_executor::WgpuExecutorHandle>,
data: RenderIntermediate, data: RenderIntermediate,
) -> RenderOutput { ) -> RenderOutput {
let footprint = ctx.footprint(); let footprint = ctx.footprint();
@@ -133,7 +131,6 @@ async fn render<'a: 'n>(
let texture = executor let texture = executor
.expect("GPU executor not available") .expect("GPU executor not available")
.render_vello_scene(&transformed_scene, footprint.resolution, context, None) .render_vello_scene(&transformed_scene, footprint.resolution, context, None)
.await
.expect("Failed to render Vello scene"); .expect("Failed to render Vello scene");
RenderOutputType::Texture(texture) RenderOutputType::Texture(texture)
} }
@@ -144,11 +141,13 @@ async fn render<'a: 'n>(
} }
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn create_context<'a: 'n>( fn create_context(ctx: impl Ctx + ExtractVarArgs + DeriveCtx, data: impl Node<Context<'_>, Output = RenderOutput>) -> Result<RenderOutput, Interrupt> {
// Context injections are defined in the wrap_network_in_scope function let render_config = *ctx
render_config: RenderConfig, .vararg(0)
data: impl Node<Context<'static>, Output = RenderOutput>, .expect("Did not find var args")
) -> RenderOutput { .downcast_ref::<RenderConfig>()
.expect("Downcasting render config yielded invalid type");
let render_output_type = match render_config.export_format { let render_output_type = match render_config.export_format {
ExportFormat::Svg => RenderOutputTypeRequest::Svg, ExportFormat::Svg => RenderOutputTypeRequest::Svg,
ExportFormat::Raster => RenderOutputTypeRequest::Vello, ExportFormat::Raster => RenderOutputTypeRequest::Vello,
@@ -169,16 +168,89 @@ async fn create_context<'a: 'n>(
..Default::default() ..Default::default()
}; };
let ctx = OwnedContextImpl::default() let scope = ctx
.with_footprint(footprint) .scope()
.with_real_time(render_config.time.time) .with_real_time(Some(render_config.time.time))
.with_animation_time(render_config.time.animation_time.as_secs_f64()) .with_animation_time(Some(render_config.time.animation_time.as_secs_f64()))
.with_pointer_position(render_config.pointer) .with_pointer_position(Some(render_config.pointer));
.with_vararg(Box::new(render_params)) let varargs = VarArgLink {
.into_context(); args: VarArgSlots::Single(&render_params),
outer: None,
let mut result = data.eval(ctx).await; };
let scoped = ctx.with_scope(&scope);
let with_params = scoped.with_varargs(&varargs);
let mut result = data.eval(&with_params.with_footprint(&footprint))?;
result.metadata.apply_transform(glam::DAffine2::from_scale(glam::DVec2::splat(1. / render_config.scale))); result.metadata.apply_transform(glam::DAffine2::from_scale(glam::DVec2::splat(1. / render_config.scale)));
result Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use core_types::arena::Arena;
use core_types::context::{ContextImpl, EvalScope, VarArgsResult};
use core_types::gpoll::GPoll;
use core_types::node::Node;
use core_types::{ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime};
use graphene_application_io::TimingInformation;
struct ProbeNode;
impl<'a> Node<ContextImpl<'a>> for ProbeNode {
type Output = RenderOutput;
fn eval(&self, ctx: &ContextImpl<'a>) -> GPoll<RenderOutput> {
let render_params = ctx.vararg(0).unwrap().downcast_ref::<RenderParams>().expect("the vararg chain must start with RenderParams");
assert_eq!(render_params.scale, 2.0);
assert!(matches!(ctx.vararg(1), Err(VarArgsResult::IndexOutOfBounds)), "the RenderConfig must not leak downstream");
assert_eq!(ctx.footprint().transform, glam::DAffine2::from_scale(glam::DVec2::splat(2.0)) * Footprint::DEFAULT.transform);
assert_eq!(ctx.try_real_time(), Some(1.5));
assert_eq!(ctx.try_animation_time(), Some(2.0));
assert_eq!(ctx.try_pointer_position(), Some(glam::DVec2::new(3.0, 4.0)));
GPoll::Final(RenderOutput {
data: RenderOutputType::Buffer {
data: Vec::new(),
width: 0,
height: 0,
},
metadata: RenderMetadata::default(),
})
}
}
#[test]
fn create_context_builds_the_render_context_from_the_root_vararg() {
let arena = Arena::new(256);
let generations = [];
let scope = EvalScope::new(None, None, None, &generations, &arena);
let root = ContextImpl::root(&scope);
let render_config = RenderConfig {
scale: 2.0,
time: TimingInformation {
time: 1.5,
animation_time: std::time::Duration::from_secs(2),
},
pointer: glam::DVec2::new(3.0, 4.0),
..Default::default()
};
let varargs = VarArgLink {
args: VarArgSlots::Single(&render_config),
outer: None,
};
let ctx = root.with_varargs(&varargs);
let graph = CreateContextNode::new(ProbeNode);
let GPoll::Final(result) = <CreateContextNode<ProbeNode> as Node<ContextImpl>>::eval(&graph, &ctx) else {
panic!("create_context must complete synchronously");
};
assert_eq!(
result.data,
RenderOutputType::Buffer {
data: Vec::new(),
width: 0,
height: 0
}
);
}
} }
@@ -1,22 +1,22 @@
use core_types::gpoll::Interrupt;
use core_types::transform::{Footprint, Transform}; use core_types::transform::{Footprint, Transform};
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, OwnedContextImpl}; use core_types::{Ctx, DeriveCtx, ExtractAll};
use glam::{DAffine2, DVec2, UVec2, Vec2}; use glam::{DAffine2, DVec2, UVec2, Vec2};
use graph_craft::document::value::{RenderOutput, RenderOutputType}; use graph_craft::document::value::{RenderOutput, RenderOutputType};
use graphic_types::raster_types::Texture; use graphic_types::raster_types::Texture;
use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams}; use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams};
use vector_types::vector::style::RenderMode; use vector_types::vector::style::RenderMode;
use wgpu_executor::{AsyncWgpuPipeline, WgpuExecutor, WgpuPipelineCache}; use wgpu_executor::{WgpuExecutor, WgpuPipeline, WgpuPipelineCache};
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
pub async fn render_pixel_preview<'a: 'n>( pub fn render_pixel_preview(
ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync, ctx: impl Ctx + ExtractAll + DeriveCtx,
#[scope(pixel_preview_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache, #[scope(pixel_preview_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache,
data: impl Node<Context<'static>, Output = RenderOutput> + Send + Sync, data: impl Node<Context<'_>, Output = RenderOutput>,
) -> RenderOutput { ) -> Result<RenderOutput, Interrupt> {
let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::<RenderParams>()).cloned() else { let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::<RenderParams>()).cloned() else {
log::error!("invalid render params for pixel preview"); log::error!("invalid render params for pixel preview");
let context = OwnedContextImpl::from(ctx).into_context(); return data.eval(&ctx.derived());
return data.eval(context).await;
}; };
let physical_scale = render_params.scale; let physical_scale = render_params.scale;
@@ -24,8 +24,7 @@ pub async fn render_pixel_preview<'a: 'n>(
let viewport_zoom = footprint.scale_magnitudes().x; let viewport_zoom = footprint.scale_magnitudes().x;
if render_params.render_mode != RenderMode::PixelPreview || !matches!(render_params.render_output_type, RenderOutputTypeRequest::Vello) || viewport_zoom <= 1. { if render_params.render_mode != RenderMode::PixelPreview || !matches!(render_params.render_output_type, RenderOutputTypeRequest::Vello) || viewport_zoom <= 1. {
let context = OwnedContextImpl::from(ctx).into_context(); return data.eval(&ctx.derived());
return data.eval(context).await;
} }
let physical_resolution = footprint.resolution; let physical_resolution = footprint.resolution;
@@ -51,33 +50,31 @@ pub async fn render_pixel_preview<'a: 'n>(
quality: footprint.quality, quality: footprint.quality,
}; };
let new_ctx = OwnedContextImpl::from(ctx).with_footprint(upstream_footprint).with_vararg(Box::new(render_params)).into_context(); let scoped = ctx.push_vararg(&render_params);
let mut result = data.eval(new_ctx).await; let mut result = data.eval(&scoped.ctx().with_footprint(&upstream_footprint))?;
let RenderOutputType::Texture(ref source_texture) = result.data else { return result }; let RenderOutputType::Texture(ref source_texture) = result.data else { return Ok(result) };
let logical_transform = DAffine2::from_scale(DVec2::splat(1. / physical_scale)) * footprint.transform; let logical_transform = DAffine2::from_scale(DVec2::splat(1. / physical_scale)) * footprint.transform;
let transform = DAffine2::from_translation(-upstream_min) * logical_transform.inverse() * DAffine2::from_scale(logical_resolution); let transform = DAffine2::from_translation(-upstream_min) * logical_transform.inverse() * DAffine2::from_scale(logical_resolution);
let resampled = pipeline let resampled = pipeline.run::<PixelPreview>(&PixelPreviewArgs {
.run::<PixelPreview>(&PixelPreviewArgs { source: source_texture.as_ref(),
source: source_texture.as_ref(), transform: &transform,
transform: &transform, size: physical_resolution,
size: physical_resolution, });
})
.await;
result.data = RenderOutputType::Texture(resampled); result.data = RenderOutputType::Texture(resampled);
result.metadata.apply_transform(footprint.transform * DAffine2::from_translation(upstream_min)); result.metadata.apply_transform(footprint.transform * DAffine2::from_translation(upstream_min));
result Ok(result)
} }
#[node_macro::node(category(""), inject_scope)] #[node_macro::node(category(""), inject_scope)]
async fn pixel_preview_pipeline<'a: 'n>( fn pixel_preview_pipeline(
_ctx: impl Ctx, _ctx: impl Ctx,
#[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<&'a WgpuExecutor>, #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<wgpu_executor::WgpuExecutorHandle>,
#[data] pipeline: WgpuPipelineCache, #[data] pipeline: WgpuPipelineCache,
) -> WgpuPipelineCache { ) -> WgpuPipelineCache {
if let Some(executor) = executor { if let Some(executor) = executor {
@@ -97,7 +94,7 @@ pub struct PixelPreviewArgs<'a> {
size: UVec2, size: UVec2,
} }
impl AsyncWgpuPipeline for PixelPreview { impl WgpuPipeline for PixelPreview {
type Args<'a> = PixelPreviewArgs<'a>; type Args<'a> = PixelPreviewArgs<'a>;
type Out = Texture; type Out = Texture;
@@ -169,11 +166,11 @@ impl AsyncWgpuPipeline for PixelPreview {
PixelPreview { pipeline, bind_group_layout } PixelPreview { pipeline, bind_group_layout }
} }
async fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> Self::Out { fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> Self::Out {
let context = &executor.context(); let context = &executor.context();
let &PixelPreviewArgs { source, transform, size } = args; let &PixelPreviewArgs { source, transform, size } = args;
let output = executor.request_texture(size).await; let output = executor.request_texture(size);
let source_view = source.create_view(&wgpu::TextureViewDescriptor::default()); let source_view = source.create_view(&wgpu::TextureViewDescriptor::default());
let output_view = output.create_view(&wgpu::TextureViewDescriptor::default()); let output_view = output.create_view(&wgpu::TextureViewDescriptor::default());
+11
View File
@@ -0,0 +1,11 @@
pub use core_types::runtime::*;
use crate::platform_application_io::editor_api;
use core_types::Ctx;
use graph_craft::application_io::PlatformEditorApi;
use std::sync::Arc;
#[node_macro::node(category(""), inject_scope)]
pub fn runtime(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Arc<PlatformEditorApi>) -> RuntimeHandle {
editor_api.runtime.clone()
}
+228 -14
View File
@@ -1,4 +1,5 @@
use core_types::Context; use core_types::Context;
use core_types::gpoll::GPoll;
use core_types::list::List; use core_types::list::List;
use core_types::registry::types::{Fraction, Percentage, PixelSize}; use core_types::registry::types::{Fraction, Percentage, PixelSize};
use core_types::transform::Footprint; use core_types::transform::Footprint;
@@ -740,7 +741,7 @@ fn logical_not(
/// Evaluates either the "If True" or "If False" input branch based on whether the input condition is true or false. /// Evaluates either the "If True" or "If False" input branch based on whether the input condition is true or false.
#[node_macro::node(category("Math: Logic"))] #[node_macro::node(category("Math: Logic"))]
async fn switch<T, C: Send + 'n + Clone>( fn switch<T, C>(
#[implementations(Context)] ctx: C, #[implementations(Context)] ctx: C,
condition: bool, condition: bool,
#[expose] #[expose]
@@ -781,8 +782,8 @@ async fn switch<T, C: Send + 'n + Clone>(
Context -> List<GradientStops>, Context -> List<GradientStops>,
)] )]
if_false: impl Node<C, Output = T>, if_false: impl Node<C, Output = T>,
) -> T { ) -> GPoll<T> {
if condition { if_true.eval(ctx).await } else { if_false.eval(ctx).await } if condition { if_true.eval(ctx) } else { if_false.eval(ctx) }
} }
/// Constructs a bool value which may be set to true or false. /// Constructs a bool value which may be set to true or false.
@@ -995,36 +996,36 @@ mod test {
pub fn dot_product_function() { pub fn dot_product_function() {
let vector_a = DVec2::new(1., 2.); let vector_a = DVec2::new(1., 2.);
let vector_b = DVec2::new(3., 4.); let vector_b = DVec2::new(3., 4.);
assert_eq!(dot_product((), vector_a, vector_b, false), 11.); assert_eq!(dot_product(&(), vector_a, vector_b, false), 11.);
} }
#[test] #[test]
pub fn length_function() { pub fn length_function() {
let vector = DVec2::new(3., 4.); let vector = DVec2::new(3., 4.);
assert_eq!(length((), vector), 5.); assert_eq!(length(&(), vector), 5.);
} }
#[test] #[test]
fn test_basic_expression() { fn test_basic_expression() {
let result = math((), 0., "2 + 2".to_string(), 0.); let result = math(&(), 0., "2 + 2".to_string(), 0.);
assert_eq!(result, 4.); assert_eq!(result, 4.);
} }
#[test] #[test]
fn test_complex_expression() { fn test_complex_expression() {
let result = math((), 0., "(5 * 3) + (10 / 2)".to_string(), 0.); let result = math(&(), 0., "(5 * 3) + (10 / 2)".to_string(), 0.);
assert_eq!(result, 20.); assert_eq!(result, 20.);
} }
#[test] #[test]
fn test_default_expression() { fn test_default_expression() {
let result = math((), 0., "0".to_string(), 0.); let result = math(&(), 0., "0".to_string(), 0.);
assert_eq!(result, 0.); assert_eq!(result, 0.);
} }
#[test] #[test]
fn test_invalid_expression() { fn test_invalid_expression() {
let result = math((), 0., "invalid".to_string(), 0.); let result = math(&(), 0., "invalid".to_string(), 0.);
assert_eq!(result, 0.); assert_eq!(result, 0.);
} }
@@ -1036,26 +1037,239 @@ mod test {
#[test] #[test]
pub fn add_vectors() { pub fn add_vectors() {
assert_eq!(super::add((), DVec2::ONE, DVec2::ONE), DVec2::ONE * 2.); assert_eq!(super::add(&(), DVec2::ONE, DVec2::ONE), DVec2::ONE * 2.);
} }
#[test] #[test]
pub fn subtract_f64() { pub fn subtract_f64() {
assert_eq!(super::subtract((), 5_f64, 3_f64), 2.); assert_eq!(super::subtract(&(), 5_f64, 3_f64), 2.);
} }
#[test] #[test]
pub fn divide_vectors() { pub fn divide_vectors() {
assert_eq!(super::divide((), DVec2::ONE, 2_f64), DVec2::ONE / 2.); assert_eq!(super::divide(&(), DVec2::ONE, 2_f64), DVec2::ONE / 2.);
} }
#[test] #[test]
pub fn modulo_positive() { pub fn modulo_positive() {
assert_eq!(super::modulo((), -5_f64, 2_f64, true), 1_f64); assert_eq!(super::modulo(&(), -5_f64, 2_f64, true), 1_f64);
} }
#[test] #[test]
pub fn modulo_negative() { pub fn modulo_negative() {
assert_eq!(super::modulo((), -5_f64, 2_f64, false), -1_f64); assert_eq!(super::modulo(&(), -5_f64, 2_f64, false), -1_f64);
}
}
#[cfg(test)]
mod graphene_test {
use super::*;
use core_types::arena::Arena;
use core_types::context::{ContextImpl, EvalScope, ExtractIndex};
use core_types::gpoll::{Finality, GPoll};
use core_types::node::{BatchStatus, Node};
use core_types::registry::{EdgeHandle, ErasedNode, construct};
use std::mem::MaybeUninit;
use std::sync::Arc;
struct SourceNode<T>(T);
impl<T: Clone, Input> Node<Input> for SourceNode<T> {
type Output = T;
fn eval(&self, _input: &Input) -> GPoll<T> {
GPoll::Final(self.0.clone())
}
}
struct IndexNode;
impl<Input: ExtractIndex> Node<Input> for IndexNode {
type Output = f64;
fn eval(&self, input: &Input) -> GPoll<f64> {
GPoll::Final(input.innermost_index() as f64)
}
}
fn scope_fixture(arena: &Arena) -> EvalScope<'_> {
EvalScope::new(None, None, None, &[], arena)
}
#[test]
fn generated_add_evaluates_through_the_node_path() {
let arena = Arena::new(64);
let scope = scope_fixture(&arena);
let ctx = ContextImpl::root(&scope);
let graph = AddNode::new(SourceNode(1.0f64), SourceNode(2.0f64));
assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(3.0));
}
#[test]
fn generated_add_batches_through_the_erased_edge() {
let arena = Arena::new(64);
let scope = scope_fixture(&arena);
let ctx = ContextImpl::root(&scope);
let erased: Box<ErasedNode<f64>> = Box::new(AddNode::new(IndexNode, SourceNode(10.0f64)));
let mut scratch = [const { MaybeUninit::uninit() }; 4];
let status = erased.eval_batch(&ctx, 2..6, Some(&mut scratch));
let BatchStatus::Filled(lanes, finality) = status else {
panic!("expected filled, got {status:?}");
};
assert_eq!(lanes, &[12.0, 13.0, 14.0, 15.0]);
assert_eq!(finality, Finality::AllFinal);
}
#[test]
fn generated_wire_constructor_resolves_and_wires() {
let arena = Arena::new(64);
let scope = scope_fixture(&arena);
let ctx = ContextImpl::root(&scope);
let entries = logical_or_entries();
let value = EdgeHandle::new(Arc::new(SourceNode(true)) as Arc<ErasedNode<bool>>);
let other_value = EdgeHandle::new(Arc::new(SourceNode(false)) as Arc<ErasedNode<bool>>);
let wired = construct(&entries[0], vec![value, other_value]).unwrap().downcast::<bool>().unwrap();
assert_eq!(Node::eval(&wired, &ctx), GPoll::Final(true));
}
#[test]
fn ctor_registration_populates_the_node_registry() {
let registry = core_types::registry::NODE_REGISTRY.lock().unwrap();
let rows = registry
.iter()
.find_map(|(id, rows)| id.as_str().ends_with("::AddNode").then_some(rows))
.expect("AddNode rows registered at startup");
assert_eq!(rows.len(), 6);
}
#[test]
fn generic_add_registers_one_entry_per_implementation() {
let arena = Arena::new(64);
let scope = scope_fixture(&arena);
let ctx = ContextImpl::root(&scope);
let entries = add_entries();
assert_eq!(entries.len(), 6);
assert_eq!(entries[0].io.inputs, vec![core_types::concrete!(f64), core_types::concrete!(f64)]);
assert_eq!(entries[0].io.output, core_types::concrete!(f64));
assert_eq!(entries[3].io.inputs, vec![core_types::concrete!(DVec2), core_types::concrete!(DVec2)]);
assert_eq!(entries[3].io.output, core_types::concrete!(DVec2));
let augend = EdgeHandle::new(Arc::new(SourceNode(1.5f64)) as Arc<ErasedNode<f64>>);
let addend = EdgeHandle::new(Arc::new(SourceNode(2.5f64)) as Arc<ErasedNode<f64>>);
let wired = construct(&entries[0], vec![augend, addend]).unwrap().downcast::<f64>().unwrap();
assert_eq!(Node::eval(&wired, &ctx), GPoll::Final(4.0));
}
#[test]
fn converted_switch_evaluates_only_the_taken_branch() {
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
struct CountingSource(Arc<AtomicU32>, f64);
impl<Input> Node<Input> for CountingSource {
type Output = f64;
fn eval(&self, _input: &Input) -> GPoll<f64> {
self.0.fetch_add(1, Ordering::Relaxed);
GPoll::Final(self.1)
}
}
let arena = Arena::new(64);
let scope = scope_fixture(&arena);
let ctx = ContextImpl::root(&scope);
let taken = Arc::new(AtomicU32::new(0));
let untaken = Arc::new(AtomicU32::new(0));
let graph = SwitchNode::new(SourceNode(true), CountingSource(taken.clone(), 1.0), CountingSource(untaken.clone(), 2.0));
assert_eq!(Node::eval(&graph, &ctx), GPoll::Final(1.0));
assert_eq!(taken.load(Ordering::Relaxed), 1);
assert_eq!(untaken.load(Ordering::Relaxed), 0);
}
#[test]
fn converted_switch_passes_branch_status_through() {
struct PendingSource;
impl<Input> Node<Input> for PendingSource {
type Output = f64;
fn eval(&self, _input: &Input) -> GPoll<f64> {
GPoll::Pending
}
}
struct PartialSource;
impl<Input> Node<Input> for PartialSource {
type Output = f64;
fn eval(&self, _input: &Input) -> GPoll<f64> {
GPoll::Partial(7.0)
}
}
let arena = Arena::new(64);
let scope = scope_fixture(&arena);
let ctx = ContextImpl::root(&scope);
let pending = SwitchNode::new(SourceNode(true), PendingSource, PartialSource);
assert_eq!(Node::eval(&pending, &ctx), GPoll::Pending);
let partial = SwitchNode::new(SourceNode(false), PendingSource, PartialSource);
assert_eq!(Node::eval(&partial, &ctx), GPoll::Partial(7.0));
}
#[test]
fn converted_switch_merges_condition_status_into_the_branch_result() {
struct PartialCondition;
impl<Input> Node<Input> for PartialCondition {
type Output = bool;
fn eval(&self, _input: &Input) -> GPoll<bool> {
GPoll::Partial(true)
}
}
let arena = Arena::new(64);
let scope = scope_fixture(&arena);
let ctx = ContextImpl::root(&scope);
let graph = SwitchNode::new(PartialCondition, SourceNode(1.0f64), SourceNode(2.0f64));
assert_eq!(Node::eval(&graph, &ctx), GPoll::Partial(1.0));
}
#[test]
fn generated_eval_computes_on_stand_in_and_traces_fallback() {
struct FallbackNode;
impl<Input> Node<Input> for FallbackNode {
type Output = f64;
fn eval(&self, _input: &Input) -> GPoll<f64> {
GPoll::fallback(0.0, "upstream failed")
}
}
let arena = Arena::new(64);
let scope = scope_fixture(&arena);
let ctx = ContextImpl::root(&scope);
let graph = AddNode::new(FallbackNode, SourceNode(5.0f64));
let GPoll::Fallback(boxed) = Node::eval(&graph, &ctx) else {
panic!("fallback must propagate with the computed stand-in");
};
assert_eq!(boxed.0, 5.0);
assert!(boxed.1.kind == "upstream failed");
assert_eq!(boxed.1.trace, vec![0]);
} }
} }
+1 -1
View File
@@ -23,7 +23,7 @@ pub use vector_types::vector::misc::BooleanOperation;
/// Combines the geometric forms of one or more closed paths into a new vector path that results from cutting or joining the paths by the chosen method. /// Combines the geometric forms of one or more closed paths into a new vector path that results from cutting or joining the paths by the chosen method.
#[node_macro::node(category("Vector: Modifier"), memoize)] #[node_macro::node(category("Vector: Modifier"), memoize)]
async fn boolean_operation<I: graphic_types::IntoGraphicList>( fn boolean_operation<I: graphic_types::IntoGraphicList>(
_: impl Ctx, _: impl Ctx,
/// The `List` of vector paths to perform the boolean operation on. Nested `List`s are automatically flattened. /// The `List` of vector paths to perform the boolean operation on. Nested `List`s are automatically flattened.
#[implementations(List<Graphic>, List<Vector>)] #[implementations(List<Graphic>, List<Vector>)]
+1 -1
View File
@@ -8,7 +8,7 @@ use raster_types::{CPU, Raster};
use std::cmp::{max, min}; use std::cmp::{max, min};
#[node_macro::node(category("Raster: Filter"))] #[node_macro::node(category("Raster: Filter"))]
async fn dehaze(_: impl Ctx, image_frame: List<Raster<CPU>>, strength: Percentage) -> List<Raster<CPU>> { fn dehaze(_: impl Ctx, image_frame: List<Raster<CPU>>, strength: Percentage) -> List<Raster<CPU>> {
image_frame image_frame
.into_iter() .into_iter()
.map(|mut row| { .map(|mut row| {
+2 -2
View File
@@ -87,7 +87,7 @@ fn unpremultiply_gamma_to_linear(buffer: Image<PremultipliedGammaPixel>) -> Imag
/// Blurs the image with a Gaussian or box blur kernel filter. /// Blurs the image with a Gaussian or box blur kernel filter.
#[node_macro::node(category("Raster: Filter"))] #[node_macro::node(category("Raster: Filter"))]
async fn blur( fn blur(
_: impl Ctx, _: impl Ctx,
/// The image to be blurred. /// The image to be blurred.
image_frame: List<Raster<CPU>>, image_frame: List<Raster<CPU>>,
@@ -124,7 +124,7 @@ async fn blur(
/// Applies a median filter to reduce noise while preserving edges. /// Applies a median filter to reduce noise while preserving edges.
#[node_macro::node(category("Raster: Filter"))] #[node_macro::node(category("Raster: Filter"))]
async fn median_filter( fn median_filter(
_: impl Ctx, _: impl Ctx,
/// The image to be filtered. /// The image to be filtered.
image_frame: List<Raster<CPU>>, image_frame: List<Raster<CPU>>,
+1 -1
View File
@@ -10,7 +10,7 @@ use vector_types::GradientStops;
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27grdm%27%20%3D%20Gradient%20Map // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27grdm%27%20%3D%20Gradient%20Map
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Gradient%20settings%20(Photoshop%206.0) // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Gradient%20settings%20(Photoshop%206.0)
#[node_macro::node(category("Raster: Adjustment"))] #[node_macro::node(category("Raster: Adjustment"))]
async fn gradient_map<T: Adjust<Color>>( fn gradient_map<T: Adjust<Color>>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Raster<CPU>>, List<Raster<CPU>>,
@@ -4,7 +4,7 @@ use core_types::list::{Item, List};
use raster_types::{CPU, Raster}; use raster_types::{CPU, Raster};
#[node_macro::node(category("Color"))] #[node_macro::node(category("Color"))]
async fn image_color_palette( fn image_color_palette(
_: impl Ctx, _: impl Ctx,
image: List<Raster<CPU>>, image: List<Raster<CPU>>,
#[default(4)] #[default(4)]
+2 -2
View File
@@ -241,7 +241,7 @@ pub fn extend_image_to_bounds(_: impl Ctx, image: List<Raster<CPU>>, bounds: DAf
let image_data = &row.element().data; let image_data = &row.element().data;
let (image_width, image_height) = (row.element().width, row.element().height); let (image_width, image_height) = (row.element().width, row.element().height);
if image_width == 0 || image_height == 0 { if image_width == 0 || image_height == 0 {
return empty_image((), bounds, List::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap(); return empty_image(&(), bounds, List::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap();
} }
let orig_image_scale = DVec2::new(image_width as f64, image_height as f64); let orig_image_scale = DVec2::new(image_width as f64, image_height as f64);
@@ -290,7 +290,7 @@ pub fn empty_image(_: impl Ctx, transform: DAffine2, color: List<Color>) -> List
} }
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
pub fn image<'a: 'n>(_: impl Ctx, resource: Resource) -> List<Raster<CPU>> { pub fn image(_: impl Ctx, resource: Resource) -> List<Raster<CPU>> {
let image_data = resource.as_ref(); let image_data = resource.as_ref();
let Some(image) = ::image::load_from_memory(image_data).ok() else { let Some(image) = ::image::load_from_memory(image_data).ok() else {
+39 -182
View File
@@ -1,16 +1,17 @@
use crate::gcore::Context; use crate::gcore::Context;
use core::f64::consts::TAU; use core::f64::consts::TAU;
use core_types::gpoll::Interrupt;
use core_types::list::List; use core_types::list::List;
use core_types::registry::types::{Angle, PixelSize}; use core_types::registry::types::{Angle, PixelSize};
use core_types::{ATTR_TRANSFORM, CloneVarArgs, Color, Ctx, ExtractAll, InjectVarArgs, OwnedContextImpl}; use core_types::{ATTR_TRANSFORM, Color, Ctx, DeriveCtx, InjectVarArgs};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphic_types::{Graphic, Vector}; use graphic_types::{Graphic, Vector};
use raster_types::{CPU, Raster}; use raster_types::{CPU, Raster};
use vector_types::GradientStops; use vector_types::GradientStops;
#[node_macro::node(category("Repeat"))] #[node_macro::node(category("Repeat"))]
async fn repeat<T: Into<Graphic> + Default + Send + Clone + 'static>( fn repeat<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Ctx, ctx: impl Ctx + DeriveCtx,
#[implementations( #[implementations(
Context -> List<Graphic>, Context -> List<Graphic>,
Context -> List<Vector>, Context -> List<Vector>,
@@ -18,35 +19,35 @@ async fn repeat<T: Into<Graphic> + Default + Send + Clone + 'static>(
Context -> List<Color>, Context -> List<Color>,
Context -> List<GradientStops>, Context -> List<GradientStops>,
)] )]
content: impl Node<'n, Context<'static>, Output = List<T>>, content: impl Node<Context<'_>, Output = List<T>>,
#[default(1)] #[default(1)]
#[hard(1..)] #[hard(1..)]
count: u32, count: u32,
reverse: bool, reverse: bool,
) -> List<T> { ) -> Result<List<T>, Interrupt> {
// Someday this node can have the option to generate infinitely instead of a fixed count (basically `std::iter::repeat`). // Someday this node can have the option to generate infinitely instead of a fixed count (basically `std::iter::repeat`).
let count = count as usize; let count = count as u64;
let spilled = ctx.index_head();
let mut result_list = List::new(); let mut result_list = List::new();
for index in 0..count { for index in 0..count {
let index = if reverse { count - index - 1 } else { index }; let index = if reverse { count - index - 1 } else { index };
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index); let generated_content = content.eval(&ctx.promoted(&spilled, index))?;
let generated_content = content.eval(new_ctx.into_context()).await;
for generated_row in generated_content.into_iter() { for generated_row in generated_content.into_iter() {
result_list.push(generated_row); result_list.push(generated_row);
} }
} }
result_list Ok(result_list)
} }
#[node_macro::node(category("Repeat"))] #[node_macro::node(category("Repeat"))]
pub async fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>( pub fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Ctx, ctx: impl Ctx + DeriveCtx,
#[implementations( #[implementations(
Context -> List<Graphic>, Context -> List<Graphic>,
Context -> List<Vector>, Context -> List<Vector>,
@@ -54,7 +55,7 @@ pub async fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
Context -> List<Color>, Context -> List<Color>,
Context -> List<GradientStops>, Context -> List<GradientStops>,
)] )]
content: impl Node<'n, Context<'static>, Output = List<T>>, content: impl Node<Context<'_>, Output = List<T>>,
#[default(100., 100.)] #[default(100., 100.)]
// TODO: When using a custom Properties panel layout in document_node_definitions.rs and this default is set, the widget weirdly doesn't show up in the Properties panel. Investigation is needed. // TODO: When using a custom Properties panel layout in document_node_definitions.rs and this default is set, the widget weirdly doesn't show up in the Properties panel. Investigation is needed.
direction: PixelSize, direction: PixelSize,
@@ -62,10 +63,11 @@ pub async fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
#[default(5)] #[default(5)]
#[hard(1..)] #[hard(1..)]
count: u32, count: u32,
) -> List<T> { ) -> Result<List<T>, Interrupt> {
let angle = angle.to_radians(); let angle = angle.to_radians();
// A single copy has no steps between copies, so the denominator is kept at 1 to avoid `0. / 0.` producing a NaN transform // A single copy has no steps between copies, so the denominator is kept at 1 to avoid `0. / 0.` producing a NaN transform
let total = (count - 1).max(1) as f64; let total = (count - 1).max(1) as f64;
let spilled = ctx.index_head();
let mut result_list = List::new(); let mut result_list = List::new();
@@ -74,8 +76,7 @@ pub async fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
let translation = index as f64 * direction / total; let translation = index as f64 * direction / total;
let transform = DAffine2::from_angle(angle) * DAffine2::from_translation(translation); let transform = DAffine2::from_angle(angle) * DAffine2::from_translation(translation);
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index as usize); let generated_content = content.eval(&ctx.promoted(&spilled, index as u64))?;
let generated_content = content.eval(new_ctx.into_context()).await;
for row_index in 0..generated_content.len() { for row_index in 0..generated_content.len() {
let Some(mut row) = generated_content.clone_item(row_index) else { continue }; let Some(mut row) = generated_content.clone_item(row_index) else { continue };
@@ -89,12 +90,12 @@ pub async fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
} }
} }
result_list Ok(result_list)
} }
#[node_macro::node(category("Repeat"))] #[node_macro::node(category("Repeat"))]
async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>( fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Ctx, ctx: impl Ctx + DeriveCtx,
#[implementations( #[implementations(
Context -> List<Graphic>, Context -> List<Graphic>,
Context -> List<Vector>, Context -> List<Vector>,
@@ -102,7 +103,7 @@ async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
Context -> List<Color>, Context -> List<Color>,
Context -> List<GradientStops>, Context -> List<GradientStops>,
)] )]
content: impl Node<'n, Context<'static>, Output = List<T>>, content: impl Node<Context<'_>, Output = List<T>>,
start_angle: Angle, start_angle: Angle,
#[unit(" px")] #[unit(" px")]
#[default(5)] #[default(5)]
@@ -110,7 +111,8 @@ async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
#[default(5)] #[default(5)]
#[hard(1..)] #[hard(1..)]
count: u32, count: u32,
) -> List<T> { ) -> Result<List<T>, Interrupt> {
let spilled = ctx.index_head();
let mut result_list = List::new(); let mut result_list = List::new();
for index in 0..count { for index in 0..count {
@@ -118,8 +120,7 @@ async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
let translation = DAffine2::from_translation(radius * DVec2::Y); let translation = DAffine2::from_translation(radius * DVec2::Y);
let transform = angle * translation; let transform = angle * translation;
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index as usize); let generated_content = content.eval(&ctx.promoted(&spilled, index as u64))?;
let generated_content = content.eval(new_ctx.into_context()).await;
for row_index in 0..generated_content.len() { for row_index in 0..generated_content.len() {
let Some(mut row) = generated_content.clone_item(row_index) else { continue }; let Some(mut row) = generated_content.clone_item(row_index) else { continue };
@@ -133,12 +134,12 @@ async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
} }
} }
result_list Ok(result_list)
} }
#[node_macro::node(category("Repeat"), name("Repeat on Points"))] #[node_macro::node(category("Repeat"), name("Repeat on Points"))]
async fn repeat_on_points<T: Into<Graphic> + Default + Send + Clone + 'static>( fn repeat_on_points<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Sync + Ctx + InjectVarArgs, ctx: impl Ctx + DeriveCtx + InjectVarArgs,
points: List<Vector>, points: List<Vector>,
#[implementations( #[implementations(
Context -> List<Graphic>, Context -> List<Graphic>,
@@ -147,178 +148,34 @@ async fn repeat_on_points<T: Into<Graphic> + Default + Send + Clone + 'static>(
Context -> List<Color>, Context -> List<Color>,
Context -> List<GradientStops>, Context -> List<GradientStops>,
)] )]
content: impl Node<'n, Context<'static>, Output = List<T>>, content: impl Node<Context<'_>, Output = List<T>>,
reverse: bool, reverse: bool,
) -> List<T> { ) -> Result<List<T>, Interrupt> {
let spilled = ctx.index_head();
let mut result_list = List::new(); let mut result_list = List::new();
for points_index in 0..points.len() { for points_index in 0..points.len() {
let Some(points_element) = points.element(points_index) else { continue }; let Some(points_element) = points.element(points_index) else { continue };
let transform: DAffine2 = points.attribute_cloned_or_default(ATTR_TRANSFORM, points_index); let transform: DAffine2 = points.attribute_cloned_or_default(ATTR_TRANSFORM, points_index);
let mut iteration = async |index, point| { let positions = points_element.point_domain.positions();
let range: Box<dyn Iterator<Item = (usize, &DVec2)>> = match reverse {
true => Box::new(positions.iter().enumerate().rev()),
false => Box::new(positions.iter().enumerate()),
};
for (index, &point) in range {
let transformed_point = transform.transform_point2(point); let transformed_point = transform.transform_point2(point);
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index).with_position(transformed_point); let scoped = ctx.push_position(transformed_point);
let generated_content = content.eval(new_ctx.into_context()).await; let generated_content = content.eval(&scoped.ctx().promoted(&spilled, index as u64))?;
for mut generated_row in generated_content.into_iter() { for mut generated_row in generated_content.into_iter() {
generated_row.attribute_mut_or_insert_default::<DAffine2>(ATTR_TRANSFORM).translation = transformed_point; generated_row.attribute_mut_or_insert_default::<DAffine2>(ATTR_TRANSFORM).translation = transformed_point;
result_list.push(generated_row); result_list.push(generated_row);
} }
};
let range = points_element.point_domain.positions().iter().enumerate();
if reverse {
for (index, &point) in range.rev() {
iteration(index, point).await;
}
} else {
for (index, &point) in range {
iteration(index, point).await;
}
} }
} }
result_list Ok(result_list)
}
#[cfg(test)]
mod test {
use super::*;
use core_types::Ctx;
use core_types::Node;
use core_types::transform::Footprint;
use glam::DVec2;
use graphene_core::ReadPositionNode;
use graphene_core::extract_xy::{ExtractXyNode, XY};
use graphic_types::Vector;
use kurbo::Shape;
use kurbo::{BezPath, DEFAULT_ACCURACY, Rect};
use std::future::Future;
use std::pin::Pin;
use vector_nodes::generator_nodes::RectangleNode;
use vector_types::subpath::Subpath;
fn vector_node_from_bezpath(bezpath: BezPath) -> List<Vector> {
List::new_from_element(Vector::from_bezpath(bezpath))
}
#[derive(Clone)]
pub struct FutureWrapperNode<T: Clone>(T);
impl<'i, I: Ctx, T: 'i + Clone + Send> Node<'i, I> for FutureWrapperNode<T> {
type Output = Pin<Box<dyn Future<Output = T> + 'i + Send>>;
fn eval(&'i self, _input: I) -> Self::Output {
let value = self.0.clone();
Box::pin(async move { value })
}
}
#[tokio::test]
async fn repeat_on_points_test() {
let context = OwnedContextImpl::default().into_context();
let rect = RectangleNode::new(
FutureWrapperNode(()),
ExtractXyNode::new(ReadPositionNode::new(FutureWrapperNode(()), FutureWrapperNode(0)), FutureWrapperNode(XY::Y)),
FutureWrapperNode(2_f64),
FutureWrapperNode(false),
FutureWrapperNode(0_f64),
FutureWrapperNode(false),
);
let positions = [DVec2::new(40., 20.), DVec2::ONE, DVec2::new(-42., 9.), DVec2::new(10., 345.)];
let points = List::new_from_element(Vector::from_subpath(Subpath::from_anchors(positions, false)));
let generated = super::repeat_on_points(context, points, &rect, false).await;
assert_eq!(generated.len(), positions.len());
for (position, index) in positions.into_iter().zip(0..generated.len()) {
let bounds = generated
.element(index)
.unwrap()
.bounding_box_with_transform(generated.attribute_cloned_or_default(ATTR_TRANSFORM, index))
.unwrap();
assert!(position.abs_diff_eq((bounds[0] + bounds[1]) / 2., 1e-10));
assert_eq!((bounds[1] - bounds[0]).x, position.y);
}
}
#[tokio::test]
async fn repeat() {
let direction = DVec2::X * 1.5;
let count = 3;
let context = OwnedContextImpl::default().into_context();
let repeated = super::repeat_array(
context,
&FutureWrapperNode(vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY))),
direction,
0.,
count,
)
.await;
let vector_list = vector_nodes::flatten_path(Footprint::default(), repeated).await;
let vector = vector_list.element(0).unwrap();
assert_eq!(vector.region_manipulator_groups().count(), 3);
for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() {
assert!((manipulator_groups[0].anchor - direction * index as f64 / (count - 1) as f64).length() < 1e-5);
}
}
#[tokio::test]
async fn repeat_single_copy() {
let context = OwnedContextImpl::default().into_context();
let repeated = super::repeat_array(
context,
&FutureWrapperNode(vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY))),
DVec2::new(12., 10.),
45.,
1,
)
.await;
let vector_list = vector_nodes::flatten_path(Footprint::default(), repeated).await;
let vector = vector_list.element(0).unwrap();
assert_eq!(vector.region_manipulator_groups().count(), 1);
let (_, manipulator_groups) = vector.region_manipulator_groups().next().unwrap();
let anchor = manipulator_groups[0].anchor;
assert!(anchor.length() < 1e-5, "Expected the single copy to be untransformed, found anchor {anchor}");
}
#[tokio::test]
async fn repeat_transform_position() {
let direction = DVec2::new(12., 10.);
let count = 8;
let context = OwnedContextImpl::default().into_context();
let repeated = super::repeat_array(
context,
&FutureWrapperNode(vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY))),
direction,
0.,
count,
)
.await;
let vector_list = vector_nodes::flatten_path(Footprint::default(), repeated).await;
let vector = vector_list.element(0).unwrap();
assert_eq!(vector.region_manipulator_groups().count(), 8);
for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() {
assert!((manipulator_groups[0].anchor - direction * index as f64 / (count - 1) as f64).length() < 1e-5);
}
}
#[tokio::test]
async fn repeat_radial() {
let context = OwnedContextImpl::default().into_context();
let repeated = super::repeat_radial(context, &FutureWrapperNode(vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY))), 45., 4., 8).await;
let vector_list = vector_nodes::flatten_path(Footprint::default(), repeated).await;
let vector = vector_list.element(0).unwrap();
assert_eq!(vector.region_manipulator_groups().count(), 8);
for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() {
let expected_angle = (index as f64 + 1.) * 45.;
let center = (manipulator_groups[0].anchor + manipulator_groups[2].anchor) / 2.;
let actual_angle = DVec2::Y.angle_to(center).to_degrees();
assert!((actual_angle - expected_angle).abs() % 360. < 1e-5, "Expected {expected_angle} found {actual_angle}");
}
}
} }
+10 -9
View File
@@ -7,10 +7,11 @@ mod text_context;
mod to_path; mod to_path;
use convert_case::{Boundary, Converter, pattern}; use convert_case::{Boundary, Converter, pattern};
use core_types::gpoll::Interrupt;
use core_types::graphene_hash::CacheHash; use core_types::graphene_hash::CacheHash;
use core_types::list::{Item, List}; use core_types::list::{Item, List};
use core_types::registry::types::{SignedInteger, TextArea}; use core_types::registry::types::{SignedInteger, TextArea};
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractVarArgs, OwnedContextImpl}; use core_types::{Context, Ctx, DeriveCtx, ExtractVarArgs};
use dyn_any::DynAny; use dyn_any::DynAny;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
@@ -768,25 +769,25 @@ fn string_join(
/// Iterates over a list of strings, evaluating the mapped operation for each one. Use the **Read String** node to access the current string inside the loop. /// Iterates over a list of strings, evaluating the mapped operation for each one. Use the **Read String** node to access the current string inside the loop.
#[node_macro::node(category("Text"))] #[node_macro::node(category("Text"))]
async fn map_string( fn map_string(
ctx: impl Ctx + CloneVarArgs + ExtractAll, ctx: impl Ctx + DeriveCtx,
strings: List<String>, strings: List<String>,
#[expose] #[expose]
#[implementations(Context -> String)] #[implementations(Context -> String)]
mapped: impl Node<Context<'static>, Output = String>, mapped: impl Node<Context<'_>, Output = String>,
) -> List<String> { ) -> Result<List<String>, Interrupt> {
let spilled = ctx.index_head();
let mut result = List::new(); let mut result = List::new();
for (i, row) in strings.into_iter().enumerate() { for (i, row) in strings.into_iter().enumerate() {
let string = row.into_element(); let string = row.into_element();
let owned_ctx = OwnedContextImpl::from(ctx.clone()); let scoped = ctx.push_vararg(&string);
let owned_ctx = owned_ctx.with_vararg(Box::new(string)).with_index(i); let mapped_string = mapped.eval(&scoped.ctx().promoted(&spilled, i as u64))?;
let mapped_string = mapped.eval(owned_ctx.into_context()).await;
result.push(Item::new_from_element(mapped_string)); result.push(Item::new_from_element(mapped_string));
} }
result Ok(result)
} }
/// Reads the current string from within a **Map String** node's loop. /// Reads the current string from within a **Map String** node's loop.
@@ -1,8 +1,9 @@
use core::f64; use core::f64;
use core_types::color::Color; use core_types::color::Color;
use core_types::gpoll::Interrupt;
use core_types::list::{List, ListDyn}; use core_types::list::{List, ListDyn};
use core_types::transform::{ApplyTransform, ScaleType, Transform}; use core_types::transform::{ApplyTransform, ScaleType, Transform};
use core_types::{ATTR_TRANSFORM, CloneVarArgs, Context, Ctx, ExtractAll, InjectFootprint, ModifyFootprint, OwnedContextImpl}; use core_types::{ATTR_TRANSFORM, Context, Ctx, DeriveCtx, InjectFootprint, ModifyFootprint};
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use graphic_types::Graphic; use graphic_types::Graphic;
use graphic_types::Vector; use graphic_types::Vector;
@@ -11,8 +12,8 @@ use vector_types::GradientStops;
/// Applies the specified transform to the input value, which may be a graphic type or another transform. /// Applies the specified transform to the input value, which may be a graphic type or another transform.
#[node_macro::node(category("Math: Transform"))] #[node_macro::node(category("Math: Transform"))]
async fn transform<T: ApplyTransform + 'n + 'static>( fn transform<T: ApplyTransform + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll + ModifyFootprint, ctx: impl Ctx + DeriveCtx + ModifyFootprint,
#[implementations( #[implementations(
Context -> DAffine2, Context -> DAffine2,
Context -> DVec2, Context -> DVec2,
@@ -24,31 +25,24 @@ async fn transform<T: ApplyTransform + 'n + 'static>(
Context -> List<Color>, Context -> List<Color>,
Context -> List<GradientStops>, Context -> List<GradientStops>,
)] )]
content: impl Node<Context<'static>, Output = T>, content: impl Node<Context<'_>, Output = T>,
#[widget(ParsedWidgetOverride::Custom = "transform_translation")] translation: DVec2, #[widget(ParsedWidgetOverride::Custom = "transform_translation")] translation: DVec2,
#[widget(ParsedWidgetOverride::Custom = "transform_rotation")] rotation: f64, #[widget(ParsedWidgetOverride::Custom = "transform_rotation")] rotation: f64,
#[widget(ParsedWidgetOverride::Custom = "transform_scale")] #[widget(ParsedWidgetOverride::Custom = "transform_scale")]
#[default(1., 1.)] #[default(1., 1.)]
scale: DVec2, scale: DVec2,
#[widget(ParsedWidgetOverride::Custom = "transform_skew")] skew: DVec2, #[widget(ParsedWidgetOverride::Custom = "transform_skew")] skew: DVec2,
) -> T { ) -> Result<T, Interrupt> {
let trs = DAffine2::from_scale_angle_translation(scale, rotation.to_radians(), translation); let trs = DAffine2::from_scale_angle_translation(scale, rotation.to_radians(), translation);
let skew = DAffine2::from_cols_array(&[1., skew.y.to_radians().tan(), skew.x.to_radians().tan(), 1., 0., 0.]); let skew = DAffine2::from_cols_array(&[1., skew.y.to_radians().tan(), skew.x.to_radians().tan(), 1., 0., 0.]);
let matrix = trs * skew; let matrix = trs * skew;
let footprint = ctx.try_footprint().copied(); let transformed = ctx.modify_footprint(|footprint| footprint.apply_transform(&matrix));
let mut transform_target = content.eval(&transformed.ctx())?;
let mut ctx = OwnedContextImpl::from(ctx);
if let Some(mut footprint) = footprint {
footprint.apply_transform(&matrix);
ctx = ctx.with_footprint(footprint);
}
let mut transform_target = content.eval(ctx.into_context()).await;
transform_target.left_apply_transform(&matrix); transform_target.left_apply_transform(&matrix);
transform_target Ok(transform_target)
} }
/// Resets the desired components of the input transform to their default values. If all components are reset, the output will be set to the identity transform. /// Resets the desired components of the input transform to their default values. If all components are reset, the output will be set to the identity transform.
@@ -114,7 +108,7 @@ fn replace_transform<T>(
// TODO: Figure out how this node should behave once #2982 is implemented. // TODO: Figure out how this node should behave once #2982 is implemented.
/// Obtains the transform of the first item in the input `List`, if present. /// Obtains the transform of the first item in the input `List`, if present.
#[node_macro::node(category("Math: Transform"), path(core_types::vector))] #[node_macro::node(category("Math: Transform"), path(core_types::vector))]
async fn extract_transform(_: impl Ctx, content: ListDyn) -> DAffine2 { fn extract_transform(_: impl Ctx, content: ListDyn) -> DAffine2 {
content.attribute::<DAffine2>(ATTR_TRANSFORM, 0).copied().unwrap_or_default() content.attribute::<DAffine2>(ATTR_TRANSFORM, 0).copied().unwrap_or_default()
} }
@@ -7,7 +7,7 @@ use vector_types::vector::VectorModification;
/// Applies a differential modification to a vector path, associating changes made by the Pen and Path tools to indices of edited points and segments. /// Applies a differential modification to a vector path, associating changes made by the Pen and Path tools to indices of edited points and segments.
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn path_modify(_ctx: impl Ctx, mut vector: List<Vector>, modification: Box<VectorModification>, node_path: List<NodeId>) -> List<Vector> { fn path_modify(_ctx: impl Ctx, mut vector: List<Vector>, modification: Box<VectorModification>, node_path: List<NodeId>) -> List<Vector> {
use core_types::list::Item; use core_types::list::Item;
if vector.is_empty() { if vector.is_empty() {
@@ -35,7 +35,7 @@ async fn path_modify(_ctx: impl Ctx, mut vector: List<Vector>, modification: Box
/// Applies the vector path's local transformation to its geometry and resets the transform to the identity. /// Applies the vector path's local transformation to its geometry and resets the transform to the identity.
#[node_macro::node(category("Vector"))] #[node_macro::node(category("Vector"))]
async fn apply_transform(_ctx: impl Ctx, mut vector: List<Vector>) -> List<Vector> { fn apply_transform(_ctx: impl Ctx, mut vector: List<Vector>) -> List<Vector> {
let (elements, transforms) = vector.element_and_attribute_slices_mut::<DAffine2>(ATTR_TRANSFORM); let (elements, transforms) = vector.element_and_attribute_slices_mut::<DAffine2>(ATTR_TRANSFORM);
for (element, transform) in elements.iter_mut().zip(transforms.iter_mut()) { for (element, transform) in elements.iter_mut().zip(transforms.iter_mut()) {
for (_, point) in element.point_domain.positions_mut() { for (_, point) in element.point_domain.positions_mut() {
+52 -53
View File
@@ -3,13 +3,14 @@ use core::f64::consts::{PI, TAU};
use core::hash::{Hash, Hasher}; use core::hash::{Hash, Hasher};
use core_types::blending::BlendMode; use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::gpoll::Interrupt;
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn}; use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn};
use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue}; use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue};
use core_types::transform::{Footprint, Transform}; use core_types::transform::{Footprint, Transform};
use core_types::uuid::NodeId; use core_types::uuid::NodeId;
use core_types::{ use core_types::{
ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, CloneVarArgs, ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color, Ctx,
Color, Context, Ctx, ExtractAll, OwnedContextImpl, DeriveCtx,
}; };
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use graphic_types::Vector; use graphic_types::Vector;
@@ -88,7 +89,7 @@ impl VectorListIterMut for List<Vector> {
/// Uniquely sets the fill and/or stroke style of every vector element to individual colors sampled along a chosen gradient. /// Uniquely sets the fill and/or stroke style of every vector element to individual colors sampled along a chosen gradient.
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector))] #[node_macro::node(category("Vector: Style"), path(graphene_core::vector))]
async fn assign_colors<T>( fn assign_colors<T>(
_: impl Ctx, _: impl Ctx,
/// The content with vector paths to apply the fill and/or stroke style to. /// The content with vector paths to apply the fill and/or stroke style to.
#[implementations(List<Graphic>, List<Vector>)] #[implementations(List<Graphic>, List<Vector>)]
@@ -115,7 +116,7 @@ async fn assign_colors<T>(
repeat_every: u32, repeat_every: u32,
) -> T ) -> T
where where
T: VectorListIterMut + 'n + Send, T: VectorListIterMut + Send,
{ {
let Some(row) = gradient.into_iter().next() else { return content }; let Some(row) = gradient.into_iter().next() else { return content };
@@ -156,7 +157,7 @@ where
/// Applies a fill style to the vector content, giving an appearance to the area within the interior of the geometry. /// Applies a fill style to the vector content, giving an appearance to the area within the interior of the geometry.
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))] #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))]
async fn fill<V: VectorListIterMut + 'n + Send, F: IntoGraphicList + 'n + Send + 'static>( fn fill<V: VectorListIterMut + Send, F: IntoGraphicList + Send + 'static>(
_: impl Ctx, _: impl Ctx,
/// The content with vector paths to apply the fill style to. /// The content with vector paths to apply the fill style to.
#[implementations( #[implementations(
@@ -251,7 +252,7 @@ impl IntoF64Vec for String {
/// Applies a stroke style to the vector content, giving an appearance to the area within the outline of the geometry. /// Applies a stroke style to the vector content, giving an appearance to the area within the outline of the geometry.
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))] #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))]
async fn stroke<V, L: IntoF64Vec, P: IntoGraphicList + 'n + Send + 'static>( fn stroke<V, L: IntoF64Vec, P: IntoGraphicList + Send + 'static>(
_: impl Ctx, _: impl Ctx,
/// The content with vector paths to apply the stroke style to. /// The content with vector paths to apply the stroke style to.
#[implementations( #[implementations(
@@ -323,7 +324,7 @@ async fn stroke<V, L: IntoF64Vec, P: IntoGraphicList + 'n + Send + 'static>(
dash_offset: f64, dash_offset: f64,
) -> List<V> ) -> List<V>
where where
List<V>: VectorListIterMut + 'n + Send, List<V>: VectorListIterMut + Send,
{ {
let dash_lengths = dash_lengths.into_vec().into_iter().map(|length| length.max(0.)).collect(); let dash_lengths = dash_lengths.into_vec().into_iter().map(|length| length.max(0.)).collect();
@@ -356,7 +357,7 @@ where
} }
#[node_macro::node(name("Copy to Points"), category("Repeat"), path(core_types::vector))] #[node_macro::node(name("Copy to Points"), category("Repeat"), path(core_types::vector))]
async fn copy_to_points<I: 'n + Send + Clone>( fn copy_to_points<I: Send + Clone>(
_: impl Ctx, _: impl Ctx,
points: List<Vector>, points: List<Vector>,
/// Artwork to be copied and placed at each point. /// Artwork to be copied and placed at each point.
@@ -440,7 +441,7 @@ async fn copy_to_points<I: 'n + Send + Clone>(
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn round_corners( fn round_corners(
_: impl Ctx, _: impl Ctx,
source: List<Vector>, source: List<Vector>,
#[hard(0..)] #[hard(0..)]
@@ -777,7 +778,7 @@ pub mod extrude_algorithms {
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn extrude(_: impl Ctx, mut source: List<Vector>, direction: DVec2, joining_algorithm: ExtrudeJoiningAlgorithm) -> List<Vector> { fn extrude(_: impl Ctx, mut source: List<Vector>, direction: DVec2, joining_algorithm: ExtrudeJoiningAlgorithm) -> List<Vector> {
for vector in source.iter_element_values_mut() { for vector in source.iter_element_values_mut() {
extrude_algorithms::extrude(vector, direction, joining_algorithm); extrude_algorithms::extrude(vector, direction, joining_algorithm);
} }
@@ -785,7 +786,7 @@ async fn extrude(_: impl Ctx, mut source: List<Vector>, direction: DVec2, joinin
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn box_warp(_: impl Ctx, content: List<Vector>, #[expose] rectangle: List<Vector>) -> List<Vector> { fn box_warp(_: impl Ctx, content: List<Vector>, #[expose] rectangle: List<Vector>) -> List<Vector> {
let Some(target) = rectangle.element(0).cloned() else { return content }; let Some(target) = rectangle.element(0).cloned() else { return content };
let target_transform: DAffine2 = rectangle.attribute_cloned_or_default(ATTR_TRANSFORM, 0); let target_transform: DAffine2 = rectangle.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
@@ -870,7 +871,7 @@ fn bilinear_interpolate(t: DVec2, quad: &[DVec2; 4]) -> DVec2 {
} }
#[node_macro::node(category("Vector"), path(graphene_core::vector))] #[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn pack_strips<T: 'n + Send + Clone>( fn pack_strips<T: Send + Clone>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Graphic>, List<Graphic>,
@@ -991,7 +992,7 @@ where
/// Automatically constructs tangents (Bézier handles) for anchor points in a vector path. /// Automatically constructs tangents (Bézier handles) for anchor points in a vector path.
#[node_macro::node(category("Vector: Modifier"), name("Auto-Tangents"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), name("Auto-Tangents"), path(core_types::vector))]
async fn auto_tangents( fn auto_tangents(
_: impl Ctx, _: impl Ctx,
source: List<Vector>, source: List<Vector>,
/// The amount of spread for the auto-tangents, from 0 (sharp corner) to 1 (full spread). /// The amount of spread for the auto-tangents, from 0 (sharp corner) to 1 (full spread).
@@ -1145,7 +1146,7 @@ async fn auto_tangents(
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn bounding_box(_: impl Ctx, content: List<Vector>) -> List<Vector> { fn bounding_box(_: impl Ctx, content: List<Vector>) -> List<Vector> {
content content
.into_iter() .into_iter()
.map(|mut row| { .map(|mut row| {
@@ -1170,7 +1171,7 @@ async fn bounding_box(_: impl Ctx, content: List<Vector>) -> List<Vector> {
} }
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))] #[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
async fn dimensions(_: impl Ctx, content: List<Vector>) -> DVec2 { fn dimensions(_: impl Ctx, content: List<Vector>) -> DVec2 {
(0..content.len()) (0..content.len())
.filter_map(|index| content.element(index).unwrap().bounding_box_with_transform(content.attribute_cloned_or_default(ATTR_TRANSFORM, index))) .filter_map(|index| content.element(index).unwrap().bounding_box_with_transform(content.attribute_cloned_or_default(ATTR_TRANSFORM, index)))
.reduce(|[acc_top_left, acc_bottom_right], [top_left, bottom_right]| [acc_top_left.min(top_left), acc_bottom_right.max(bottom_right)]) .reduce(|[acc_top_left, acc_bottom_right], [top_left, bottom_right]| [acc_top_left.min(top_left), acc_bottom_right.max(bottom_right)])
@@ -1186,7 +1187,7 @@ fn as_vector(_: impl Ctx, value: List<Vector>) -> List<Vector> {
/// Creates a polyline from a series of vector points, replacing any existing segments and regions that may already exist. /// Creates a polyline from a series of vector points, replacing any existing segments and regions that may already exist.
#[node_macro::node(category("Vector"), name("Points to Polyline"), path(core_types::vector))] #[node_macro::node(category("Vector"), name("Points to Polyline"), path(core_types::vector))]
async fn points_to_polyline(_: impl Ctx, mut points: List<Vector>, #[default(true)] closed: bool) -> List<Vector> { fn points_to_polyline(_: impl Ctx, mut points: List<Vector>, #[default(true)] closed: bool) -> List<Vector> {
for vector in points.iter_element_values_mut() { for vector in points.iter_element_values_mut() {
let mut segment_domain = SegmentDomain::new(); let mut segment_domain = SegmentDomain::new();
let mut next_id = SegmentId::ZERO; let mut next_id = SegmentId::ZERO;
@@ -1214,7 +1215,7 @@ async fn points_to_polyline(_: impl Ctx, mut points: List<Vector>, #[default(tru
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector), properties("offset_path_properties"))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector), properties("offset_path_properties"))]
async fn offset_path(_: impl Ctx, content: List<Vector>, distance: f64, join: StrokeJoin, #[default(4.)] miter_limit: f64) -> List<Vector> { fn offset_path(_: impl Ctx, content: List<Vector>, distance: f64, join: StrokeJoin, #[default(4.)] miter_limit: f64) -> List<Vector> {
content content
.into_iter() .into_iter()
.map(|mut row| { .map(|mut row| {
@@ -1258,7 +1259,7 @@ async fn offset_path(_: impl Ctx, content: List<Vector>, distance: f64, join: St
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn solidify_stroke<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> { fn solidify_stroke<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> {
// TODO: Make this node support stroke align, which it currently ignores // TODO: Make this node support stroke align, which it currently ignores
let graphic_list = content.into_graphic_list(); let graphic_list = content.into_graphic_list();
@@ -1366,7 +1367,7 @@ async fn solidify_stroke<T: IntoGraphicList>(_: impl Ctx, #[implementations(List
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn separate_subpaths(_: impl Ctx, content: List<Vector>) -> List<Vector> { fn separate_subpaths(_: impl Ctx, content: List<Vector>) -> List<Vector> {
content content
.into_iter() .into_iter()
.flat_map(|row| { .flat_map(|row| {
@@ -1397,7 +1398,7 @@ async fn separate_subpaths(_: impl Ctx, content: List<Vector>) -> List<Vector> {
/// Determines if the subpath at the given index (across all vector element subpaths) is closed, meaning its ends are connected together forming a loop. /// Determines if the subpath at the given index (across all vector element subpaths) is closed, meaning its ends are connected together forming a loop.
#[node_macro::node(name("Path is Closed"), category("Vector: Measure"), path(core_types::vector))] #[node_macro::node(name("Path is Closed"), category("Vector: Measure"), path(core_types::vector))]
async fn path_is_closed( fn path_is_closed(
_: impl Ctx, _: impl Ctx,
/// The vector content whose subpaths are inspected. /// The vector content whose subpaths are inspected.
content: List<Vector>, content: List<Vector>,
@@ -1412,25 +1413,25 @@ async fn path_is_closed(
} }
#[node_macro::node(category("Vector"), path(graphene_core::vector))] #[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn map_points(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: List<Vector>, mapped: impl Node<Context<'static>, Output = DVec2>) -> List<Vector> { fn map_points(ctx: impl Ctx + DeriveCtx, content: List<Vector>, mapped: impl Node<Context<'_>, Output = DVec2>) -> Result<List<Vector>, Interrupt> {
let spilled = ctx.index_head();
let mut content = content; let mut content = content;
let mut index = 0; let mut index = 0;
for vector in content.iter_element_values_mut() { for vector in content.iter_element_values_mut() {
for (_, position) in vector.point_domain.positions_mut() { for (_, position) in vector.point_domain.positions_mut() {
let owned_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index).with_position(*position); let scoped = ctx.push_position(*position);
*position = mapped.eval(&scoped.ctx().promoted(&spilled, index))?;
index += 1; index += 1;
*position = mapped.eval(owned_ctx.into_context()).await;
} }
} }
content Ok(content)
} }
// TODO: Rename to "Combine Paths" and make this happen per-element instead of flattening every element into a single path. The migration for this should then become a Flatten Vector -> Combine Paths pair of nodes. // TODO: Rename to "Combine Paths" and make this happen per-element instead of flattening every element into a single path. The migration for this should then become a Flatten Vector -> Combine Paths pair of nodes.
#[node_macro::node(category("Vector"), path(graphene_core::vector))] #[node_macro::node(category("Vector"), path(graphene_core::vector))]
pub async fn flatten_path<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> { pub fn flatten_path<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> {
let graphic_list = content.into_graphic_list(); let graphic_list = content.into_graphic_list();
let flattened = graphic_list.clone().into_flattened_list::<Vector>(); let flattened = graphic_list.clone().into_flattened_list::<Vector>();
@@ -1486,7 +1487,7 @@ pub async fn flatten_path<T: IntoGraphicList>(_: impl Ctx, #[implementations(Lis
/// Convert vector geometry into a polyline composed of evenly spaced points. /// Convert vector geometry into a polyline composed of evenly spaced points.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector), properties("sample_polyline_properties"), memoize)] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector), properties("sample_polyline_properties"), memoize)]
async fn sample_polyline( fn sample_polyline(
_: impl Ctx, _: impl Ctx,
content: List<Vector>, content: List<Vector>,
spacing: PointSpacingType, spacing: PointSpacingType,
@@ -1572,7 +1573,7 @@ async fn sample_polyline(
/// Simplifies vector paths by reducing the number of curve segments while preserving the overall shape within the given tolerance. /// Simplifies vector paths by reducing the number of curve segments while preserving the overall shape within the given tolerance.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn simplify( fn simplify(
_: impl Ctx, _: impl Ctx,
/// The vector paths to simplify. /// The vector paths to simplify.
content: List<Vector>, content: List<Vector>,
@@ -1616,7 +1617,7 @@ async fn simplify(
/// Decimates vector paths into polylines by sampling any curves into line segments, then removing points that don't significantly contribute to the shape using the Ramer-Douglas-Peucker algorithm. /// Decimates vector paths into polylines by sampling any curves into line segments, then removing points that don't significantly contribute to the shape using the Ramer-Douglas-Peucker algorithm.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn decimate( fn decimate(
_: impl Ctx, _: impl Ctx,
/// The vector paths to decimate. /// The vector paths to decimate.
content: List<Vector>, content: List<Vector>,
@@ -1744,7 +1745,7 @@ async fn decimate(
/// ///
/// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it. /// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it.
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))] #[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
async fn cut_path( fn cut_path(
_: impl Ctx, _: impl Ctx,
/// The path to insert a cut into. /// The path to insert a cut into.
mut content: List<Vector>, mut content: List<Vector>,
@@ -1795,7 +1796,7 @@ async fn cut_path(
/// Cuts path segments into separate disconnected pieces where each is a distinct subpath. /// Cuts path segments into separate disconnected pieces where each is a distinct subpath.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn cut_segments(_: impl Ctx, mut content: List<Vector>) -> List<Vector> { fn cut_segments(_: impl Ctx, mut content: List<Vector>) -> List<Vector> {
// Iterate through every segment and make a copy of each of its endpoints, then reassign each segment's endpoints to its own unique point copy // Iterate through every segment and make a copy of each of its endpoints, then reassign each segment's endpoints to its own unique point copy
for vector in content.iter_element_values_mut() { for vector in content.iter_element_values_mut() {
let points_count = vector.point_domain.ids().len(); let points_count = vector.point_domain.ids().len();
@@ -1854,7 +1855,7 @@ async fn cut_segments(_: impl Ctx, mut content: List<Vector>) -> List<Vector> {
/// ///
/// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it. /// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it.
#[node_macro::node(name("Position on Path"), category("Vector: Measure"), path(graphene_core::vector))] #[node_macro::node(name("Position on Path"), category("Vector: Measure"), path(graphene_core::vector))]
async fn position_on_path( fn position_on_path(
_: impl Ctx, _: impl Ctx,
/// The path to traverse. /// The path to traverse.
content: List<Vector>, content: List<Vector>,
@@ -1892,7 +1893,7 @@ async fn position_on_path(
/// ///
/// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it. /// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it.
#[node_macro::node(name("Tangent on Path"), category("Vector: Measure"), path(graphene_core::vector))] #[node_macro::node(name("Tangent on Path"), category("Vector: Measure"), path(graphene_core::vector))]
async fn tangent_on_path( fn tangent_on_path(
_: impl Ctx, _: impl Ctx,
/// The path to traverse. /// The path to traverse.
content: List<Vector>, content: List<Vector>,
@@ -1940,7 +1941,7 @@ async fn tangent_on_path(
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector), memoize)] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector), memoize)]
async fn scatter_points( fn scatter_points(
_: impl Ctx, _: impl Ctx,
content: List<Vector>, content: List<Vector>,
#[unit(" px")] #[unit(" px")]
@@ -1990,7 +1991,7 @@ async fn scatter_points(
} }
#[node_macro::node(name("Spline"), category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(name("Spline"), category("Vector: Modifier"), path(core_types::vector))]
async fn spline(_: impl Ctx, content: List<Vector>) -> List<Vector> { fn spline(_: impl Ctx, content: List<Vector>) -> List<Vector> {
content content
.into_iter() .into_iter()
.filter_map(|mut row| { .filter_map(|mut row| {
@@ -2090,7 +2091,7 @@ fn apply_point_deltas(element: &mut Vector, deltas: &[DVec2], transform: DAffine
/// Perturbs the positions of anchor points in vector geometry by random amounts and directions. /// Perturbs the positions of anchor points in vector geometry by random amounts and directions.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn jitter_points( fn jitter_points(
_: impl Ctx, _: impl Ctx,
/// The vector geometry with points to be jittered. /// The vector geometry with points to be jittered.
content: List<Vector>, content: List<Vector>,
@@ -2140,7 +2141,7 @@ async fn jitter_points(
/// Displaces anchor points along their normal direction (perpendicular to the path) by a set distance. /// Displaces anchor points along their normal direction (perpendicular to the path) by a set distance.
/// Points with 0 or 3+ segment connections have no well-defined normal and are left in place. /// Points with 0 or 3+ segment connections have no well-defined normal and are left in place.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn offset_points( fn offset_points(
_: impl Ctx, _: impl Ctx,
/// The vector geometry with points to be offset. /// The vector geometry with points to be offset.
content: List<Vector>, content: List<Vector>,
@@ -2177,7 +2178,7 @@ async fn offset_points(
/// ///
/// *Progression* morphs through all objects. Interpolation is linear unless *Path* geometry is provided to control the trajectory between key objects. The **Origins to Polyline** node may be used to create a path with anchor points corresponding to each object. Other nodes can modify its path segments. /// *Progression* morphs through all objects. Interpolation is linear unless *Path* geometry is provided to control the trajectory between key objects. The **Origins to Polyline** node may be used to create a path with anchor points corresponding to each object. Other nodes can modify its path segments.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn morph<I: IntoGraphicList>( fn morph<I: IntoGraphicList>(
_: impl Ctx, _: impl Ctx,
/// The vector objects to interpolate between. Mixed graphic content is deeply flattened to keep only vector elements. /// The vector objects to interpolate between. Mixed graphic content is deeply flattened to keep only vector elements.
#[implementations(List<Graphic>, List<Vector>)] #[implementations(List<Graphic>, List<Vector>)]
@@ -3124,19 +3125,19 @@ fn point_inside(_: impl Ctx, source: List<Vector>, point: DVec2) -> bool {
// TODO: Return u32, u64, or usize instead of f64 after #1621 is resolved and has allowed us to implement automatic type conversion in the node graph for nodes with generic type inputs. // TODO: Return u32, u64, or usize instead of f64 after #1621 is resolved and has allowed us to implement automatic type conversion in the node graph for nodes with generic type inputs.
// TODO: (Currently automatic type conversion only works for concrete types, via the Graphene preprocessor and not the full Graphene type system.) // TODO: (Currently automatic type conversion only works for concrete types, via the Graphene preprocessor and not the full Graphene type system.)
#[node_macro::node(category("General"), path(graphene_core::vector))] #[node_macro::node(category("General"), path(graphene_core::vector))]
async fn count_elements(_: impl Ctx, content: ListDyn) -> f64 { fn count_elements(_: impl Ctx, content: ListDyn) -> f64 {
content.len() as f64 content.len() as f64
} }
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))] #[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
async fn count_points(_: impl Ctx, content: List<Vector>) -> f64 { fn count_points(_: impl Ctx, content: List<Vector>) -> f64 {
content.iter_element_values().map(|vector| vector.point_domain.positions().len() as f64).sum() content.iter_element_values().map(|vector| vector.point_domain.positions().len() as f64).sum()
} }
/// Retrieves the vec2 position (in local space) of the anchor point at the specified index in a `List` of vector elements. /// Retrieves the vec2 position (in local space) of the anchor point at the specified index in a `List` of vector elements.
/// If no value exists at that index, the position (0, 0) is returned. /// If no value exists at that index, the position (0, 0) is returned.
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))] #[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
async fn index_points( fn index_points(
_: impl Ctx, _: impl Ctx,
/// The vector element or elements containing the anchor points to be retrieved. /// The vector element or elements containing the anchor points to be retrieved.
content: List<Vector>, content: List<Vector>,
@@ -3170,7 +3171,7 @@ async fn index_points(
} }
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))] #[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
async fn path_length(_: impl Ctx, source: List<Vector>) -> f64 { fn path_length(_: impl Ctx, source: List<Vector>) -> f64 {
(0..source.len()) (0..source.len())
.map(|index| { .map(|index| {
let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM, index);
@@ -3189,26 +3190,24 @@ async fn path_length(_: impl Ctx, source: List<Vector>) -> f64 {
} }
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))] #[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node<Context<'static>, Output = List<Vector>>) -> f64 { fn area(ctx: impl Ctx + DeriveCtx, content: impl Node<Context<'_>, Output = List<Vector>>) -> Result<f64, Interrupt> {
let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context(); let vector = content.eval(&ctx.with_footprint(&Footprint::DEFAULT))?;
let vector = content.eval(new_ctx).await;
(0..vector.len()) Ok((0..vector.len())
.map(|index| { .map(|index| {
let transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let area_scale = transform.matrix2.determinant().abs(); let area_scale = transform.matrix2.determinant().abs();
vector.element(index).unwrap().stroke_bezpath_iter().map(|subpath| subpath.area() * area_scale).sum::<f64>() vector.element(index).unwrap().stroke_bezpath_iter().map(|subpath| subpath.area() * area_scale).sum::<f64>()
}) })
.sum() .sum())
} }
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))] #[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node<Context<'static>, Output = List<Vector>>, centroid_type: CentroidType) -> DVec2 { fn centroid(ctx: impl Ctx + DeriveCtx, content: impl Node<Context<'_>, Output = List<Vector>>, centroid_type: CentroidType) -> Result<DVec2, Interrupt> {
let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context(); let vector = content.eval(&ctx.with_footprint(&Footprint::DEFAULT))?;
let vector = content.eval(new_ctx).await;
if vector.is_empty() { if vector.is_empty() {
return DVec2::ZERO; return Ok(DVec2::ZERO);
} }
// All subpath centroid positions added together as if they were vectors from the origin. // All subpath centroid positions added together as if they were vectors from the origin.
@@ -3234,7 +3233,7 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node<
} }
if sum > 0. { if sum > 0. {
centroid / sum Ok(centroid / sum)
} }
// Without a summed denominator, return the average of all positions instead // Without a summed denominator, return the average of all positions instead
else { else {
@@ -3255,7 +3254,7 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node<
.inspect(|_| count += 1) .inspect(|_| count += 1)
.sum::<DVec2>(); .sum::<DVec2>();
if count != 0 { summed_positions / (count as f64) } else { DVec2::ZERO } if count != 0 { Ok(summed_positions / (count as f64)) } else { Ok(DVec2::ZERO) }
} }
} }
+6 -5
View File
@@ -118,11 +118,11 @@ impl Preprocessor {
let NodeMetadata { fields, memoize, inject_scope, .. } = metadata; let NodeMetadata { fields, memoize, inject_scope, .. } = metadata;
let Some(implementations) = node_registry.get(&id) else { continue }; let Some(implementations) = node_registry.get(&id) else { continue };
let valid_call_args: HashSet<_> = implementations.iter().map(|(_, node_io)| node_io.call_argument.clone()).collect(); let valid_call_args: HashSet<_> = implementations.iter().map(|entry| entry.io.call_argument.clone()).collect();
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 mut node_io_types = vec![HashSet::new(); fields.len()]; let mut node_io_types = vec![HashSet::new(); fields.len()];
for (_, node_io) in implementations.iter() { for entry in implementations.iter() {
for (i, ty) in node_io.inputs.iter().enumerate() { for (i, ty) in entry.io.inputs.iter().enumerate() {
node_io_types[i].insert(ty.clone()); node_io_types[i].insert(ty.clone());
} }
} }
@@ -238,7 +238,7 @@ impl Preprocessor {
// If `inject_scope` is requested, prepare the proto node template and type info needed // If `inject_scope` is requested, prepare the proto node template and type info needed
if *inject_scope if *inject_scope
&& let Some(implementations) = node_registry.get(&id) && let Some(implementations) = node_registry.get(&id)
&& let Some((_, node_io)) = implementations.first() && let Some(node_io) = implementations.first().map(|entry| &entry.io)
{ {
let template = DocumentNode { let template = DocumentNode {
inputs: node_inputs(fields, node_io), inputs: node_inputs(fields, node_io),
@@ -279,6 +279,7 @@ pub fn node_inputs(fields: &[registry::FieldMetadata], first_node_io: &NodeIOTyp
} }
} }
RegistryValueSource::Scope(data) => return NodeInput::scope(*data), RegistryValueSource::Scope(data) => return NodeInput::scope(*data),
RegistryValueSource::SourceId => return NodeInput::Reflection(DocumentNodeMetadata::SourceId),
}; };
if let Some(type_default) = TaggedValue::from_type(ty) { if let Some(type_default) = TaggedValue::from_type(ty) {
@@ -126,6 +126,12 @@ pub trait ModifyIndex: ExtractIndex + InjectIndex {}
pub trait ModifyVarArgs: ExtractVarArgs + InjectVarArgs {} pub trait ModifyVarArgs: ExtractVarArgs + InjectVarArgs {}
``` ```
### Authoring rule: forward with Modify*, consume with Extract*
Declare a feature via `Modify*` when the node only reads it to compute a new value it injects for its children (a "forward"), and via `Extract*` only when the node genuinely consumes the value for its own output (a "sink").
The analysis skips `Modify*` bounds but treats every `Extract*` bound as an *unconditional* requirement. Because `Modify*` is a supertrait of `Extract*`, a `Modify*` bound already grants the read capability (e.g. `modify_footprint`, which is `where Self: ExtractFootprint`), so it is a mistake to list both. Writing `impl Ctx + ExtractFootprint + ModifyFootprint` on a forwarding node re-introduces the feature as a hard dependency at every such node, which propagates up the whole tree and pins upstream memos to a value the node never actually consumes (e.g. a viewport pan invalidating a render cache that renders in local space). Use `impl Ctx + ModifyFootprint` alone.
### Conditional Context Dependencies ### Conditional Context Dependencies
Modify* traits represent a special case in context analysis: Modify* traits represent a special case in context analysis:
+4 -3
View File
@@ -76,15 +76,16 @@ fn write_nodes_table_rows(page: &mut std::fs::File, nodes: &[(&core_types::Proto
let implementations = node_registry.get(id)?; let implementations = node_registry.get(id)?;
let valid_primary_inputs_to_outputs = implementations let valid_primary_inputs_to_outputs = implementations
.iter() .iter()
.map(|(_, node_io)| { .map(|entry| {
let input = node_io let input = entry
.io
.inputs .inputs
.first() .first()
.map(|ty| ty.nested_type()) .map(|ty| ty.nested_type())
.filter(|&ty| ty != &concrete!(())) .filter(|&ty| ty != &concrete!(()))
.map(ToString::to_string) .map(ToString::to_string)
.unwrap_or_default(); .unwrap_or_default();
let output = node_io.return_value.nested_type().to_string(); let output = entry.io.return_value.nested_type().to_string();
format!("`{input} → {output}`") format!("`{input} → {output}`")
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
+3 -3
View File
@@ -23,8 +23,8 @@ pub fn write_node_page(index: usize, id: &core_types::ProtoNodeIdentifier, metad
// Input types // Input types
let mut valid_input_types = vec![Vec::new(); metadata.fields.len()]; let mut valid_input_types = vec![Vec::new(); metadata.fields.len()];
for (_, node_io) in implementations.iter() { for entry in implementations.iter() {
for (i, ty) in node_io.inputs.iter().enumerate() { for (i, ty) in entry.io.inputs.iter().enumerate() {
valid_input_types[i].push(ty.nested_type().clone()); valid_input_types[i].push(ty.nested_type().clone());
} }
} }
@@ -35,7 +35,7 @@ pub fn write_node_page(index: usize, id: &core_types::ProtoNodeIdentifier, metad
} }
// Primary output types // Primary output types
let valid_primary_outputs = implementations.iter().map(|(_, node_io)| node_io.return_value.nested_type().clone()).collect::<Vec<_>>(); let valid_primary_outputs = implementations.iter().map(|entry| entry.io.return_value.nested_type().clone()).collect::<Vec<_>>();
// Write sections to the file // Write sections to the file
write_frontmatter(&mut page, metadata, index + 1); write_frontmatter(&mut page, metadata, index + 1);