Fix wireing of async sources

This commit is contained in:
Dennis Kobert
2026-08-02 13:55:53 +02:00
parent 77d439ec35
commit e3aa32bc83
6 changed files with 75 additions and 33 deletions

View File

@@ -167,6 +167,11 @@ impl NodeRuntime {
}
}
#[cfg(test)]
pub fn take_dirty(&self) -> bool {
self.executor.take_dirty()
}
pub async fn run(&mut self) -> Option<Texture> {
let mut preferences = None;
let mut graph = None;

View File

@@ -53,6 +53,22 @@ impl EditorTestUtils {
}
runtime.run().await;
// An async source reports `Pending` on the evaluation that starts it and marks the runtime dirty
// once it completes, so the value only reaches the render on a follow-up evaluation. That first
// response is superseded, so it is drained rather than asserted on.
while runtime.take_dirty() {
let _ = editor.poll_node_graph_evaluation(&mut VecDeque::new());
let portfolio = &mut editor.dispatcher.message_handlers.portfolio_message_handler;
let (executor, documents) = (&mut portfolio.executor, &mut portfolio.documents);
let document = documents.get_mut(&document_id).unwrap();
if let Err(e) = executor.submit_current_node_graph_evaluation(document, document_id, UVec2::ONE, 1., Default::default(), DVec2::ZERO) {
return Err(format!("submit_current_node_graph_evaluation failed\n\n{e}"));
}
runtime.run().await;
}
let mut messages = VecDeque::new();
if let Err(e) = editor.poll_node_graph_evaluation(&mut messages) {
return Err(format!("Graph should render\n\n{e}"));

View File

@@ -189,11 +189,16 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
// Context nullification
#[cfg(feature = "gpu")]
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 => RuntimeHandle, Context => graphene_std::ContextModification]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => SourceId, Context => graphene_std::ContextModification]),
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::ContextModification]),
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::ContextModification]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => ListDyn, Context => graphene_std::ContextModification]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientType, Context => graphene_std::ContextModification]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientSpreadMethod, Context => graphene_std::ContextModification]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Option<DAffine2>, Context => graphene_std::ContextModification]),
#[cfg(target_family = "wasm")]
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 => std::sync::Arc<PlatformEditorApi>, Context => graphene_std::ContextModification]),
@@ -204,6 +209,8 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
// MEMO NODES
// ==========
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => ()]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => RuntimeHandle]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => SourceId]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => bool]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Artboard>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<Graphic>]),
@@ -276,6 +283,8 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeAlign]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::PaintOrder]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientType]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::GradientSpreadMethod]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option<DAffine2>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::BooleanOperation]),

View File

@@ -59,12 +59,15 @@ impl<S: Spawner + ?Sized> Spawner for Box<S> {
}
}
/// Dropped tasks never complete.
/// Polls each task once inline. Tasks that are not immediately ready never complete.
pub struct NoopSpawner;
impl Spawner for NoopSpawner {
fn spawn(&self, _task: SourceFuture) {
log::warn!("async source spawned before a host spawner is wired; the task is dropped");
fn spawn(&self, mut task: SourceFuture) {
let mut context = std::task::Context::from_waker(std::task::Waker::noop());
if task.as_mut().poll(&mut context).is_pending() {
log::warn!("async source is not immediately ready and no host spawner is wired; the task is dropped");
}
}
}

View File

@@ -131,16 +131,38 @@ impl Preprocessor {
input_type = &const { generic!(D) };
}
let inputs: Vec<_> = node_inputs(fields, first_node_io);
let mut inputs: Vec<_> = node_inputs(fields, first_node_io);
let input_count = inputs.len();
let network_inputs = (0..input_count).map(|i| NodeInput::node(NodeId(i as u64), 0)).collect();
// The node macro appends a `RuntimeHandle` scope field and a `SourceId` reflection field to every
// async or source kernel. They are resolved inside the substitution network, so the wrapper neither
// exposes them nor carries their context modification.
let injected_field_count = match &fields[..] {
[.., runtime, source] if matches!(runtime.value_source, RegistryValueSource::Scope(_)) && matches!(source.value_source, RegistryValueSource::SourceId) => 2,
_ => 0,
};
let wrapper_input_count = input_count - injected_field_count;
inputs.truncate(wrapper_input_count);
// The injected fields go straight onto the inner node, so the source it reflects is recorded on the
// node that consumes it rather than on a forwarding node that later gets dissolved.
let network_inputs = (0..input_count)
.map(|i| {
if i < wrapper_input_count {
NodeInput::node(NodeId(i as u64), 0)
} else {
injected_field_input(&fields[i])
}
})
.collect();
let passthrough_node = ops::passthrough::IDENTIFIER;
let mut generated_nodes = 0;
let mut nodes: HashMap<_, _, _> = node_io_types
.iter()
.take(input_count)
.take(wrapper_input_count)
.enumerate()
.map(|(i, inputs)| {
(
@@ -185,7 +207,7 @@ impl Preprocessor {
})
.collect();
if generated_nodes == 0 && !memoize && !inject_scope {
if generated_nodes == 0 && !memoize && !inject_scope && injected_field_count == 0 {
continue;
}
@@ -199,13 +221,14 @@ impl Preprocessor {
..Default::default()
};
nodes.insert(NodeId(input_count as u64), document_node);
let main_node_id = NodeId(wrapper_input_count as u64);
nodes.insert(main_node_id, document_node);
// If memoize is requested, append a Memoize node after the main node and redirect the export through it
let export_node_id = if *memoize {
let memoize_node_id = NodeId(input_count as u64 + 1);
let memoize_node_id = NodeId(wrapper_input_count as u64 + 1);
let memoize_node = DocumentNode {
inputs: vec![NodeInput::node(NodeId(input_count as u64), 0)],
inputs: vec![NodeInput::node(main_node_id, 0)],
implementation: DocumentNodeImplementation::ProtoNode(graphene_core::memo::memoize::IDENTIFIER.clone()),
visible: true,
..Default::default()
@@ -213,7 +236,7 @@ impl Preprocessor {
nodes.insert(memoize_node_id, memoize_node);
memoize_node_id
} else {
NodeId(input_count as u64)
main_node_id
};
let node = DocumentNode {
@@ -256,6 +279,14 @@ impl Preprocessor {
}
}
fn injected_field_input(field: &registry::FieldMetadata) -> NodeInput {
match field.value_source {
RegistryValueSource::Scope(data) => NodeInput::scope(data),
RegistryValueSource::SourceId => NodeInput::Reflection(DocumentNodeMetadata::SourceId),
_ => NodeInput::value(TaggedValue::None, false),
}
}
pub fn node_inputs(fields: &[registry::FieldMetadata], first_node_io: &NodeIOTypes) -> Vec<NodeInput> {
fields
.iter()

22
test.rs
View File

@@ -1,22 +0,0 @@
trait Attr {
fn name() -> &'static str
}
impl Attr for bool {
fn name() {"condition"}
}
struct Opacity(f64);
impl Attr for Opacity {
fn name() {"opacity"}
}
#[node]
fn opacity<T>(_: impl Ctx, input: T, x: ReadAttr<bool>, opacity: WriteAttr<Opacity>) -> T {
if x {
*opacity = 1.;
}
input
}