Skip to content

Commit ad5d8fc

Browse files
committed
Optimize rendering by caching intermediate render results when panning/zooming (#3147)
* Add svg render node WIP Fix error in context nullificaton node insertion Correctly translate metadata Fix warning * Cleanup * Remove color / gradient check and fix svg defs * Implement viewport filling transform modification for vello * Cleanup and comments * Code review
1 parent 09ece94 commit ad5d8fc

12 files changed

Lines changed: 335 additions & 259 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ winit = { version = "0.30", features = ["wayland", "rwh_06"] }
136136
url = "2.5"
137137
tokio = { version = "1.29", features = ["fs", "macros", "io-std", "rt"] }
138138
vello = { git = "https://github.com/linebender/vello.git", rev = "87cc5bee6d3a34d15017dbbb58634ddc7f33ff9b" } # TODO switch back to stable when a release is made
139+
vello_encoding = { git = "https://github.com/linebender/vello.git", rev = "87cc5bee6d3a34d15017dbbb58634ddc7f33ff9b" } # TODO switch back to stable when a release is made
139140
resvg = "0.45"
140141
usvg = "0.45"
141142
rand = { version = "0.9", default-features = false, features = ["std_rng"] }

editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -481,71 +481,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
481481
description: Cow::Borrowed("Loads an image from a given URL"),
482482
properties: None,
483483
},
484-
#[cfg(feature = "gpu")]
485-
DocumentNodeDefinition {
486-
identifier: "Create Canvas",
487-
category: "Debug: GPU",
488-
node_template: NodeTemplate {
489-
document_node: DocumentNode {
490-
implementation: DocumentNodeImplementation::Network(NodeNetwork {
491-
exports: vec![NodeInput::node(NodeId(1), 0)],
492-
nodes: [
493-
DocumentNode {
494-
inputs: vec![NodeInput::scope("editor-api")],
495-
implementation: DocumentNodeImplementation::ProtoNode(wasm_application_io::create_surface::IDENTIFIER),
496-
skip_deduplication: true,
497-
..Default::default()
498-
},
499-
DocumentNode {
500-
inputs: vec![NodeInput::node(NodeId(0), 0)],
501-
implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER),
502-
..Default::default()
503-
},
504-
]
505-
.into_iter()
506-
.enumerate()
507-
.map(|(id, node)| (NodeId(id as u64), node))
508-
.collect(),
509-
..Default::default()
510-
}),
511-
..Default::default()
512-
},
513-
persistent_node_metadata: DocumentNodePersistentMetadata {
514-
output_names: vec!["Image".to_string()],
515-
network_metadata: Some(NodeNetworkMetadata {
516-
persistent_metadata: NodeNetworkPersistentMetadata {
517-
node_metadata: [
518-
DocumentNodeMetadata {
519-
persistent_metadata: DocumentNodePersistentMetadata {
520-
display_name: "Create Canvas".to_string(),
521-
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
522-
..Default::default()
523-
},
524-
..Default::default()
525-
},
526-
DocumentNodeMetadata {
527-
persistent_metadata: DocumentNodePersistentMetadata {
528-
display_name: "Cache".to_string(),
529-
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 0)),
530-
..Default::default()
531-
},
532-
..Default::default()
533-
},
534-
]
535-
.into_iter()
536-
.enumerate()
537-
.map(|(id, node)| (NodeId(id as u64), node))
538-
.collect(),
539-
..Default::default()
540-
},
541-
..Default::default()
542-
}),
543-
..Default::default()
544-
},
545-
},
546-
description: Cow::Borrowed("Creates a new canvas object."),
547-
properties: None,
548-
},
549484
#[cfg(all(feature = "gpu", target_family = "wasm"))]
550485
DocumentNodeDefinition {
551486
identifier: "Rasterize",

node-graph/gapplication-io/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ pub enum ExportFormat {
227227
},
228228
Jpeg,
229229
Canvas,
230+
Texture,
230231
}
231232

232233
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]

