Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
a69a859
Add editor task module, drop NodeGraphUpdateSender
timon-schelling Sep 28, 2026
0695cf1
Remove Node::reset and unused value nodes
timon-schelling Sep 29, 2026
2ecccf2
Remove unneeded async from vector nodes
timon-schelling Sep 29, 2026
81f0798
Remove unneeded async from graphic nodes
timon-schelling Sep 29, 2026
fe482b8
Remove unneeded async from raster nodes
timon-schelling Sep 29, 2026
0d43744
Remove unneeded async from the remaining nodes
timon-schelling Sep 29, 2026
206aee1
Make most node fns private
timon-schelling Sep 29, 2026
26be1d3
Make node construction sync
timon-schelling Sep 29, 2026
c7eb37a
Remove block_on around node construction
timon-schelling Sep 29, 2026
b725e05
Move GPU readback into ConvertAsync
timon-schelling Sep 29, 2026
12c1638
Make Convert sync
timon-schelling Sep 29, 2026
3f841fe
Make wgpu pipelines and vello rendering sync
timon-schelling Sep 29, 2026
f09b77c
Make convert, brush and render nodes sync
timon-schelling Sep 29, 2026
dea878a
Pass editor API as owned Arc
timon-schelling Sep 29, 2026
28f2246
Take scope inputs by value
timon-schelling Sep 29, 2026
bc6e52b
Move Spawn and Task into core-types
timon-schelling Sep 30, 2026
bf51294
Add Poll type and status tracking
timon-schelling Sep 30, 2026
45f0de4
Add source generations to context
timon-schelling Sep 30, 2026
a5703b4
Stop storing context features in the document
timon-schelling Oct 10, 2026
c2de87e
Track source ids in context modification
timon-schelling Oct 10, 2026
6e5b814
Add async runtime and memo table
timon-schelling Oct 9, 2026
bfe51f5
Pass runtime through scope, expose source ids
timon-schelling Oct 9, 2026
436b41c
Set up async runtime in editor, CLI and benches
timon-schelling Oct 9, 2026
208d541
Give async nodes runtime and source id inputs
timon-schelling Oct 9, 2026
6078782
Classify node return types in the macro
timon-schelling Oct 9, 2026
7a9fb61
Make Node eval sync and return Poll
timon-schelling Oct 9, 2026
a1b33d5
Make lazy input nodes, tests and benches sync
timon-schelling Oct 9, 2026
0cf17f8
Rerender when a spawned task finishes
timon-schelling Oct 12, 2026
325061e
Rerender in CLI until all tasks are done
timon-schelling Oct 12, 2026
3bc1a28
Abort pending tasks when node is dropped
timon-schelling Oct 13, 2026
35b7ead
Trace errors through Poll node inputs
timon-schelling Oct 13, 2026
ef3c84c
Let node errors carry upstream errors
timon-schelling Oct 14, 2026
52da8c7
Resolve error traces to the origin node
timon-schelling Oct 14, 2026
eef450a
Show evaluation errors in the node graph
timon-schelling Oct 14, 2026
0bae066
Default unmapped item primaries to a value
timon-schelling Oct 13, 2026
cd389d5
Add debug nodes
timon-schelling Oct 14, 2026
bb7461c
Memoize fallback results too
timon-schelling Oct 15, 2026
913ac42
Show multiple node graph errors
timon-schelling Oct 14, 2026
918ea18
Report node errors in CLI
timon-schelling Oct 14, 2026
b728304
Attach input fallback error as upstream
timon-schelling Oct 14, 2026
ea2b9e5
Keep input fallback error when node fails
timon-schelling Oct 15, 2026
f818fbd
Keep Poll status in render cache
timon-schelling Oct 15, 2026
826018d
Keep last frame while graph is pending
timon-schelling Oct 15, 2026
396bfcc
Treat partial export output as pending
timon-schelling Oct 15, 2026
42926b8
Move environment from global to dispatcher
timon-schelling Oct 16, 2026
fcf6747
Add editor builder, drop global node runtime
timon-schelling Oct 16, 2026
20034b6
Add Wake message, rerun graph when tasks finish
timon-schelling Oct 16, 2026
87e769c
Add source generation tests
timon-schelling Oct 16, 2026
cf9b898
Add test for node error origin
timon-schelling Oct 16, 2026
4771b5e
Add render status overlay
timon-schelling Oct 17, 2026
11cff62
Replace error artwork with render status
timon-schelling Oct 17, 2026
e1ee8a4
Poll node graph through portfolio message
timon-schelling Oct 17, 2026
f21e350
Minor review fixups
TrueDoctor Oct 17, 2026
b03061a
Remove future type and macro
TrueDoctor Oct 17, 2026
13299fc
Merge master into async-refactor
timon-schelling Oct 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

