Skip to content

Switch to new async execution model - #4391

Open
TrueDoctor wants to merge 1 commit into
async-pr1-core-typesfrom
async-pr2-cutover
Open

Switch to new async execution model#4391
TrueDoctor wants to merge 1 commit into
async-pr1-core-typesfrom
async-pr2-cutover

Conversation

@TrueDoctor

Copy link
Copy Markdown
Member

No description provided.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

12 issues found across 71 files

Confidence score: 2/5

  • The highest risk is in async execution plumbing across editor/src/node_graph_executor/runtime.rs and node-graph/graphene-cli/src/export.rs: NoopSpawner drops source futures and export stops on the first Pending, so async graphs can stall indefinitely or fail instead of completing — wire a real runtime spawner plus completion/retry signaling until terminal GPoll results.
  • GPU adjustment graph construction looks brittle in node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs because generated nodes reference an unregistered scope, which can leave executor inputs unresolved and break graph builds — target the existing wgpu_executor scope node and its expected input type.
  • Compatibility paths have concrete regression risk in editor/src/messages/portfolio/document/utility_types/network_interface.rs and node-graph/graph-craft/src/document/value.rs: legacy load_resource docs can migrate with missing inputs, and several list/context outputs cannot round-trip from edges back to TaggedValue, causing load/evaluation failures — run migration normalization after migrate_node and add missing from_edge conversions.
  • node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs now uses std::sync::Mutex with lock().unwrap(), so a poisoned lock can panic the whole app under thread failure — switch to non-panicking poison handling (for example unwrap_or_else) to contain runtime faults.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs">

<violation number="1" location="node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs:236">
P1: Generated GPU adjustment nodes now request a scope node that is not registered, so their executor input cannot be resolved when graphs are built. Point this field at the existing `wgpu_executor` scope node and use its `WgpuExecutorHandle` wire type instead of inventing an Arc-specific node ID.</violation>
</file>

<file name="node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs">

<violation number="1" location="node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs:37">
P1: Switching from `futures::lock::Mutex` to `std::sync::Mutex` introduces a poison panic risk: `lock().unwrap()` will crash the entire application if another thread panicked while holding the mutex. Use `.lock().unwrap_or_else(|e| e.into_inner())` to recover the inner pipeline cache from the poison error instead of panicking.</violation>
</file>

<file name="node-graph/graphene-cli/src/export.rs">

<violation number="1" location="node-graph/graphene-cli/src/export.rs:20">
P3: Successful fallback exports are recorded as errors, adding expected recovery events to error logs. Use `warn!` (or lower) unless this path is meant to fail the export.

