Render artwork at correct resolution when using vello on wasm (#3416)

* Work on fixing rendering for wasm+vello

* Render vello canvas in wasm at the correct resolution

* Cleanup unused surface rendering code

* Remove vector to raster conversion

* Remove desktop changes

* Revert window.rs changes

* Don't round logical coordinates

* Fix desktop compilation + don't round logical coordinates for svg rendering

* Further cleanup

* Compute logical size from acutal physical sizes
This commit is contained in:
Dennis Kobert
2025-11-24 15:23:27 +01:00
committed by GitHub
parent 6e66c79392
commit a932eaedcf
13 changed files with 197 additions and 216 deletions

View File

@@ -6,8 +6,6 @@ use graph_craft::proto::{NodeConstructor, TypeErasedBox};
use graphene_std::Artboard;
use graphene_std::Context;
use graphene_std::Graphic;
#[cfg(feature = "gpu")]
use graphene_std::any::DowncastBothNode;
use graphene_std::any::DynAnyNode;
use graphene_std::application_io::{ImageTexture, SurfaceFrame};
use graphene_std::brush::brush_cache::BrushCache;
@@ -234,28 +232,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => path_bool_nodes::BooleanOperation]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::text::TextAlign]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => RenderIntermediate]),
// =======================
// CREATE GPU SURFACE NODE
// =======================
#[cfg(feature = "gpu")]
(
ProtoNodeIdentifier::new(stringify!(wgpu_executor::CreateGpuSurfaceNode<_>)),
|args| {
Box::pin(async move {
let editor_api: DowncastBothNode<Context, &WasmEditorApi> = DowncastBothNode::new(args[0].clone());
let node = <wgpu_executor::CreateGpuSurfaceNode<_>>::new(editor_api);
let any: DynAnyNode<Context, _, _> = DynAnyNode::new(node);
Box::new(any) as TypeErasedBox
})
},
{
let node = <wgpu_executor::CreateGpuSurfaceNode<_>>::new(graphene_std::any::PanicNode::<Context, dyn_any::DynFuture<'static, &WasmEditorApi>>::new());
let params = vec![fn_type_fut!(Context, &WasmEditorApi)];
let mut node_io = <wgpu_executor::CreateGpuSurfaceNode<_> as NodeIO<'_, Context>>::to_async_node_io(&node, params);
node_io.call_argument = concrete!(<Context as StaticType>::Static);
node_io
},
),
];
// =============
// CONVERT NODES

View File