70 changes: 39 additions & 31 deletions desktop/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ use crate::input::InputState;
use crate::persist;
use crate::preferences;
use crate::render::{RenderError, RenderState};
use crate::spawner::TokioSpawner;
use crate::ui::{InputEvent, UiCommand, UiInstance};
use crate::window::Window;
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, IngestAction, Preferences};
use crate::wrapper::{DesktopWrapper, FolderStore, MmapResourceStorage, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
use crate::wrapper::{DesktopWrapper, FolderStore, MmapResourceStorage, WgpuContext, serialize_frontend_messages};

pub(crate) struct App {
render_state: Option<RenderState>,
Expand Down Expand Up @@ -77,36 +78,47 @@ impl App {

let exiting = Arc::new(AtomicBool::new(false));

let rendering_app_event_scheduler = app_event_scheduler.clone();
let (start_render_sender, start_render_receiver) = std::sync::mpsc::sync_channel(1);
let exiting_clone = exiting.clone();
std::thread::spawn(move || {
let runtime = tokio::runtime::Runtime::new().unwrap();
loop {
let result = runtime.block_on(DesktopWrapper::execute_node_graph());
rendering_app_event_scheduler.schedule(AppEvent::NodeGraphExecutionResult(result));
let _ = start_render_receiver.recv_timeout(Duration::from_millis(10));
if exiting_clone.load(Ordering::Relaxed) {
break;
}
}
});

let async_runtime = tokio::runtime::Builder::new_multi_thread()
.thread_name("graphite-async")
.enable_all()
.build()
.expect("failed to construct tokio runtime");

let resource_storage = MmapResourceStorage::new(dirs::app_resources_dir()).expect("Failed to initialize on-disk resource storage");

// Wake the winit event loop when an editor future completes.
let wake_scheduler = app_event_scheduler.clone();
let wake = Arc::new(move || {
let editor_wake = move || {
wake_scheduler.schedule(AppEvent::DesktopWrapperMessage(DesktopWrapperMessage::Wake));
});
let desktop_wrapper = DesktopWrapper::new(
};
let runtime_wake_sender = start_render_sender.clone();
let runtime_wake = move || {
let _ = runtime_wake_sender.try_send(());
};
let (desktop_wrapper, mut node_runtime) = DesktopWrapper::new(
rand::rng().random(),
Arc::new(resource_storage),
Arc::new(FolderStore::new(dirs::app_autosave_documents_dir())),
wgpu_context.clone(),
wake,
TokioSpawner::new(async_runtime.handle().clone()),
editor_wake,
runtime_wake,
);

let rendering_app_event_scheduler = app_event_scheduler.clone();
let exiting_clone = exiting.clone();
std::thread::spawn(move || {
loop {
let texture = async_runtime.block_on(node_runtime.run());

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.

P2: The runtime thread wakes the editor unconditionally on every 10 ms polling iteration, causing idle sessions to dispatch editor graph polling roughly 100 times per second. Only wake the editor when requests or runtime results actually require processing, and block on a notification instead of polling continuously.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At desktop/src/app.rs, line 113:

<comment>The runtime thread wakes the editor unconditionally on every 10 ms polling iteration, causing idle sessions to dispatch editor graph polling roughly 100 times per second. Only wake the editor when requests or runtime results actually require processing, and block on a notification instead of polling continuously.</comment>

<file context>
@@ -77,36 +78,47 @@ impl App {
+		let exiting_clone = exiting.clone();
+		std::thread::spawn(move || {
+			loop {
+				let texture = async_runtime.block_on(node_runtime.run());
+				rendering_app_event_scheduler.schedule(AppEvent::ViewportTexture(texture));
+				let _ = start_render_receiver.recv_timeout(Duration::from_millis(10));
</file context>

rendering_app_event_scheduler.schedule(AppEvent::ViewportTexture(texture));
let _ = start_render_receiver.recv_timeout(Duration::from_millis(10));
if exiting_clone.load(Ordering::Relaxed) {
break;
}
}
});

Self {
render_state: None,
wgpu_context,
Expand Down Expand Up @@ -436,19 +448,15 @@ impl App {
}
}
AppEvent::DesktopWrapperMessage(message) => self.dispatch_desktop_wrapper_message(message),
AppEvent::NodeGraphExecutionResult(result) => match result {
NodeGraphExecutionResult::HasRun(texture) => {
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::PollNodeGraphEvaluation);
if let Some(texture) = texture
&& let Some(render_state) = self.render_state.as_mut()
&& let Some(window) = self.window.as_ref()
{
render_state.bind_viewport_texture(texture);
window.request_redraw();
}
AppEvent::ViewportTexture(texture) => {
if let Some(texture) = texture
&& let Some(render_state) = self.render_state.as_mut()
&& let Some(window) = self.window.as_ref()
{
render_state.bind_viewport_texture(texture);
window.request_redraw();
}
NodeGraphExecutionResult::NotRun => {}
},
}
AppEvent::UiUpdate(texture) => {
if let Some(render_state) = self.render_state.as_mut() {
render_state.bind_ui_texture(texture);
Expand Down
4 changes: 2 additions & 2 deletions desktop/src/event.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
use crate::ui::Cursor;
use crate::wrapper::NodeGraphExecutionResult;
use crate::wrapper::Texture;
use crate::wrapper::messages::DesktopWrapperMessage;

pub(crate) enum AppEvent {
UiUpdate(wgpu::Texture),
CursorChange(Cursor),
WebCommunicationInitialized,
DesktopWrapperMessage(DesktopWrapperMessage),
NodeGraphExecutionResult(NodeGraphExecutionResult),
ViewportTexture(Option<Texture>),
ClipboardRead,
ClipboardWrite {
content: String,
Expand Down
1 change: 1 addition & 0 deletions desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ mod persist;
mod preferences;
mod render;
mod socket;
mod spawner;
mod window;

pub(crate) mod consts;
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/render/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ impl RenderState {
return;
};
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 {
Ok(texture) => {
self.overlays_texture = Some(texture);
Expand Down
13 changes: 13 additions & 0 deletions desktop/src/spawner.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use crate::wrapper::{Spawn, Task};

pub(crate) struct TokioSpawner(tokio::runtime::Handle);
impl TokioSpawner {
pub(crate) fn new(handle: tokio::runtime::Handle) -> Self {
Self(handle)
}
}
impl Spawn for TokioSpawner {
fn spawn(&self, task: Task) {
self.0.spawn(task);
}
}
3 changes: 1 addition & 2 deletions desktop/wrapper/src/handle_desktop_wrapper_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
dispatcher.queue_editor_message(*message);
}
DesktopWrapperMessage::Wake => {
dispatcher.queue_editor_message(EditorMessage::Future(FutureMessage::Wake));
dispatcher.queue_editor_message(EditorMessage::Wake);
}
DesktopWrapperMessage::Input(message) => {
dispatcher.queue_editor_message(EditorMessage::InputPreprocessor(message));
Expand All @@ -35,7 +35,6 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
dispatcher.respond(DesktopFrontendMessage::WriteFile { path, content });
}
},
DesktopWrapperMessage::PollNodeGraphEvaluation => dispatcher.poll_node_graph_evaluation(),
DesktopWrapperMessage::UpdateMaximized { maximized } => {
let message = FrontendMessage::UpdateMaximized { maximized };
dispatcher.queue_editor_message(message);
Expand Down
44 changes: 26 additions & 18 deletions desktop/wrapper/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use graph_craft::application_io::PlatformApplicationIo;
use graph_craft::application_io::resource::ResourceStorage;
use graphite_editor::application::{Editor, Environment, Host, Platform};
use graphite_editor::builder::{Builder, EditorBuilder, RuntimeBuilder};
use graphite_editor::messages::frontend::FrontendMessage;
use graphite_editor::messages::prelude::Wake;
use graphite_editor::node_graph_executor::NodeRuntime;

use message_dispatcher::DesktopWrapperMessageDispatcher;
use messages::{DesktopFrontendMessage, DesktopWrapperMessage};
Expand All @@ -11,6 +12,7 @@ use std::sync::Arc;
pub use graph_craft::application_io::resource::MmapResourceStorage;
pub use graphite_editor::consts::{DOUBLE_CLICK_MILLISECONDS, FILE_EXTENSION};
pub use graphite_editor::document_store::{DocumentStore, FolderStore};
pub use graphite_editor::task::{Spawn, Task};
pub use wgpu_executor::Texture;
pub use wgpu_executor::WgpuBackends;
pub use wgpu_executor::WgpuContext;
Expand All @@ -33,7 +35,15 @@ pub struct DesktopWrapper {
}

impl DesktopWrapper {
pub fn new(uuid_random_seed: u64, resource_storage: Arc<dyn ResourceStorage>, document_store: Arc<dyn DocumentStore>, wgpu_context: WgpuContext, schedule_wake: Wake) -> Self {
pub fn new(
random_seed: u64,
resource_storage: Arc<dyn ResourceStorage>,
document_store: Arc<dyn DocumentStore>,
wgpu_context: WgpuContext,
spawner: impl Spawn + Send + Sync + 'static,
editor_wake: impl Fn() + Send + Sync + 'static,
runtime_wake: impl Fn() + Send + Sync + 'static,
) -> (Self, NodeRuntime) {
#[cfg(target_os = "windows")]
let host = Host::Windows;
#[cfg(target_os = "macos")]
Expand All @@ -43,29 +53,27 @@ impl DesktopWrapper {
let env = Environment { platform: Platform::Desktop, host };
let application_io = PlatformApplicationIo::new_with_context(wgpu_context);

Self {
editor: Editor::new(env, uuid_random_seed, resource_storage, document_store, application_io, schedule_wake),
}
let (editor, runtime) = Builder::new()
.with_editor(
EditorBuilder::new()
.with_wake(editor_wake)
.with_environment(env)
.with_resource_storage(resource_storage)
.with_document_store(document_store),
)
.with_runtime(RuntimeBuilder::new().with_wake(runtime_wake).with_application_io(application_io))
.with_spawner(spawner)
.with_random_seed(random_seed)
.build();

(Self { editor }, runtime)
}

pub fn dispatch(&mut self, message: DesktopWrapperMessage) -> Vec<DesktopFrontendMessage> {
let mut executor = DesktopWrapperMessageDispatcher::new(&mut self.editor);
executor.queue_desktop_wrapper_message(message);
executor.execute()
}

pub async fn execute_node_graph() -> NodeGraphExecutionResult {
let result = graphite_editor::node_graph_executor::run_node_graph().await;
match result {
(true, texture) => NodeGraphExecutionResult::HasRun(texture),
(false, _) => NodeGraphExecutionResult::NotRun,
}
}
}

pub enum NodeGraphExecutionResult {
HasRun(Option<Texture>),
NotRun,
}

pub fn deserialize_editor_message(data: &[u8]) -> Option<DesktopWrapperMessage> {
Expand Down
12 changes: 0 additions & 12 deletions desktop/wrapper/src/message_dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,6 @@ impl<'a> DesktopWrapperMessageDispatcher<'a> {
self.responses.push(response);
}

pub(super) fn poll_node_graph_evaluation(&mut self) {
let mut responses = VecDeque::new();
if let Err(e) = self.editor.poll_node_graph_evaluation(&mut responses)
&& e != "No active document"
{
tracing::error!("Error polling node graph: {}", e);
}
while let Some(message) = responses.pop_front() {
self.queue_editor_message(message);
}
}

fn process_queue(&mut self) {
let mut frontend_messages = Vec::new();

Expand Down
1 change: 0 additions & 1 deletion desktop/wrapper/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ pub enum DesktopWrapperMessage {
Input(InputMessage),
IngestFile { path: PathBuf, content: Vec<u8>, action: IngestAction },
SaveFileDialogResult { path: PathBuf, context: SaveFileDialogContext },
PollNodeGraphEvaluation,
UpdateMaximized { maximized: bool },
UpdateFullscreen { fullscreen: bool },
LoadPersistedState { state: PersistedState },
Expand Down
1 change: 0 additions & 1 deletion document/graph-storage/src/attributes/attr.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
pub mod node {
pub const CALL_ARGUMENT: &str = "call_argument";
pub const CONTEXT_FEATURES: &str = "context_features";
pub const VISIBLE: &str = "visible";
pub const SKIP_DEDUPLICATION: &str = "skip_deduplication";
pub const REFLECTION_METADATA: &str = "reflection_metadata";
Expand Down
9 changes: 0 additions & 9 deletions document/graph-storage/src/from_runtime.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use std::collections::HashMap;

use core_types::Context;
use core_types::context::ContextDependencies;
use core_types::uuid::NodeId as RuntimeNodeId;
use graph_craft::concrete;
use graph_craft::document::value::TaggedValue;
Expand Down Expand Up @@ -447,9 +446,6 @@ fn convert_node<M: NodeMetadataSource + ?Sized>(
attributes
.set_if_not_default(node::CALL_ARGUMENT, &doc_node.call_argument, &concrete!(Context), timestamp)
.map_err(map_serialization_error(node::CALL_ARGUMENT))?;
attributes
.set_if_not_default(node::CONTEXT_FEATURES, &doc_node.context_features, &ContextDependencies::default(), timestamp)
.map_err(map_serialization_error(node::CONTEXT_FEATURES))?;
attributes
.set_if_not_default(node::VISIBLE, &doc_node.visible, &true, timestamp)
.map_err(map_serialization_error(node::VISIBLE))?;
Expand Down Expand Up @@ -477,11 +473,6 @@ pub fn encode_call_argument(call_argument: &core_types::Type) -> Result<crate::A
encode_if_not_default(node::CALL_ARGUMENT, call_argument, &concrete!(Context))
}

/// The `context_features` attribute write, cleared at its default like [`encode_call_argument`].
pub fn encode_context_features(context_features: &ContextDependencies) -> Result<crate::AttributeDelta, ConversionError> {
encode_if_not_default(node::CONTEXT_FEATURES, context_features, &ContextDependencies::default())
}

fn encode_if_not_default<T: Serialize + PartialEq>(key: &str, value: &T, default: &T) -> Result<crate::AttributeDelta, ConversionError> {
let value = if value == default { None } else { Some(to_value(value).map_err(map_serialization_error(key))?) };

Expand Down
Loading
Loading