(Based on your team's feedback about error-level logging for expected fallbacks.) [FEEDBACK_USED]</violation>

<violation number="2" location="node-graph/graphene-cli/src/export.rs:23">
P1: Exports containing async nodes now fail on their initial `Pending` result instead of waiting for graph work to complete. Wire a real runtime spawner and drive/retry evaluation until a terminal `GPoll` result; the current `NoopSpawner` drops the scheduled task entirely.</violation>
</file>

<file name="node-graph/node-macro/src/parsing.rs">

<violation number="1" location="node-graph/node-macro/src/parsing.rs:474">
P3: A `placeholder(...)` on a synchronous non-source node is silently ignored, making a typo/configuration mistake look valid. Validate that this attribute is used only by async/source-kernel nodes.</violation>
</file>

<file name="editor/src/messages/portfolio/document/utility_types/network_interface.rs">

<violation number="1" location="editor/src/messages/portfolio/document/utility_types/network_interface.rs:118">
P1: Opening documents with the older 3-input `load_resource` layout leaves the node at two inputs after its legacy migration, without the required runtime and source-ID inputs. Run this pass after `migrate_node` normalizes legacy node arities (or invoke it again then) so those documents enter the new async model too.</violation>
</file>

<file name="node-graph/nodes/repeat/src/repeat_nodes.rs">

<violation number="1" location="node-graph/nodes/repeat/src/repeat_nodes.rs:13">
P3: Repeat nodes no longer have regression coverage for count/reverse ordering, transforms, or position-context propagation after this execution-model migration. Retain or rewrite the removed tests for synchronous `Node::eval`/`GPoll` behavior so these paths remain verified.</violation>
</file>

<file name="node-graph/libraries/core-types/src/context.rs">

<violation number="1" location="node-graph/libraries/core-types/src/context.rs:455">
P3: Node type displays will regress from `Context` to `ContextImpl`, since aliases are transparent to `type_name` and the user-readable type mapping still only handles `OwnedContextImpl`. Update that mapping (or register a descriptor alias) for the new representation.

(Based on your team's feedback about names reflecting current roles.) [FEEDBACK_USED]</violation>
</file>

<file name="node-graph/libraries/core-types/src/registry.rs">

<violation number="1" location="node-graph/libraries/core-types/src/registry.rs:106">
P3: Every edge evaluation now depends on a manually maintained raw pointer although `Arc<N>` already forwards `Node`; retain only the `Arc` and delegate through it to remove unchecked dereferences and manual auto-trait invariants.

(Based on your team's feedback about using safe code before unsafe.) [FEEDBACK_USED]</violation>

<violation number="2" location="node-graph/libraries/core-types/src/registry.rs:244">
P3: Registry-level arity/type validation is test-only because production invokes `NodeConstructor` directly. Store/pass the `RegistryEntry` or route execution through `construct` so validation is centralized instead of duplicated in every constructor.</violation>
</file>

<file name="editor/src/node_graph_executor/runtime.rs">

<violation number="1" location="editor/src/node_graph_executor/runtime.rs:126">
P0: Async graph nodes never finish because `NoopSpawner` drops every source future. Wire a real platform spawner and completion notification into the editor so pending/partial graphs are re-executed when their sources complete.</violation>
</file>

<file name="node-graph/graph-craft/src/document/value.rs">

<violation number="1" location="node-graph/graph-craft/src/document/value.rs:339">
P1: Graph outputs such as `List<Graphic>`, `List<Color>`, `List<f64>`, `List<NodeId>`, or `ContextModification` cannot be converted back to `TaggedValue`: `to_edge` supports creating these edges, but `from_edge` has no corresponding cases and returns an error. This makes valid non-`RenderOutput` graph outputs fail in `DynamicExecutor::execute`; the conversion should cover every type materialized by `to_edge` (including the `TypeDefault` list types).</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic


impl NodeRuntime {
pub fn new(receiver: Receiver<GraphRuntimeRequest>, sender: Sender<NodeGraphUpdate>) -> Self {
let spawner: Box<DynSpawner> = Box::new(NoopSpawner);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0: Async graph nodes never finish because NoopSpawner drops every source future. Wire a real platform spawner and completion notification into the editor so pending/partial graphs are re-executed when their sources complete.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/node_graph_executor/runtime.rs, line 126:

<comment>Async graph nodes never finish because `NoopSpawner` drops every source future. Wire a real platform spawner and completion notification into the editor so pending/partial graphs are re-executed when their sources complete.</comment>

<file context>
@@ -121,18 +123,25 @@ pub static NODE_RUNTIME: once_cell::sync::Lazy<Mutex<Option<NodeRuntime>>> = onc
 
 impl NodeRuntime {
 	pub fn new(receiver: Receiver<GraphRuntimeRequest>, sender: Sender<NodeGraphUpdate>) -> Self {
+		let spawner: Box<DynSpawner> = Box::new(NoopSpawner);
+		let graph_runtime: Arc<DynGraphRuntime> = Arc::new(GraphRuntime::new(spawner));
+		let mut executor = DynamicExecutor::default();
</file context>

ty: parse_quote!(std::sync::Arc<WgpuExecutor>),
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"))),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Generated GPU adjustment nodes now request a scope node that is not registered, so their executor input cannot be resolved when graphs are built. Point this field at the existing wgpu_executor scope node and use its WgpuExecutorHandle wire type instead of inventing an Arc-specific node ID.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs, line 236:

<comment>Generated GPU adjustment nodes now request a scope node that is not registered, so their executor input cannot be resolved when graphs are built. Point this field at the existing `wgpu_executor` scope node and use its `WgpuExecutorHandle` wire type instead of inventing an Arc-specific node ID.</comment>

<file context>
@@ -231,9 +231,9 @@ impl PerPixelAdjustCodegen<'_> {
+				ty: parse_quote!(std::sync::Arc<WgpuExecutor>),
 				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_max: None,
</file context>

pub async 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;
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().unwrap();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Switching from futures::lock::Mutex to std::sync::Mutex introduces a poison panic risk: lock().unwrap() will crash the entire application if another thread panicked while holding the mutex. Use .lock().unwrap_or_else(|e| e.into_inner()) to recover the inner pipeline cache from the poison error instead of panicking.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs, line 37:

<comment>Switching from `futures::lock::Mutex` to `std::sync::Mutex` introduces a poison panic risk: `lock().unwrap()` will crash the entire application if another thread panicked while holding the mutex. Use `.lock().unwrap_or_else(|e| e.into_inner())` to recover the inner pipeline cache from the poison error instead of panicking.</comment>

<file context>
@@ -33,8 +33,8 @@ impl PerPixelAdjustShaderRuntime {
-	pub async 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;
+	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().unwrap();
 		let pipeline = cache
 			.entry(shaders.fragment_shader_name.to_owned())
</file context>

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()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Exports containing async nodes now fail on their initial Pending result instead of waiting for graph work to complete. Wire a real runtime spawner and drive/retry evaluation until a terminal GPoll result; the current NoopSpawner drops the scheduled task entirely.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/graphene-cli/src/export.rs, line 23:

<comment>Exports containing async nodes now fail on their initial `Pending` result instead of waiting for graph work to complete. Wire a real runtime spawner and drive/retry evaluation until a terminal `GPoll` result; the current `NoopSpawner` drops the scheduled task entirely.</comment>

<file context>
@@ -10,6 +12,19 @@ use std::io::Cursor;
+			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()),
+	}
</file context>

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Opening documents with the older 3-input load_resource layout leaves the node at two inputs after its legacy migration, without the required runtime and source-ID inputs. Run this pass after migrate_node normalizes legacy node arities (or invoke it again then) so those documents enter the new async model too.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/utility_types/network_interface.rs, line 118:

<comment>Opening documents with the older 3-input `load_resource` layout leaves the node at two inputs after its legacy migration, without the required runtime and source-ID inputs. Run this pass after `migrate_node` normalizes legacy node arities (or invoke it again then) so those documents enter the new async model too.</comment>

<file context>
@@ -95,6 +95,34 @@ impl NodeNetworkInterface {
+				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"));
</file context>

//
// Example usage:
// #[node_macro::node(..., placeholder(empty_image), ...)]
"placeholder" => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: A placeholder(...) on a synchronous non-source node is silently ignored, making a typo/configuration mistake look valid. Validate that this attribute is used only by async/source-kernel nodes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/node-macro/src/parsing.rs, line 474:

<comment>A `placeholder(...)` on a synchronous non-source node is silently ignored, making a typo/configuration mistake look valid. Validate that this attribute is used only by async/source-kernel nodes.</comment>

<file context>
@@ -453,13 +466,63 @@ impl Parse for NodeFnAttributes {
+				//
+				// Example usage:
+				// #[node_macro::node(..., placeholder(empty_image), ...)]
+				"placeholder" => {
+					let meta = meta.require_list()?;
+					if placeholder.is_some() {
</file context>

#[node_macro::node(category("Repeat"))]
async fn repeat<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Ctx,
fn repeat<T: Into<Graphic> + Default + Send + Clone + 'static>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Repeat nodes no longer have regression coverage for count/reverse ordering, transforms, or position-context propagation after this execution-model migration. Retain or rewrite the removed tests for synchronous Node::eval/GPoll behavior so these paths remain verified.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/nodes/repeat/src/repeat_nodes.rs, line 13:

<comment>Repeat nodes no longer have regression coverage for count/reverse ordering, transforms, or position-context propagation after this execution-model migration. Retain or rewrite the removed tests for synchronous `Node::eval`/`GPoll` behavior so these paths remain verified.</comment>

<file context>
@@ -1,71 +1,73 @@
 #[node_macro::node(category("Repeat"))]
-async fn repeat<T: Into<Graphic> + Default + Send + Clone + 'static>(
-	ctx: impl ExtractAll + CloneVarArgs + Ctx,
+fn repeat<T: Into<Graphic> + Default + Send + Clone + 'static>(
+	ctx: impl Ctx + DeriveCtx,
 	#[implementations(
</file context>

// ==============

pub type Context<'a> = Option<Arc<OwnedContextImpl>>;
pub type Context<'a> = ContextImpl<'a>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Node type displays will regress from Context to ContextImpl, since aliases are transparent to type_name and the user-readable type mapping still only handles OwnedContextImpl. Update that mapping (or register a descriptor alias) for the new representation.

(Based on your team's feedback about names reflecting current roles.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/libraries/core-types/src/context.rs, line 455:

<comment>Node type displays will regress from `Context` to `ContextImpl`, since aliases are transparent to `type_name` and the user-readable type mapping still only handles `OwnedContextImpl`. Update that mapping (or register a descriptor alias) for the new representation.

(Based on your team's feedback about names reflecting current roles.) </comment>

<file context>
@@ -448,185 +448,12 @@ impl ExtractFootprint for () {
+// ==============
 
-pub type Context<'a> = Option<Arc<OwnedContextImpl>>;
+pub type Context<'a> = ContextImpl<'a>;
 type DynRef<'a> = &'a (dyn Any + Send + Sync);
-type DynBox = Box<dyn AnyHash + Send + Sync>;
</file context>

match dyn_any::downcast(input) {
Ok(input) => Box::pin(output(*input)),
Err(e) => panic!("DynAnyNode Input, {e} in:\n{node_name}"),
pub fn construct(entry: &RegistryEntry, inputs: Vec<EdgeHandle>) -> Result<EdgeHandle, ConstructionError> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Registry-level arity/type validation is test-only because production invokes NodeConstructor directly. Store/pass the RegistryEntry or route execution through construct so validation is centralized instead of duplicated in every constructor.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/libraries/core-types/src/registry.rs, line 244:

<comment>Registry-level arity/type validation is test-only because production invokes `NodeConstructor` directly. Store/pass the `RegistryEntry` or route execution through `construct` so validation is centralized instead of duplicated in every constructor.</comment>

<file context>
@@ -55,239 +57,457 @@ pub enum RegistryValueSource {
-		match dyn_any::downcast(input) {
-			Ok(input) => Box::pin(output(*input)),
-			Err(e) => panic!("DynAnyNode Input, {e} in:\n{node_name}"),
+pub fn construct(entry: &RegistryEntry, inputs: Vec<EdgeHandle>) -> Result<EdgeHandle, ConstructionError> {
+	if inputs.len() != entry.io.inputs.len() {
+		return Err(ConstructionError::Arity {
</file context>

#[cfg(not(feature = "dealloc_nodes"))]
self.node
pub struct SharedEdge<N: ?Sized> {
ptr: std::ptr::NonNull<N>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Every edge evaluation now depends on a manually maintained raw pointer although Arc<N> already forwards Node; retain only the Arc and delegate through it to remove unchecked dereferences and manual auto-trait invariants.

(Based on your team's feedback about using safe code before unsafe.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/libraries/core-types/src/registry.rs, line 106:

<comment>Every edge evaluation now depends on a manually maintained raw pointer although `Arc<N>` already forwards `Node`; retain only the `Arc` and delegate through it to remove unchecked dereferences and manual auto-trait invariants.

(Based on your team's feedback about using safe code before unsafe.) </comment>

<file context>
@@ -55,239 +57,457 @@ pub enum RegistryValueSource {
-		#[cfg(not(feature = "dealloc_nodes"))]
-		self.node
+pub struct SharedEdge<N: ?Sized> {
+	ptr: std::ptr::NonNull<N>,
+	own: std::sync::Arc<N>,
+}
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant