Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 11 additions & 1 deletion desktop/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,17 @@ impl App {
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));
// A wake within a frame continues it, while waiting out a frame or idling starts a new one
let next_frame = match node_runtime.until_next_frame() {
Some(wait) => {
thread::sleep(wait);
true
}
None => start_render_receiver.recv_timeout(Duration::from_millis(10)).is_err(),
};
if next_frame {
node_runtime.begin_frame();
}
if exiting_clone.load(Ordering::Relaxed) {
break;
}
Expand Down
17 changes: 16 additions & 1 deletion desktop/wrapper/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ use graphite_editor::builder::{Builder, EditorBuilder, RuntimeBuilder};
use graphite_editor::messages::frontend::FrontendMessage;
use graphite_editor::node_graph_executor::NodeRuntime;

use graphite_editor::task::{Clock, Scheduling};
use message_dispatcher::DesktopWrapperMessageDispatcher;
use messages::{DesktopFrontendMessage, DesktopWrapperMessage};
use std::sync::Arc;
use std::time::{Duration, Instant};

pub use graph_craft::application_io::resource::MmapResourceStorage;
pub use graphite_editor::consts::{DOUBLE_CLICK_MILLISECONDS, FILE_EXTENSION};
Expand Down Expand Up @@ -61,7 +63,12 @@ impl DesktopWrapper {
.with_resource_storage(resource_storage)
.with_document_store(document_store),
)
.with_runtime(RuntimeBuilder::new().with_wake(runtime_wake).with_application_io(application_io))
.with_runtime(
RuntimeBuilder::new(Arc::new(InstantClock(Instant::now())))
.with_wake(runtime_wake)
.with_application_io(application_io)
.with_scheduling(Scheduling::default()),
)
.with_spawner(spawner)
.with_random_seed(random_seed)
.build();
Expand All @@ -76,6 +83,14 @@ impl DesktopWrapper {
}
}

struct InstantClock(Instant);

impl Clock for InstantClock {
fn now(&self) -> Duration {
self.0.elapsed()
}
}

