Skip to content
Open
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
17 changes: 17 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ members = [
"document/migrations/nodes",
"document/migrations/compiled",
"document/peer-transport",
"document/live",
"editor",
"frontend/wrapper",
"libraries/dyn-any",
Expand Down Expand Up @@ -117,6 +118,7 @@ migration-formats = { path = "document/migrations/formats" }
migration-compiled = { path = "document/migrations/compiled" }
migration-nodes = { path = "document/migrations/nodes" }
peer-transport = { path = "document/peer-transport" }
document-live = { path = "document/live" }
raster-nodes = { path = "node-graph/nodes/raster" }
graphene-std = { path = "node-graph/nodes/gstd" }
interpreted-executor = { path = "node-graph/interpreted-executor" }
Expand Down
6 changes: 6 additions & 0 deletions document/format/src/resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ impl<L: Layout + Send + Sync> LoadResource for Gdd<L> {

pub struct ResourceProxy<T: Layout>(AnyContainer, T);

impl<T: Layout + Clone> Clone for ResourceProxy<T> {
fn clone(&self) -> Self {
Self(self.0.clone(), self.1.clone())
}
}

impl<L: Layout + Send + Sync> LoadResource for ResourceProxy<L> {
fn load(&self, hash: ResourceHash) -> ResourceFuture<'_> {
Box::pin(async move {
Expand Down
21 changes: 21 additions & 0 deletions document/live/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "document-live"
description = "A memory-backed document following or hosting a collaborative session, for viewers and embedders"
version.workspace = true
license.workspace = true
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
publish.workspace = true

[dependencies]
document-container = { workspace = true }
document-format = { workspace = true, features = ["network"] }
document-graph-storage = { workspace = true }
peer-transport = { workspace = true }
graph-craft = { workspace = true }
thiserror = { workspace = true }
log = { workspace = true }

[lints]
workspace = true
118 changes: 118 additions & 0 deletions document/live/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//! A memory-backed document in a collaborative session. The caller spawns the transport driver.

use document_container::AnyContainer;
use document_container::backends::memory::MemoryBackend;
use document_format::{GddV1, GddV1Layout, ResourceProxy};
use document_graph_storage::{PeerId, UserId};
use graph_craft::application_io::resource::ResourceHash;
use graph_craft::document::NodeNetwork;
use peer_transport::{Room, SyncTarget};

pub use document_format::Error as FormatError;
pub use document_graph_storage::to_runtime::ConversionError;
pub use peer_transport::{DEFAULT_SIGNALING_SERVER, Event, MessageLoopFuture, Role, SessionToken};

#[derive(Debug, thiserror::Error)]
pub enum LiveError {
#[error(transparent)]
Format(#[from] FormatError),
#[error(transparent)]
Conversion(#[from] ConversionError),
}

pub struct LiveDocument {
gdd: GddV1,
token: SessionToken,
}

impl LiveDocument {
/// Host an empty document under `token`. `spawn` must drive the returned future for the whole session.
pub async fn host(signaling_server: &str, token: SessionToken, peer: PeerId, user: UserId, spawn: impl FnOnce(MessageLoopFuture)) -> Result<Self, LiveError> {
let gdd = Self::empty_document(peer, user).await?;
Self::host_document(gdd, signaling_server, token, user, spawn)
}

pub fn host_document(mut gdd: GddV1, signaling_server: &str, token: SessionToken, user: UserId, spawn: impl FnOnce(MessageLoopFuture)) -> Result<Self, LiveError> {
gdd.share(Self::connect(signaling_server, token, spawn), user)?;
Ok(Self { gdd, token })
}

pub async fn join(signaling_server: &str, token: SessionToken, peer: PeerId, user: UserId, spawn: impl FnOnce(MessageLoopFuture)) -> Result<Self, LiveError> {
let mut gdd = Self::empty_document(peer, user).await?;
gdd.connect(Self::connect(signaling_server, token, spawn), user)?;
Ok(Self { gdd, token })
}

fn connect(signaling_server: &str, token: SessionToken, spawn: impl FnOnce(MessageLoopFuture)) -> Room {
let (room, driver) = Room::connect(&token.signaling_url(signaling_server));
spawn(driver);
room
}

async fn empty_document(peer: PeerId, user: UserId) -> Result<GddV1, FormatError> {
let version = env!("CARGO_PKG_VERSION").to_string();
GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, peer, user, peer.0, version.clone(), version)
}

pub fn token(&self) -> SessionToken {
self.token
}

pub fn role(&self) -> Option<Role> {
self.gdd.role()
}

pub fn is_synced(&self) -> bool {
self.gdd.is_synced()
}

pub fn is_ready(&self) -> bool {
self.is_synced() && SyncTarget::missing_resources(&self.gdd).is_empty()
}

pub fn missing_resources(&self) -> Vec<ResourceHash> {
let mut missing: Vec<ResourceHash> = SyncTarget::missing_resources(&self.gdd).into_iter().collect();
missing.sort_unstable();
missing
}

/// Apply what peers sent and answer their requests. Call it regularly.
pub async fn poll(&mut self) -> Vec<Event> {
let events = self.gdd.poll_peers();
for event in &events {
let Event::ResourceRequested { from, hash } = event else { continue };
match self.gdd.read_resource(hash).await {
Ok(bytes) => {
if let Err(error) = self.gdd.send_resource(*from, *hash, bytes.as_slice().to_vec()) {
log::warn!("Failed to send resource {hash}: {error}");
}
}
Err(error) => log::warn!("Peer asked for resource {hash} which is not in this document: {error}"),
}
}
events
}

pub fn document(&self) -> &GddV1 {
&self.gdd
}

pub fn document_mut(&mut self) -> &mut GddV1 {
&mut self.gdd
}

/// Async because proto-node declarations are resources.
pub async fn network(&self) -> Result<NodeNetwork, LiveError> {
let declarations = self.gdd.declarations(&self.gdd).await;
let (network, _metadata) = self.gdd.registry().to_runtime_with_metadata(&declarations)?;
Ok(network)
}

pub fn resource_proxy(&self) -> ResourceProxy<GddV1Layout> {
self.gdd.resource_proxy()
}

pub fn leave(&mut self) {
self.gdd.leave();
}
}
6 changes: 5 additions & 1 deletion node-graph/graphene-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,20 @@ document-container = { workspace = true, features = ["zip", "xz"] }
migration-formats = { workspace = true }
migration-compiled = { workspace = true }
migration-nodes = { workspace = true }
document-graph-storage = { workspace = true }
document-live = { workspace = true }

# Workspace dependencies
log = { workspace = true }
fern = { workspace = true }
chrono = { workspace = true }
wgpu = { workspace = true }
tokio = { workspace = true }
tokio = { workspace = true, features = ["io-util", "time"] }
winit = { workspace = true }
clap = { workspace = true, features = ["cargo", "derive"] }
image = { workspace = true }
serde_json = { workspace = true }
getrandom = "0.3"
wgpu-executor = { workspace = true, optional = true }

[package.metadata.cargo-shear]
Expand Down
12 changes: 11 additions & 1 deletion node-graph/graphene-cli/src/export.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::TokioSpawner;
use graph_craft::document::value::{DAffine2, DVec2, RenderOutputType, TaggedValue, UVec2};
use graph_craft::graphene_compiler::Executor;
use graphene_std::application_io::{ExportFormat, RenderConfig, TimingInformation};
use graphene_std::application_io::{ExportFormat, RenderConfig, Texture, TimingInformation};
use graphene_std::core_types::ops::ConvertAsync;
use graphene_std::core_types::transform::Footprint;
use graphene_std::raster_types::{CPU, GPU, Raster};
Expand Down Expand Up @@ -206,6 +206,16 @@ pub(crate) async fn render_frame(
}
}

pub async fn render_texture(executor: &DynamicExecutor, render_config: RenderConfig, runtime: &AsyncRuntime, spawner: &TokioSpawner) -> Result<Texture, Box<dyn Error>> {
match execute_until_settled(executor, runtime, spawner, render_config).await? {
TaggedValue::RenderOutput(output) => match output.data {
RenderOutputType::Texture(texture) => Ok(texture),
other => Err(format!("Expected a GPU render output, got: {other:?}").into()),
},
other => Err(format!("Expected RenderOutput, got: {other:?}").into()),
}
}

/// Writes a rendered frame to a file of the export's type.
fn write_frame(path: &Path, settings: ExportSettings, frame: RenderedFrame) -> Result<(), Box<dyn Error>> {
let contents = match frame {
Expand Down
43 changes: 29 additions & 14 deletions node-graph/graphene-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ pub mod export;
use document_container::AnyContainer;
use document_container::backends::memory::MemoryBackend;
use document_format::{GddV1, GddV1Layout};
use graph_craft::application_io::resource::LoadResource;
use graph_craft::application_io::{PlatformApplicationIo, PlatformEditorApi};
use graph_craft::document::{NetworkLocation, NodeId, NodeNetwork};
use graph_craft::graphene_compiler::Compiler;
use graph_craft::proto::ProtoNetwork;
use graph_craft::util::load_network;
use graphene_std::application_io::ApplicationIo;
use graphene_std::runtime::{AsyncRuntime, Spawn, Task};
use graphene_std::runtime::{AsyncRuntime, RuntimeHandle, Spawn, Task};
use interpreted_executor::dynamic_executor::DynamicExecutor;
use interpreted_executor::util::wrap_network_in_scope;
use std::collections::HashMap;
Expand Down Expand Up @@ -95,11 +96,12 @@ pub struct CompiledDocument {
spawner: Arc<TokioSpawner>,
}

pub async fn compile_document(document: OpenedDocument) -> Result<CompiledDocument, Box<dyn Error>> {
/// A GPU context and the editor API, resolving resources through `resource_proxy` if given.
pub async fn editor_api(resource_proxy: Option<Box<dyn LoadResource>>) -> (Arc<PlatformApplicationIo>, Arc<PlatformEditorApi>) {
log::info!("Creating GPU context");
let mut application_io = PlatformApplicationIo::new().await;
if let Some(gdd) = &document.gdd {
application_io.inject_resource_proxy(Box::new(gdd.resource_proxy()));
if let Some(resource_proxy) = resource_proxy {
application_io.inject_resource_proxy(resource_proxy);
}
let application_io = Arc::new(application_io);
if let Some(executor) = application_io.gpu_executor() {
Expand All @@ -110,14 +112,15 @@ pub async fn compile_document(document: OpenedDocument) -> Result<CompiledDocume
let editor_api = Arc::new(PlatformEditorApi {
application_io: Some(application_io.clone()),
});
let spawner = Arc::new(TokioSpawner::default());
let runtime = AsyncRuntime::new(spawner.clone());
(application_io, editor_api)
}

let mut network = wrap_network_in_scope(document.network, editor_api, runtime.handle());
/// Compile `network`. Only a `.gdd` has a resource store.
pub fn compile_graph(network: NodeNetwork, editor_api: Arc<PlatformEditorApi>, gdd: Option<&GddV1>, runtime: RuntimeHandle) -> Result<ProtoNetwork, Box<dyn Error>> {
let mut network = wrap_network_in_scope(network, editor_api, runtime);

// A `.gdd` resolves resource hashes from its registry, while a legacy `.graphite` has no resource store
let preprocessor = preprocessor::Preprocessor::new();
let preprocessed = match &document.gdd {
let preprocessed = match gdd {
Some(gdd) => preprocessor.preprocess(&mut network, &|resource_id| gdd.registry().resources.get(&resource_id).and_then(|resource| resource.hash)),
None => preprocessor.preprocess(&mut network, &|_| None),
};
Expand All @@ -127,9 +130,24 @@ pub async fn compile_document(document: OpenedDocument) -> Result<CompiledDocume
for warning in &compiled_network.warnings {
eprintln!("{warning:?}");
}
Ok(compiled_network.network)
}

pub fn create_executor(proto_network: ProtoNetwork) -> Result<DynamicExecutor, Box<dyn Error>> {
DynamicExecutor::new(proto_network).map_err(|errors| {
let messages: Vec<String> = errors.iter().map(|error| format!("{error:?}")).collect();
messages.join("\n").into()
})
}

pub async fn compile_document(document: OpenedDocument) -> Result<CompiledDocument, Box<dyn Error>> {
let (application_io, editor_api) = editor_api(document.gdd.as_ref().map(|gdd| Box::new(gdd.resource_proxy()) as Box<dyn LoadResource>)).await;
let spawner = Arc::new(TokioSpawner::default());
let runtime = AsyncRuntime::new(spawner.clone());
let proto_network = compile_graph(document.network, editor_api, document.gdd.as_ref(), runtime.handle())?;

Ok(CompiledDocument {
proto_network: compiled_network.network,
proto_network,
application_io,
runtime,
spawner,
Expand All @@ -138,10 +156,7 @@ pub async fn compile_document(document: OpenedDocument) -> Result<CompiledDocume

impl CompiledDocument {
pub fn into_renderer(self) -> Result<Renderer, Box<dyn Error>> {
let executor = DynamicExecutor::new(self.proto_network).map_err(|errors| {
let messages: Vec<String> = errors.iter().map(|error| format!("{error:?}")).collect();
messages.join("\n")
})?;
let executor = create_executor(self.proto_network)?;
self.runtime.update_sources(executor.sources());

let device = self.application_io.gpu_executor().ok_or("No GPU executor is available")?.context().device.clone();
Expand Down
Loading
Loading