node-graph/gstd/Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ authors = ["Graphite Authors <contact@graphite.rs>"]
77
license = "MIT OR Apache-2.0"
88

99
[features]
10-
default = ["wasm"]
10+
default = ["wasm", "wgpu"]
1111
gpu = []
1212
wgpu = ["gpu", "graph-craft/wgpu", "graphene-application-io/wgpu"]
1313
wasm = [
@@ -18,7 +18,7 @@ wasm = [
1818
"image/png",
1919
]
2020
image-compare = []
21-
vello = ["dep:vello", "gpu"]
21+
vello = ["gpu"]
2222
resvg = []
2323
wayland = ["graph-craft/wayland"]
2424
shader-nodes = ["graphene-raster-nodes/shader-nodes"]
@@ -48,7 +48,8 @@ base64 = { workspace = true }
4848
wasm-bindgen = { workspace = true, optional = true }
4949
wasm-bindgen-futures = { workspace = true, optional = true }
5050
tokio = { workspace = true, optional = true }
51-
vello = { workspace = true, optional = true }
51+
vello = { workspace = true }
52+
vello_encoding = { workspace = true }
5253
web-sys = { workspace = true, optional = true, features = [
5354
"Window",
5455
"CanvasRenderingContext2d",

node-graph/gstd/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod any;
2+
pub mod render_node;
23
pub mod text;
34
#[cfg(feature = "wasm")]
45
pub mod wasm_application_io;

node-graph/gstd/src/render_node.rs

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
use graph_craft::document::value::RenderOutput;
2+
pub use graph_craft::document::value::RenderOutputType;
3+
pub use graph_craft::wasm_application_io::*;
4+
use graphene_application_io::{ApplicationIo, ExportFormat, ImageTexture, RenderConfig, SurfaceFrame};
5+
use graphene_core::gradient::GradientStops;
6+
use graphene_core::raster::image::Image;
7+
use graphene_core::raster_types::{CPU, Raster};
8+
use graphene_core::table::Table;
9+
use graphene_core::transform::Footprint;
10+
use graphene_core::vector::Vector;
11+
use graphene_core::{Artboard, CloneVarArgs, ExtractAll, ExtractVarArgs};
12+
use graphene_core::{Color, Context, Ctx, ExtractFootprint, Graphic, OwnedContextImpl, WasmNotSend};
13+
use graphene_svg_renderer::{Render, RenderOutputType as RenderOutputTypeRequest, RenderParams, RenderSvgSegmentList, SvgRender, format_transform_matrix};
14+
use graphene_svg_renderer::{RenderMetadata, SvgSegment};
15+
use std::sync::Arc;
16+
use wgpu_executor::RenderContext;
17+
18+
/// List of (canvas id, image data) pairs for embedding images as canvases in the final SVG string.
19+
type ImageData = Vec<(u64, Image<Color>)>;
20+
21+
#[derive(Clone, dyn_any::DynAny)]
22+
pub enum RenderIntermediateType {
23+
Vello(Arc<(vello::Scene, RenderContext)>),
24+
Svg(Arc<(String, ImageData, String)>),
25+
}
26+
#[derive(Clone, dyn_any::DynAny)]
27+
pub struct RenderIntermediate {
28+
ty: RenderIntermediateType,
29+
metadata: RenderMetadata,
30+
contains_artboard: bool,
31+
}
32+
33+
#[node_macro::node(category(""))]
34+
async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + Sync>(
35+
ctx: impl Ctx + ExtractVarArgs + ExtractAll + CloneVarArgs,
36+
#[implementations(
37+
Context -> Table<Artboard>,
38+
Context -> Table<Graphic>,
39+
Context -> Table<Vector>,
40+
Context -> Table<Raster<CPU>>,
41+
Context -> Table<Color>,
42+
Context -> Table<GradientStops>,
43+
)]
44+
data: impl Node<Context<'static>, Output = T>,
45+
editor_api: impl Node<Context<'static>, Output = &'a WasmEditorApi>,
46+
) -> RenderIntermediate {
47+
let mut render = SvgRender::new();
48+
let render_params = ctx
49+
.vararg(0)
50+
.expect("Did not find var args")
51+
.downcast_ref::<RenderParams>()
52+
.expect("Downcasting render params yielded invalid type");
53+
54+
let ctx = OwnedContextImpl::from(ctx.clone()).into_context();
55+
let data = data.eval(ctx).await;
56+
57+
let footprint = Footprint::default();
58+
let mut metadata = RenderMetadata::default();
59+
data.collect_metadata(&mut metadata, footprint, None);
60+
let contains_artboard = data.contains_artboard();
61+
62+
let editor_api = editor_api.eval(None).await;
63+
64+
if !render_params.for_export && editor_api.editor_preferences.use_vello() && matches!(render_params.render_output_type, graphene_svg_renderer::RenderOutputType::Vello) {
65+
let mut scene = vello::Scene::new();
66+
67+
let mut context = wgpu_executor::RenderContext::default();
68+
data.render_to_vello(&mut scene, Default::default(), &mut context, render_params);
69+
70+
RenderIntermediate {
71+
ty: RenderIntermediateType::Vello(Arc::new((scene, context))),
72+
metadata,
73+
contains_artboard,
74+
}
75+
} else {
76+
data.render_svg(&mut render, render_params);
77+
78+
RenderIntermediate {
79+
ty: RenderIntermediateType::Svg(Arc::new((render.svg.to_svg_string(), render.image_data, render.svg_defs.clone()))),
80+
metadata,
81+
contains_artboard,
82+
}
83+
}
84+
}
85+
86+
#[node_macro::node(category(""))]
87+
async fn create_context<'a: 'n>(
88+
// Context injections are defined in the wrap_network_in_scope function
89+
render_config: RenderConfig,
90+
data: impl Node<Context<'static>, Output = RenderOutput>,
91+
) -> RenderOutput {
92+
let footprint = render_config.viewport;
93+
94+
let render_output_type = match render_config.export_format {
95+
ExportFormat::Svg => RenderOutputTypeRequest::Svg,
96+
ExportFormat::Png { .. } => todo!(),
97+
ExportFormat::Jpeg => todo!(),
98+
ExportFormat::Canvas => RenderOutputTypeRequest::Vello,
99+
ExportFormat::Texture => RenderOutputTypeRequest::Vello,
100+
};
101+
let render_params = RenderParams {
102+
render_mode: render_config.render_mode,
103+
hide_artboards: render_config.hide_artboards,
104+
for_export: render_config.for_export,
105+
render_output_type,
106+
footprint: Footprint::default(),
107+
..Default::default()
108+
};
109+
let ctx = OwnedContextImpl::default()
110+
.with_footprint(footprint)
111+
.with_real_time(render_config.time.time)
112+
.with_animation_time(render_config.time.animation_time.as_secs_f64())
113+
.with_vararg(Box::new(render_params))
114+
.into_context();
115+
116+
data.eval(ctx).await
117+
}
118+
119+
#[node_macro::node(category(""))]
120+
async fn render<'a: 'n>(
121+
ctx: impl Ctx + ExtractFootprint + ExtractVarArgs,
122+
editor_api: &'a WasmEditorApi,
123+
data: RenderIntermediate,
124+
_surface_handle: impl Node<Context<'static>, Output = Option<wgpu_executor::WgpuSurface>>,
125+
) -> RenderOutput {
126+
let footprint = ctx.footprint();
127+
let render_params = ctx
128+
.vararg(0)
129+
.expect("Did not find var args")
130+
.downcast_ref::<RenderParams>()
131+
.expect("Downcasting render params yielded invalid type");
132+
let mut render_params = render_params.clone();
133+
render_params.footprint = *footprint;
134+
let render_params = &render_params;
135+
136+
let RenderIntermediate { ty, mut metadata, contains_artboard } = data;
137+
metadata.apply_transform(footprint.transform);
138+
139+
let surface_handle = if cfg!(all(feature = "vello", target_family = "wasm")) {
140+
_surface_handle.eval(None).await
141+
} else {
142+
None
143+
};
144+
145+
let mut output_format = render_params.render_output_type;
146+
// TODO: Actually request the right thing upfront
147+
if let RenderIntermediateType::Svg(_) = ty {
148+
output_format = RenderOutputTypeRequest::Svg;
149+
}
150+
let data = match (output_format, &ty) {
151+
(RenderOutputTypeRequest::Svg, RenderIntermediateType::Svg(svg_data)) => {
152+
let mut svg_renderer = SvgRender::new();
153+
if !contains_artboard && !render_params.hide_artboards {
154+
svg_renderer.leaf_tag("rect", |attributes| {
155+
attributes.push("x", "0");
156+
attributes.push("y", "0");
157+
attributes.push("width", footprint.resolution.x.to_string());
158+
attributes.push("height", footprint.resolution.y.to_string());
159+
let matrix = format_transform_matrix(footprint.transform.inverse());
160+
if !matrix.is_empty() {
161+
attributes.push("transform", matrix);
162+
}
163+
attributes.push("fill", "white");
164+
});
165+
}
166+
svg_renderer.svg.push(SvgSegment::from(svg_data.0.clone()));
167+
svg_renderer.image_data = svg_data.1.clone();
168+
svg_renderer.svg_defs = svg_data.2.clone();
169+
170+
svg_renderer.wrap_with_transform(footprint.transform, Some(footprint.resolution.as_dvec2()));
171+
RenderOutputType::Svg {
172+
svg: svg_renderer.svg.to_svg_string(),
173+
image_data: svg_renderer.image_data,
174+
}
175+
}
176+
(RenderOutputTypeRequest::Vello, RenderIntermediateType::Vello(vello_data)) => {
177+
let Some(exec) = editor_api.application_io.as_ref().unwrap().gpu_executor() else {
178+
unreachable!("Attempted to render with Vello when no GPU executor is available");
179+
};
180+
let (child, context) = Arc::as_ref(vello_data);
181+
let footprint_transform = vello::kurbo::Affine::new(footprint.transform.to_cols_array());
182+
183+
let mut scene = vello::Scene::new();
184+
scene.append(child, Some(footprint_transform));
185+
186+
let encoding = scene.encoding_mut();
187+
188+
// We now replace all transforms which are supposed to be infinite with a transform which covers the entire viewport
189+
// See <https://xi.zulipchat.com/#narrow/channel/197075-vello/topic/Full.20screen.20color.2Fgradients/near/538435044> for more detail
190+
191+
for transform in encoding.transforms.iter_mut() {
192+
if transform.matrix[0] == f32::INFINITY {
193+
*transform = vello_encoding::Transform::from_kurbo(&(vello::kurbo::Affine::scale_non_uniform(footprint.resolution.x as f64, footprint.resolution.y as f64)))
194+
}
195+
}
196+
197+
let mut background = Color::from_rgb8_srgb(0x22, 0x22, 0x22);
198+
if !contains_artboard && !render_params.hide_artboards {
199+
background = Color::WHITE;
200+
}
201+
if let Some(surface_handle) = surface_handle {
202+
exec.render_vello_scene(&scene, &surface_handle, footprint.resolution, context, background)
203+
.await
204+
.expect("Failed to render Vello scene");
205+
206+
let frame = SurfaceFrame {
207+
surface_id: surface_handle.window_id,
208+
resolution: footprint.resolution,
209+
transform: glam::DAffine2::IDENTITY,
210+
};
211+
212+
RenderOutputType::CanvasFrame(frame)
213+
} else {
214+
let texture = exec
215+
.render_vello_scene_to_texture(&scene, footprint.resolution, context, background)
216+
.await
217+
.expect("Failed to render Vello scene");
218+
219+
RenderOutputType::Texture(ImageTexture { texture })
220+
}
221+
}
222+
_ => unreachable!("Render node did not receive its requested data type"),
223+
};
224+
RenderOutput { data, metadata }
225+
}

0 commit comments

Comments
 (0)