pub fn deserialize_editor_message(data: &[u8]) -> Option<DesktopWrapperMessage> {
let message = graphite_wasm_wrapper::native_communication::decode_editor_command(data)?;
Some(DesktopWrapperMessage::FromWeb(message.into()))
Expand Down
39 changes: 28 additions & 11 deletions editor/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::application::{Editor, Environment};
use crate::dispatcher::Dispatcher;
use crate::messages::prelude::*;
use crate::node_graph_executor::{NodeGraphExecutor, NodeRuntime};
use crate::task::{Spawn, Spawner};
use crate::task::{Clock, Scheduling, Spawn, Spawner};
use document_container::store::DocumentStore;
use graph_craft::application_io::PlatformApplicationIo;
use graph_craft::application_io::resource::ResourceStorage;
Expand Down Expand Up @@ -96,6 +96,8 @@ impl Builder<ReadyEditorBuilder, ReadyRuntimeBuilder, Spawner, u64> {
let RuntimeBuilder {
wake: runtime_wake,
mut application_io,
scheduling,
clock,
} = runtime;

graphene_std::uuid::set_uuid_seed(random_seed);
Expand All @@ -107,7 +109,8 @@ impl Builder<ReadyEditorBuilder, ReadyRuntimeBuilder, Spawner, u64> {

application_io.inject_resource_proxy(dispatcher.message_handlers.resource_storage_message_handler.resources());
node_runtime.replace_application_io(application_io);
node_runtime.replace_task_executor(spawner, runtime_wake);
node_runtime.replace_task_executor(spawner, runtime_wake, clock);
node_runtime.set_scheduling(scheduling);

(Editor { dispatcher }, node_runtime)
}
Expand Down Expand Up @@ -184,17 +187,18 @@ impl<W, E, R> EditorBuilder<W, E, R, ()> {
pub struct RuntimeBuilder<W = (), A = ()> {
wake: W,
application_io: A,
clock: Arc<dyn Clock>,
scheduling: Scheduling,
}

impl RuntimeBuilder {
pub fn new() -> Self {
Self { wake: (), application_io: () }
}
}

impl Default for RuntimeBuilder {
fn default() -> Self {
Self::new()
pub fn new(clock: Arc<dyn Clock>) -> Self {
Self {
wake: (),
application_io: (),
clock,
scheduling: Scheduling::default(),
}
}
}

Expand All @@ -204,12 +208,25 @@ impl<A> RuntimeBuilder<(), A> {
RuntimeBuilder {
wake: Arc::new(wake),
application_io: self.application_io,
clock: self.clock,
scheduling: self.scheduling,
}
}
}

impl<W> RuntimeBuilder<W, ()> {
pub fn with_application_io(self, application_io: PlatformApplicationIo) -> RuntimeBuilder<W, PlatformApplicationIo> {
RuntimeBuilder { wake: self.wake, application_io }
RuntimeBuilder {
wake: self.wake,
application_io,
clock: self.clock,
scheduling: self.scheduling,
}
}
}

impl<W, A> RuntimeBuilder<W, A> {
pub fn with_scheduling(self, scheduling: Scheduling) -> Self {
Self { scheduling, ..self }
}
}
139 changes: 120 additions & 19 deletions editor/src/node_graph_executor/runtime.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::*;
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
use crate::task::{Spawn, Spawner, Task};
use crate::task::{Scheduling, Spawn, Spawner, Task};
use glam::{DAffine2, DVec2, UVec2};
use graph_craft::application_io::resource::ResourceRegistry;
use graph_craft::application_io::{PlatformApplicationIo, PlatformEditorApi};
Expand All @@ -18,16 +18,16 @@ use graphene_std::ops::ConvertAsync;
use graphene_std::platform_application_io::canvas_utils::{Canvas, CanvasSurface, CanvasSurfaceHandle};
use graphene_std::raster_types::Raster;
use graphene_std::renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRender, SvgSegment};
use graphene_std::runtime::AsyncRuntime;
use graphene_std::runtime::{AsyncRuntime, Clock};
use graphene_std::transform::RenderQuality;
use graphene_std::vector::style::RenderMode;
use graphene_std::vector::{Vector, VectorGeometry, paths_from_list_row_attributes};
use graphene_std::{Artboard, Context, Graphic, NodeError, Poll};
use interpreted_executor::dynamic_executor::{DynamicExecutor, ResolvedDocumentNodeTypesDelta};
use interpreted_executor::util::wrap_network_in_scope;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{Receiver, Sender};
use std::time::Duration;

/// Persistent data between graph executions. It's updated via message passing from the editor thread with [`GraphRuntimeRequest`]`.
/// Some of these fields are put into a [`PlatformEditorApi`] which is passed to the final compiled graph network upon each execution.
Expand All @@ -40,13 +40,16 @@ pub struct NodeRuntime {
receiver: Receiver<GraphRuntimeRequest>,
sender: Sender<NodeGraphUpdate>,
editor_wake: Wake,
dirty: Arc<AtomicBool>,
/// A re-evaluation waiting for the next frame, or for the end of an export.
rerun: bool,
pending: Option<ExecutionRequest>,
pending_export: Option<ExecutionRequest>,
editor_preferences: EditorPreferences,
old_graph: Option<NodeNetwork>,
update_thumbnails: bool,
async_runtime: Option<AsyncRuntime>,
scheduling: Scheduling,
frame_start: Duration,

editor_api: Arc<PlatformEditorApi>,
resources: ResourceRegistry,
Expand Down Expand Up @@ -107,14 +110,16 @@ impl NodeRuntime {
receiver,
sender,
editor_wake,
dirty: Default::default(),
rerun: false,
pending: None,
pending_export: None,
editor_preferences: EditorPreferences::default(),
old_graph: None,
resources: ResourceRegistry::default(),
update_thumbnails: true,
async_runtime: None,
scheduling: Scheduling::default(),
frame_start: Duration::ZERO,

editor_api: PlatformEditorApi {
editor_preferences: Box::new(EditorPreferences::default()),
Expand Down Expand Up @@ -176,8 +181,14 @@ impl NodeRuntime {
}
}

if self.dirty.swap(false, Ordering::Acquire) && execution.is_none() {
execution = self.pending_export.or(self.pending).map(GraphRuntimeRequest::ExecutionRequest);
// An explicit request already reads the newest generations; a re-evaluation only starts while the frame has budget left
let rerun = std::mem::take(&mut self.rerun) | self.async_runtime().take_bumped();
if rerun && execution.is_none() {
if self.frame_has_budget() {
execution = self.pending_export.or(self.pending).map(GraphRuntimeRequest::ExecutionRequest);
} else {
self.rerun = true;
}
}

// Eydropper should use the same time and pointer to not invalidate the cache
Expand Down Expand Up @@ -268,7 +279,7 @@ impl NodeRuntime {
self.pending_export = Some(request);
return None;
}
(true, _) => self.dirty.store(true, Ordering::Release),
(true, _) => self.rerun = true,
_ => {}
}

Expand Down Expand Up @@ -611,33 +622,50 @@ impl NodeRuntime {
.into();
}

pub(crate) fn replace_task_executor(&mut self, spawner: Spawner, wake: Wake) {
let spawn = NodeTaskExecutor {
spawner,
dirty: self.dirty.clone(),
wake,
};
self.async_runtime = Some(AsyncRuntime::new(Arc::new(spawn)));
pub(crate) fn replace_task_executor(&mut self, spawner: Spawner, wake: Wake, clock: Arc<dyn Clock>) {
self.async_runtime = Some(AsyncRuntime::new(Arc::new(NodeTaskExecutor { spawner, wake }), clock));
self.begin_frame();
}

pub fn set_scheduling(&mut self, scheduling: Scheduling) {
self.scheduling = scheduling;
}

fn async_runtime(&self) -> &AsyncRuntime {
/// Called by the host once the previous output had a chance to be shown.
pub fn begin_frame(&mut self) {
self.frame_start = self.async_runtime().clock().now();
}

/// How long until the next frame boundary, if a re-evaluation is waiting for it.
pub fn until_next_frame(&self) -> Option<Duration> {
let budget = self.scheduling.frame_budget.filter(|budget| !budget.is_zero())?;
let elapsed = self.frame_elapsed().as_nanos() % budget.as_nanos();
self.rerun.then(|| budget - Duration::from_nanos(elapsed as u64))
}

fn frame_elapsed(&self) -> Duration {
self.async_runtime().clock().now().saturating_sub(self.frame_start)
}

fn frame_has_budget(&self) -> bool {
self.scheduling.frame_budget.is_none_or(|budget| self.frame_elapsed() < budget)
}

pub(crate) fn async_runtime(&self) -> &AsyncRuntime {
self.async_runtime.as_ref().expect("the task executor is installed before the first graph update")
}
}

struct NodeTaskExecutor {
spawner: Spawner,
dirty: Arc<AtomicBool>,
wake: Wake,
}

impl Spawn for NodeTaskExecutor {
fn spawn(&self, task: Task) {
let dirty = self.dirty.clone();
let wake = self.wake.clone();
self.spawner.spawn(Box::pin(async move {
task.await;
dirty.store(true, Ordering::Release);
wake();
}));
}
Expand Down Expand Up @@ -750,3 +778,76 @@ fn navigate_to_network_mut<'a>(network: &'a mut NodeNetwork, path: &[NodeId]) ->
}
Some(current)
}

#[cfg(test)]
mod test {
use crate::messages::prelude::*;
use crate::task::Scheduling;
use crate::test_utils::{EditorTestUtils, FakeClock};
use glam::{DVec2, UVec2};
use graphene_std::SourceId;
use graphene_std::runtime::Runtime;
use std::sync::Arc;
use std::time::Duration;

const SOURCE: SourceId = SourceId(1);

async fn editor(clock: &Arc<FakeClock>) -> EditorTestUtils {
let mut editor = EditorTestUtils::create_with(Scheduling::default(), clock.clone());
editor.new_document().await;
editor.draw_rect(0., 0., 100., 100.).await;
editor.runtime.async_runtime().retain_sources(&[SOURCE]);
editor.runtime.begin_frame();
editor
}

fn finish_task(editor: &EditorTestUtils) {
editor.runtime.async_runtime().spawn(SOURCE, Box::pin(async {}));
}

/// Whether one runtime run re-evaluated the graph.
async fn evaluates(editor: &mut EditorTestUtils) -> bool {
editor.runtime.run().await;
let messages = editor.editor.handle_message(Message::Wake);
messages.iter().any(|message| matches!(message, FrontendMessage::UpdateDocumentArtwork { .. }))
}

#[tokio::test]
async fn reevaluations_past_the_frame_budget_wait_for_the_next_frame() {
let clock = Arc::new(FakeClock::default());
let mut editor = editor(&clock).await;

finish_task(&editor);
finish_task(&editor);
assert!(evaluates(&mut editor).await);
assert!(!evaluates(&mut editor).await, "two finished tasks re-evaluated twice");

clock.advance(Duration::from_millis(20));
finish_task(&editor);
assert!(!evaluates(&mut editor).await);
assert_eq!(editor.runtime.until_next_frame(), Some(Duration::from_millis(12)));

editor.runtime.begin_frame();
assert!(evaluates(&mut editor).await);
assert_eq!(editor.runtime.until_next_frame(), None);
}

#[tokio::test]
async fn explicit_requests_ignore_the_frame_budget() {
let clock = Arc::new(FakeClock::default());
let mut editor = editor(&clock).await;

clock.advance(Duration::from_millis(100));
finish_task(&editor);
let portfolio = &mut editor.editor.dispatcher.message_handlers.portfolio_message_handler;
let document_id = portfolio.active_document_id.unwrap();
let document = portfolio.documents.get_mut(&document_id).unwrap();
portfolio
.executor
.submit_current_node_graph_evaluation(document, document_id, UVec2::ONE, 1., Default::default(), DVec2::ZERO)
.unwrap();

assert!(evaluates(&mut editor).await);
assert!(!evaluates(&mut editor).await, "the finished task re-evaluated after the explicit request");
}
}
2 changes: 1 addition & 1 deletion editor/src/task.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pub use graphene_std::runtime::{Spawn, Task};
pub use graphene_std::runtime::{Clock, Scheduling, Spawn, Task};
use std::sync::Arc;

pub type Spawner = Arc<dyn Spawn + Send + Sync>;
Loading
Loading