@@ -26,21 +26,10 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc<WasmEdito
// .document_node;
let render_node = DocumentNode {
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(2), 0)],
inputs: vec![NodeInput::node(NodeId(0), 0)],
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(4), 0)],
exports: vec![NodeInput::node(NodeId(2), 0)],
nodes: [
DocumentNode {
inputs: vec![NodeInput::scope("editor-api")],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("wgpu_executor::CreateGpuSurfaceNode")),
skip_deduplication: true,
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(0), 0)],
implementation: DocumentNodeImplementation::ProtoNode(graphene_core::memo::memo::IDENTIFIER),
..Default::default()
},
DocumentNode {
call_argument: concrete!(Context),
inputs: vec![NodeInput::import(core_types::Type::Fn(Box::new(concrete!(Context)), Box::new(generic!(T))), 0)],
@@ -54,7 +43,7 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc<WasmEdito
// Keep this in sync with the protonode in valid_input_types
DocumentNode {
call_argument: concrete!(Context),
inputs: vec![NodeInput::scope("editor-api"), NodeInput::node(NodeId(2), 0), NodeInput::node(NodeId(1), 0)],
inputs: vec![NodeInput::scope("editor-api"), NodeInput::node(NodeId(0), 0)],
implementation: DocumentNodeImplementation::ProtoNode(graphene_std::render_node::render::IDENTIFIER),
context_features: graphene_std::ContextDependencies {
extract: ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS,
@@ -64,7 +53,7 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc<WasmEdito
},
DocumentNode {
call_argument: concrete!(graphene_std::application_io::RenderConfig),
inputs: vec![NodeInput::node(NodeId(3), 0)],
inputs: vec![NodeInput::node(NodeId(1), 0)],
implementation: DocumentNodeImplementation::ProtoNode(graphene_std::render_node::create_context::IDENTIFIER),
context_features: graphene_std::ContextDependencies {
extract: ContextFeatures::empty(),

View File

@@ -1,6 +1,6 @@
use core_types::transform::Footprint;
use dyn_any::{DynAny, StaticType, StaticTypeSized};
use glam::{DAffine2, UVec2};
use glam::{DAffine2, DVec2, UVec2};
use std::fmt::Debug;
use std::future::Future;
use std::hash::{Hash, Hasher};
@@ -23,7 +23,8 @@ impl std::fmt::Display for SurfaceId {
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct SurfaceFrame {
pub surface_id: SurfaceId,
pub resolution: UVec2,
/// Logical resolution in CSS pixels (used for foreignObject dimensions)
pub resolution: DVec2,
pub transform: DAffine2,
}
@@ -101,10 +102,11 @@ impl Size for ImageTexture {
impl<S: Size> From<SurfaceHandleFrame<S>> for SurfaceFrame {
fn from(x: SurfaceHandleFrame<S>) -> Self {
let size = x.surface_handle.surface.size();
Self {
surface_id: x.surface_handle.window_id,
transform: x.transform,
resolution: x.surface_handle.surface.size(),
resolution: size.into(),
}
}
}

View File

@@ -5,7 +5,7 @@ use core_types::{Color, Context, Ctx, ExtractFootprint, OwnedContextImpl, WasmNo
use graph_craft::document::value::RenderOutput;
pub use graph_craft::document::value::RenderOutputType;
pub use graph_craft::wasm_application_io::*;
use graphene_application_io::{ApplicationIo, ExportFormat, ImageTexture, RenderConfig, SurfaceFrame};
use graphene_application_io::{ApplicationIo, ExportFormat, ImageTexture, RenderConfig};
use graphic_types::Artboard;
use graphic_types::Graphic;
use graphic_types::Vector;
@@ -120,12 +120,7 @@ async fn create_context<'a: 'n>(
}
#[node_macro::node(category(""))]
async fn render<'a: 'n>(
ctx: impl Ctx + ExtractFootprint + ExtractVarArgs,
editor_api: &'a WasmEditorApi,
data: RenderIntermediate,
_surface_handle: impl Node<Context<'static>, Output = Option<wgpu_executor::WgpuSurface>>,
) -> RenderOutput {
async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, editor_api: &'a WasmEditorApi, data: RenderIntermediate) -> RenderOutput {
let footprint = ctx.footprint();
let render_params = ctx
.vararg(0)
@@ -136,6 +131,10 @@ async fn render<'a: 'n>(
render_params.footprint = *footprint;
let render_params = &render_params;
let scale = render_params.scale;
let physical_resolution = render_params.footprint.resolution;
let logical_resolution = render_params.footprint.resolution.as_dvec2() / scale;
let RenderIntermediate { ty, mut metadata, contains_artboard } = data;
metadata.apply_transform(footprint.transform);
@@ -146,8 +145,8 @@ async fn render<'a: 'n>(
rendering.leaf_tag("rect", |attributes| {
attributes.push("x", "0");
attributes.push("y", "0");
attributes.push("width", footprint.resolution.x.to_string());
attributes.push("height", footprint.resolution.y.to_string());
attributes.push("width", logical_resolution.x.to_string());
attributes.push("height", logical_resolution.y.to_string());
let matrix = format_transform_matrix(footprint.transform.inverse());
if !matrix.is_empty() {
attributes.push("transform", matrix);
@@ -159,7 +158,7 @@ async fn render<'a: 'n>(
rendering.image_data = svg_data.1.clone();
rendering.svg_defs = svg_data.2.clone();
rendering.wrap_with_transform(footprint.transform, Some(footprint.resolution.as_dvec2()));
rendering.wrap_with_transform(footprint.transform, Some(logical_resolution));
RenderOutputType::Svg {
svg: rendering.svg.to_svg_string(),
image_data: rendering.image_data,
@@ -171,15 +170,6 @@ async fn render<'a: 'n>(
};
let (child, context) = Arc::as_ref(vello_data);
let surface_handle = if cfg!(all(feature = "vello", target_family = "wasm")) {
_surface_handle.eval(None).await
} else {
None
};
// When rendering to a surface, we do not want to apply the scale
let scale = if surface_handle.is_none() { render_params.scale } else { 1. };
let scale_transform = glam::DAffine2::from_scale(glam::DVec2::splat(scale));
let footprint_transform = scale_transform * footprint.transform;
let footprint_transform_vello = vello::kurbo::Affine::new(footprint_transform.to_cols_array());
@@ -187,11 +177,9 @@ async fn render<'a: 'n>(
let mut scene = vello::Scene::new();
scene.append(child, Some(footprint_transform_vello));
let resolution = (footprint.resolution.as_dvec2() * scale).as_uvec2();
// We now replace all transforms which are supposed to be infinite with a transform which covers the entire viewport
// See <https://xi.zulipchat.com/#narrow/channel/197075-vello/topic/Full.20screen.20color.2Fgradients/near/538435044> for more detail
let scaled_infinite_transform = vello::kurbo::Affine::scale_non_uniform(resolution.x as f64, resolution.y as f64);
let scaled_infinite_transform = vello::kurbo::Affine::scale_non_uniform(physical_resolution.x as f64, physical_resolution.y as f64);
let encoding = scene.encoding_mut();
for transform in encoding.transforms.iter_mut() {
if transform.matrix[0] == f32::INFINITY {
@@ -204,25 +192,12 @@ async fn render<'a: 'n>(
background = Color::WHITE;
}
if let Some(surface_handle) = surface_handle {
exec.render_vello_scene(&scene, &surface_handle, resolution, context, background)
.await
.expect("Failed to render Vello scene");
let texture = exec
.render_vello_scene_to_texture(&scene, physical_resolution, context, background)
.await
.expect("Failed to render Vello scene");
let frame = SurfaceFrame {
surface_id: surface_handle.window_id,
// TODO: Find a cleaner way to get the unscaled resolution here.
// This is done because the surface frame (canvas) is in logical pixels, not physical pixels.
resolution,
transform: glam::DAffine2::IDENTITY,
};
RenderOutputType::CanvasFrame(frame)
} else {
let texture = exec.render_vello_scene_to_texture(&scene, resolution, context, background).await.expect("Failed to render Vello scene");
RenderOutputType::Texture(ImageTexture { texture })
}
RenderOutputType::Texture(ImageTexture { texture })
}
_ => unreachable!("Render node did not receive its requested data type"),
};

View File

@@ -4,7 +4,7 @@ pub mod texture_conversion;
use crate::shader_runtime::ShaderRuntime;
use anyhow::Result;
use core_types::{Color, Ctx};
use core_types::Color;
use dyn_any::StaticType;
use futures::lock::Mutex;
use glam::UVec2;
@@ -13,7 +13,7 @@ pub use rendering::RenderContext;
use std::sync::Arc;
use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
use wgpu::util::TextureBlitter;
use wgpu::{Origin3d, SurfaceConfiguration, TextureAspect};
use wgpu::{Origin3d, TextureAspect};
pub use context::Context as WgpuContext;
pub use context::ContextBuilder as WgpuContextBuilder;
@@ -66,41 +66,6 @@ unsafe impl StaticType for Surface {
const VELLO_SURFACE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
impl WgpuExecutor {
pub async fn render_vello_scene(&self, scene: &Scene, surface: &WgpuSurface, size: UVec2, context: &RenderContext, background: Color) -> Result<()> {
let mut guard = surface.surface.target_texture.lock().await;
let surface_inner = &surface.surface.inner;
let surface_caps = surface_inner.get_capabilities(&self.context.adapter);
surface_inner.configure(
&self.context.device,
&SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::STORAGE_BINDING,
format: VELLO_SURFACE_FORMAT,
width: size.x,
height: size.y,
present_mode: surface_caps.present_modes[0],
alpha_mode: wgpu::CompositeAlphaMode::Opaque,
view_formats: vec![],
desired_maximum_frame_latency: 2,
},
);
self.render_vello_scene_to_target_texture(scene, size, context, background, &mut guard).await?;
let surface_texture = surface_inner.get_current_texture()?;
let mut encoder = self.context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Surface Blit") });
surface.surface.blitter.copy(
&self.context.device,
&mut encoder,
&guard.as_ref().unwrap().view,
&surface_texture.texture.create_view(&wgpu::TextureViewDescriptor::default()),
);
self.context.queue.submit([encoder.finish()]);
surface_texture.present();
Ok(())
}
pub async fn render_vello_scene_to_texture(&self, scene: &Scene, size: UVec2, context: &RenderContext, background: Color) -> Result<wgpu::Texture> {
let mut output = None;
self.render_vello_scene_to_target_texture(scene, size, context, background, &mut output).await?;
@@ -211,10 +176,3 @@ impl WgpuExecutor {
}
pub type WindowHandle = Arc<SurfaceHandle<Window>>;
#[node_macro::node(skip_impl)]
fn create_gpu_surface<'a: 'n, Io: ApplicationIo<Executor = WgpuExecutor, Surface = Window> + 'a + Send + Sync>(_: impl Ctx + 'a, editor_api: &'a EditorApi<Io>) -> Option<WgpuSurface> {
let canvas = editor_api.application_io.as_ref()?.window()?;
let executor = editor_api.application_io.as_ref()?.gpu_executor()?;
Some(Arc::new(executor.create_surface(canvas).ok()?))
}