Skip to content

Commit e29e441

Browse files
committed
Add graphene-cli host, join and watch
1 parent 246461d commit e29e441

12 files changed

Lines changed: 692 additions & 18 deletions

File tree

‎Cargo.lock‎

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ members = [
1818
"document/migrations/snapshot",
1919
"document/migrations/nodes",
2020
"document/peer-transport",
21+
"document/live",
2122
"editor",
2223
"frontend/wrapper",
2324
"libraries/dyn-any",
@@ -115,6 +116,7 @@ migration-runner = { path = "document/migrations/runner" }
115116
migration-formats = { path = "document/migrations/formats" }
116117
migration-nodes = { path = "document/migrations/nodes" }
117118
peer-transport = { path = "document/peer-transport" }
119+
document-live = { path = "document/live" }
118120
raster-nodes = { path = "node-graph/nodes/raster" }
119121
graphene-std = { path = "node-graph/nodes/gstd" }
120122
interpreted-executor = { path = "node-graph/interpreted-executor" }

‎document/format/src/resource.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,12 @@ impl<L: Layout + Send + Sync> LoadResource for Gdd<L> {
137137

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

140+
impl<T: Layout + Clone> Clone for ResourceProxy<T> {
141+
fn clone(&self) -> Self {
142+
Self(self.0.clone(), self.1.clone())
143+
}
144+
}
145+
140146
impl<L: Layout + Send + Sync> LoadResource for ResourceProxy<L> {
141147
fn load(&self, hash: ResourceHash) -> ResourceFuture<'_> {
142148
Box::pin(async move {

‎document/live/Cargo.toml‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "document-live"
3+
description = "A memory-backed document following or hosting a collaborative session, for viewers and embedders"
4+
version.workspace = true
5+
license.workspace = true
6+
authors.workspace = true
7+
edition.workspace = true
8+
rust-version.workspace = true
9+
publish.workspace = true
10+
11+
[dependencies]
12+
document-container = { workspace = true }
13+
document-format = { workspace = true, features = ["network"] }
14+
document-graph-storage = { workspace = true }
15+
peer-transport = { workspace = true }
16+
graph-craft = { workspace = true }
17+
thiserror = { workspace = true }
18+
log = { workspace = true }
19+
20+
[lints]
21+
workspace = true

‎document/live/src/lib.rs‎

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
//! A memory-backed document hosting or following a collaborative session, handing out its runtime `NodeNetwork`
2+
//! on demand. Owns no executor or async runtime, so the caller spawns the transport driver.
3+
4+
use document_container::AnyContainer;
5+
use document_container::backends::memory::MemoryBackend;
6+
use document_format::{GddV1, GddV1Layout, ResourceProxy};
7+
use document_graph_storage::{PeerId, UserId};
8+
use graph_craft::application_io::resource::ResourceHash;
9+
use graph_craft::document::NodeNetwork;
10+
use peer_transport::{Room, SyncTarget};
11+
12+
pub use document_format::Error as FormatError;
13+
pub use document_graph_storage::to_runtime::ConversionError;
14+
pub use peer_transport::{DEFAULT_SIGNALING_SERVER, Event, MessageLoopFuture, Role, SessionToken};
15+
16+
#[derive(Debug, thiserror::Error)]
17+
pub enum LiveError {
18+
#[error(transparent)]
19+
Format(#[from] FormatError),
20+
#[error(transparent)]
21+
Conversion(#[from] ConversionError),
22+
}
23+
24+
pub struct LiveDocument {
25+
gdd: GddV1,
26+
token: SessionToken,
27+
}
28+
29+
impl LiveDocument {
30+
/// Create an empty document and host a session under `token`. `spawn` must drive the returned
31+
/// future for as long as the session lives.
32+
pub async fn host(signaling_server: &str, token: SessionToken, peer: PeerId, user: UserId, spawn: impl FnOnce(MessageLoopFuture)) -> Result<Self, LiveError> {
33+
let gdd = Self::empty_document(peer, user).await?;
34+
Self::host_document(gdd, signaling_server, token, user, spawn)
35+
}
36+
37+
pub fn host_document(mut gdd: GddV1, signaling_server: &str, token: SessionToken, user: UserId, spawn: impl FnOnce(MessageLoopFuture)) -> Result<Self, LiveError> {
38+
gdd.share(Self::connect(signaling_server, token, spawn), user)?;
39+
Ok(Self { gdd, token })
40+
}
41+
42+
/// Join the session under `token`. The document fills in once the host's sync arrives.
43+
pub async fn join(signaling_server: &str, token: SessionToken, peer: PeerId, user: UserId, spawn: impl FnOnce(MessageLoopFuture)) -> Result<Self, LiveError> {
44+
let mut gdd = Self::empty_document(peer, user).await?;
45+
gdd.connect(Self::connect(signaling_server, token, spawn), user)?;
46+
Ok(Self { gdd, token })
47+
}
48+
49+
fn connect(signaling_server: &str, token: SessionToken, spawn: impl FnOnce(MessageLoopFuture)) -> Room {
50+
let (room, driver) = Room::connect(&token.signaling_url(signaling_server));
51+
spawn(driver);
52+
room
53+
}
54+
55+
async fn empty_document(peer: PeerId, user: UserId) -> Result<GddV1, FormatError> {
56+
let version = env!("CARGO_PKG_VERSION").to_string();
57+
GddV1::create_in(AnyContainer::Memory(MemoryBackend::new()), GddV1Layout, peer, user, peer.0, version.clone(), version)
58+
}
59+
60+
pub fn token(&self) -> SessionToken {
61+
self.token
62+
}
63+
64+
pub fn role(&self) -> Option<Role> {
65+
self.gdd.role()
66+
}
67+
68+
pub fn is_synced(&self) -> bool {
69+
self.gdd.is_synced()
70+
}
71+
72+
/// Synced and holding every resource the document references, so [`network`](Self::network) can be built and run.
73+
pub fn is_ready(&self) -> bool {
74+
self.is_synced() && SyncTarget::missing_resources(&self.gdd).is_empty()
75+
}
76+
77+
/// Referenced resources whose bytes no peer has sent yet.
78+
pub fn missing_resources(&self) -> Vec<ResourceHash> {
79+
let mut missing: Vec<ResourceHash> = SyncTarget::missing_resources(&self.gdd).into_iter().collect();
80+
missing.sort_unstable();
81+
missing
82+
}
83+
84+
/// Apply what peers sent since the last call and answer their resource requests. Call it regularly.
85+
pub async fn poll(&mut self) -> Vec<Event> {
86+
let events = self.gdd.poll_peers();
87+
for event in &events {
88+
let Event::ResourceRequested { from, hash } = event else { continue };
89+
match self.gdd.read_resource(hash).await {
90+
Ok(bytes) => {
91+
if let Err(error) = self.gdd.send_resource(*from, *hash, bytes.as_slice().to_vec()) {
92+
log::warn!("Failed to send resource {hash}: {error}");
93+
}
94+
}
95+
Err(error) => log::warn!("Peer asked for resource {hash} which is not in this document: {error}"),
96+
}
97+
}
98+
events
99+
}
100+
101+
pub fn document(&self) -> &GddV1 {
102+
&self.gdd
103+
}
104+
105+
pub fn document_mut(&mut self) -> &mut GddV1 {
106+
&mut self.gdd
107+
}
108+
109+
/// The runtime graph, rebuilt from the registry. Async because proto-node declarations are resources.
110+
pub async fn network(&self) -> Result<NodeNetwork, LiveError> {
111+
let declarations = self.gdd.declarations(&self.gdd).await;
112+
let (network, _metadata) = self.gdd.registry().to_runtime_with_metadata(&declarations)?;
113+
Ok(network)
114+
}
115+
116+
/// Byte store of resources received from peers, for an executor to load from.
117+
pub fn resource_proxy(&self) -> ResourceProxy<GddV1Layout> {
118+
self.gdd.resource_proxy()
119+
}
120+
121+
pub fn leave(&mut self) {
122+
self.gdd.leave();
123+
}
124+
}

‎node-graph/graphene-cli/Cargo.toml‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,20 @@ document-format = { workspace = true, features = ["zip", "xz", "migrations"] }
2929
document-container = { workspace = true, features = ["zip", "xz"] }
3030
migration-formats = { workspace = true }
3131
migration-nodes = { workspace = true }
32+
document-graph-storage = { workspace = true }
33+
document-live = { workspace = true }
3234

3335
# Workspace dependencies
3436
log = { workspace = true }
3537
fern = { workspace = true }
3638
chrono = { workspace = true }
3739
wgpu = { workspace = true }
38-
tokio = { workspace = true }
40+
tokio = { workspace = true, features = ["io-util", "time"] }
41+
winit = { workspace = true }
3942
clap = { workspace = true, features = ["cargo", "derive"] }
4043
image = { workspace = true }
4144
serde_json = { workspace = true }
45+
getrandom = "0.3"
4246
wgpu-executor = { workspace = true, optional = true }
4347

4448
[package.metadata.cargo-shear]

‎node-graph/graphene-cli/src/export.rs‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::TokioSpawner;
22
use graph_craft::document::value::{DAffine2, DVec2, RenderOutputType, TaggedValue, UVec2};
33
use graph_craft::graphene_compiler::Executor;
4-
use graphene_std::application_io::{ExportFormat, RenderConfig, TimingInformation};
4+
use graphene_std::application_io::{ExportFormat, RenderConfig, Texture, TimingInformation};
55
use graphene_std::core_types::ops::ConvertAsync;
66
use graphene_std::core_types::transform::Footprint;
77
use graphene_std::raster_types::{CPU, GPU, Raster};
@@ -206,6 +206,17 @@ pub(crate) async fn render_frame(
206206
}
207207
}
208208

209+
/// Execute the graph for one frame, keeping the result on the GPU.
210+
pub async fn render_texture(executor: &DynamicExecutor, render_config: RenderConfig, runtime: &AsyncRuntime, spawner: &TokioSpawner) -> Result<Texture, Box<dyn Error>> {
211+
match execute_until_settled(executor, runtime, spawner, render_config).await? {
212+
TaggedValue::RenderOutput(output) => match output.data {
213+
RenderOutputType::Texture(texture) => Ok(texture),
214+
other => Err(format!("Expected a GPU render output, got: {other:?}").into()),
215+
},
216+
other => Err(format!("Expected RenderOutput, got: {other:?}").into()),
217+
}
218+
}
219+
209220
/// Writes a rendered frame to a file of the export's type.
210221
fn write_frame(path: &Path, settings: ExportSettings, frame: RenderedFrame) -> Result<(), Box<dyn Error>> {
211222
let contents = match frame {

‎node-graph/graphene-cli/src/lib.rs‎

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ pub mod export;
77
use document_container::AnyContainer;
88
use document_container::backends::memory::MemoryBackend;
99
use document_format::{GddV1, GddV1Layout};
10+
use graph_craft::application_io::resource::LoadResource;
1011
use graph_craft::application_io::{PlatformApplicationIo, PlatformEditorApi};
1112
use graph_craft::document::{NetworkLocation, NodeId, NodeNetwork, OwnedNetworkLocation};
1213
use graph_craft::graphene_compiler::Compiler;
1314
use graph_craft::proto::ProtoNetwork;
1415
use graph_craft::util::load_network;
1516
use graphene_std::application_io::ApplicationIo;
16-
use graphene_std::runtime::{AsyncRuntime, Spawn, Task};
17+
use graphene_std::runtime::{AsyncRuntime, RuntimeHandle, Spawn, Task};
1718
use interpreted_executor::dynamic_executor::DynamicExecutor;
1819
use interpreted_executor::util::wrap_network_in_scope;
1920
use std::collections::HashMap;
@@ -93,12 +94,12 @@ pub struct CompiledDocument {
9394
spawner: Arc<TokioSpawner>,
9495
}
9596

96-
/// Compile `document` against a fresh GPU context, resolving its resources from its `.gdd`.
97-
pub async fn compile_document(document: OpenedDocument) -> Result<CompiledDocument, Box<dyn Error>> {
97+
/// A GPU context and the editor API handed to compiled graphs, resolving resources through `resource_proxy` when given.
98+
pub async fn editor_api(resource_proxy: Option<Box<dyn LoadResource>>) -> (Arc<PlatformApplicationIo>, Arc<PlatformEditorApi>) {
9899
log::info!("Creating GPU context");
99100
let mut application_io = PlatformApplicationIo::new().await;
100-
if let Some(gdd) = &document.gdd {
101-
application_io.inject_resource_proxy(Box::new(gdd.resource_proxy()));
101+
if let Some(resource_proxy) = resource_proxy {
102+
application_io.inject_resource_proxy(resource_proxy);
102103
}
103104
let application_io = Arc::new(application_io);
104105
if let Some(executor) = application_io.gpu_executor() {
@@ -109,14 +110,15 @@ pub async fn compile_document(document: OpenedDocument) -> Result<CompiledDocume
109110
let editor_api = Arc::new(PlatformEditorApi {
110111
application_io: Some(application_io.clone()),
111112
});
112-
let spawner = Arc::new(TokioSpawner::default());
113-
let runtime = AsyncRuntime::new(spawner.clone(), Arc::new(std::time::Instant::now()));
113+
(application_io, editor_api)
114+
}
114115

115-
let mut network = wrap_network_in_scope(document.network, editor_api, runtime.handle());
116+
/// Compile `network`. A `.gdd` resolves resource hashes from its registry, while a legacy `.graphite` has no resource store.
117+
pub fn compile_graph(network: NodeNetwork, editor_api: Arc<PlatformEditorApi>, gdd: Option<&GddV1>, runtime: RuntimeHandle) -> Result<ProtoNetwork, Box<dyn Error>> {
118+
let mut network = wrap_network_in_scope(network, editor_api, runtime);
116119

117-
// A `.gdd` resolves resource hashes from its registry, while a legacy `.graphite` has no resource store
118120
let preprocessor = preprocessor::Preprocessor::new();
119-
let preprocessed = match &document.gdd {
121+
let preprocessed = match gdd {
120122
Some(gdd) => preprocessor.preprocess(&mut network, &|resource_id| gdd.registry().resources.get(&resource_id).and_then(|resource| resource.hash)),
121123
None => preprocessor.preprocess(&mut network, &|_| None),
122124
};
@@ -126,9 +128,26 @@ pub async fn compile_document(document: OpenedDocument) -> Result<CompiledDocume
126128
for warning in &compiled_network.warnings {
127129
eprintln!("{warning:?}");
128130
}
131+
Ok(compiled_network.network)
132+
}
133+
134+
/// An executor for `proto_network`, with its errors joined into one.
135+
pub fn create_executor(proto_network: ProtoNetwork) -> Result<DynamicExecutor, Box<dyn Error>> {
136+
DynamicExecutor::new(proto_network).map_err(|errors| {
137+
let messages: Vec<String> = errors.iter().map(|error| format!("{error:?}")).collect();
138+
messages.join("\n").into()
139+
})
140+
}
141+
142+
/// Compile `document` against a fresh GPU context, resolving its resources from its `.gdd`.
143+
pub async fn compile_document(document: OpenedDocument) -> Result<CompiledDocument, Box<dyn Error>> {
144+
let (application_io, editor_api) = editor_api(document.gdd.as_ref().map(|gdd| Box::new(gdd.resource_proxy()) as Box<dyn LoadResource>)).await;
145+
let spawner = Arc::new(TokioSpawner::default());
146+
let runtime = AsyncRuntime::new(spawner.clone(), Arc::new(std::time::Instant::now()));
147+
let proto_network = compile_graph(document.network, editor_api, document.gdd.as_ref(), runtime.handle())?;
129148

130149
Ok(CompiledDocument {
131-
proto_network: compiled_network.network,
150+
proto_network,
132151
application_io,
133152
runtime,
134153
spawner,
@@ -137,10 +156,7 @@ pub async fn compile_document(document: OpenedDocument) -> Result<CompiledDocume
137156

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

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

0 commit comments

Comments
 (0)