Integrate Vello for vector rendering (#1802)

* Start integrating vello into render pipeline

Cache vello render creation

Implement viewport navigation

Close vello path

Add transform parameter to vello render pass

* Fix render node types

* Fix a bunch of bugs in the path translation

* Avoid panic on empty document

* Fix rendering of holes

* Implement image rendering

* Implement graph recompilation afer editor api change

* Implement preferences toggle for using vello as the renderer

* Make surface creation optional

* Feature gate vello usages

* Implement skeleton for radial gradient

* Rename vello preference

* Fix some gradients

* Only update monitor nodes on graph recompile

* Fix warnings + remove dead code

* Update everything except for thumbnails after a node graph evaluation

* Fix missing click targets for Image frames

* Improve perfamance by removing unecessary widget updates

* Fix node graph paning

* Fix thumbnail loading

* Implement proper hash for vector modification

* Fix test and warnings

* Code review

* Fix dep

* Remove warning

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Dennis Kobert
2024-07-22 10:56:29 +02:00
committed by GitHub
parent 8e774efe9d
commit ab71d26d84
41 changed files with 807 additions and 760 deletions

View File

@@ -290,7 +290,6 @@ async fn create_compute_pass_descriptor<T: Clone + Pixel + StaticTypeSized>(
let canvas = editor_api.application_io.create_surface();
let surface = unsafe { executor.create_surface(canvas) }.unwrap();
// log::debug!("id: {surface:?}");
let surface_id = surface.surface_id;
let texture = executor.create_texture_buffer(image.image.clone(), TextureBufferOptions::Texture).unwrap();

View File

@@ -3,7 +3,8 @@ use core::any::TypeId;
use core::future::Future;
use futures::{future::Either, TryFutureExt};
use glam::{DVec2, U64Vec2};
use graph_craft::imaginate_input::{ImaginateController, ImaginateMaskStartingFill, ImaginatePreferences, ImaginateSamplingMethod, ImaginateServerStatus, ImaginateStatus, ImaginateTerminationHandle};
use graph_craft::imaginate_input::{ImaginateController, ImaginateMaskStartingFill, ImaginateSamplingMethod, ImaginateServerStatus, ImaginateStatus, ImaginateTerminationHandle};
use graph_craft::wasm_application_io::EditorPreferences;
use graphene_core::application_io::NodeGraphUpdateMessage;
use graphene_core::raster::{Color, Image, Luma, Pixel};
use image::{DynamicImage, ImageBuffer, ImageFormat};
@@ -50,17 +51,19 @@ impl core::fmt::Debug for ImaginatePersistentData {
impl Default for ImaginatePersistentData {
fn default() -> Self {
let mut status = ImaginateServerStatus::default();
let server_status = ImaginateServerStatus::default();
#[cfg(not(miri))]
let client = new_client().map_err(|err| status = ImaginateServerStatus::Failed(err.to_string())).ok();
let mut server_status = server_status;
#[cfg(not(miri))]
let client = new_client().map_err(|err| server_status = ImaginateServerStatus::Failed(err.to_string())).ok();
#[cfg(miri)]
let client = None;
let ImaginatePreferences { host_name } = Default::default();
let EditorPreferences { imaginate_hostname: host_name, .. } = Default::default();
Self {
pending_server_check: None,
host_name: parse_url(&host_name).unwrap(),
client,
server_status: status,
server_status,
}
}
}
@@ -285,14 +288,14 @@ pub async fn imaginate<'a, P: Pixel>(
) -> Image<P> {
let WasmEditorApi {
node_graph_message_sender,
imaginate_preferences,
editor_preferences,
..
} = editor_api.await;
let set_progress = |progress: ImaginateStatus| {
controller.set_status(progress);
node_graph_message_sender.send(NodeGraphUpdateMessage::ImaginateStatusUpdate);
};
let host_name = imaginate_preferences.get_host_name();
let host_name = editor_preferences.hostname();
imaginate_maybe_fail(
image,
host_name,

View File

@@ -225,10 +225,11 @@ fn to_svg_string(vector: &VectorData, transform: DAffine2) -> String {
fn from_svg_string(svg_string: &str) -> VectorData {
let svg = format!(r#"<svg xmlns="http://www.w3.org/2000/svg"><path d="{}"></path></svg>"#, svg_string);
let Some(tree) = usvg::Tree::from_str(&svg, &Default::default()).ok() else {
let fontdb = usvg::fontdb::Database::new();
let Some(tree) = usvg::Tree::from_str(&svg, &Default::default(), &fontdb).ok() else {
return VectorData::empty();
};
let Some(usvg::Node::Path(path)) = tree.root.children.first() else {
let Some(usvg::Node::Path(path)) = tree.root().children().first() else {
return VectorData::empty();
};
@@ -239,10 +240,10 @@ pub fn convert_usvg_path(path: &usvg::Path) -> Vec<Subpath<PointId>> {
let mut subpaths = Vec::new();
let mut groups = Vec::new();
let mut points = path.data.points().iter();
let mut points = path.data().points().iter();
let to_vec = |p: &usvg::tiny_skia_path::Point| DVec2::new(p.x as f64, p.y as f64);
for verb in path.data.verbs() {
for verb in path.data().verbs() {
match verb {
usvg::tiny_skia_path::PathVerb::Move => {
subpaths.push(Subpath::new(std::mem::take(&mut groups), false));

View File

@@ -1,4 +1,4 @@
use dyn_any::DynFuture;
pub use graph_craft::document::value::RenderOutput;
pub use graph_craft::wasm_application_io::*;
#[cfg(target_arch = "wasm32")]
use graphene_core::application_io::SurfaceHandle;
@@ -14,10 +14,8 @@ use graphene_core::{Color, WasmNotSend};
#[cfg(target_arch = "wasm32")]
use base64::Engine;
use core::future::Future;
#[cfg(target_arch = "wasm32")]
use glam::DAffine2;
use std::marker::PhantomData;
use std::sync::Arc;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::Clamped;
@@ -87,15 +85,6 @@ fn decode_image_node<'a: 'input>(data: Arc<[u8]>) -> ImageFrame<Color> {
};
image
}
pub use graph_craft::document::value::RenderOutput;
pub struct RenderNode<Data, Surface, Parameter> {
data: Data,
#[cfg(all(any(feature = "resvg", feature = "vello"), target_arch = "wasm32"))]
surface_handle: Surface,
#[cfg(not(all(any(feature = "resvg", feature = "vello"), target_arch = "wasm32")))]
surface_handle: PhantomData<Surface>,
parameter: PhantomData<Parameter>,
}
fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_params: RenderParams, footprint: Footprint) -> RenderOutput {
if !data.contains_artboard() && !render_params.hide_artboards {
@@ -115,50 +104,28 @@ fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_p
RenderOutput::Svg(render.svg.to_svg_string())
}
#[cfg(all(any(feature = "resvg", feature = "vello"), target_arch = "wasm32"))]
fn render_canvas(
data: impl GraphicElementRendered,
mut render: SvgRender,
render_params: RenderParams,
footprint: Footprint,
editor: &'_ WasmEditorApi,
surface_handle: wgpu_executor::WindowHandle,
) -> RenderOutput {
let resolution = footprint.resolution;
data.render_svg(&mut render, &render_params);
// TODO: reenable once we switch to full node graph
let min = footprint.transform.inverse().transform_point2((0., 0.).into());
let max = footprint.transform.inverse().transform_point2(resolution.as_dvec2());
render.format_svg(min, max);
let string = render.svg.to_svg_string();
let _array = string.as_bytes();
let canvas = &surface_handle.surface;
canvas.set_width(resolution.x);
canvas.set_height(resolution.y);
let usvg_tree = data.to_usvg_tree(resolution, [min, max]);
#[cfg(feature = "vello")]
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
async fn render_canvas(render_config: RenderConfig, data: impl GraphicElementRendered, editor: &WasmEditorApi, surface_handle: wgpu_executor::WgpuSurface) -> RenderOutput {
if let Some(exec) = editor.application_io.as_ref().unwrap().gpu_executor() {
use vello::*;
if let Some(_exec) = editor.application_io.as_ref().unwrap().gpu_executor() {
todo!()
let footprint = render_config.viewport;
let mut scene = Scene::new();
let mut child = Scene::new();
data.render_to_vello(&mut child, glam::DAffine2::IDENTITY);
// TODO: Instead of applying the transform here, pass the transform during the translation to avoid the O(Nr cost
scene.append(&child, Some(kurbo::Affine::new(footprint.transform.to_cols_array())));
exec.render_vello_scene(&scene, &surface_handle, footprint.resolution.x, footprint.resolution.y)
.await
.expect("Failed to render Vello scene");
} else {
let pixmap_size = usvg_tree.size.to_int_size();
let mut pixmap = resvg::tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap();
resvg::render(&usvg_tree, resvg::tiny_skia::Transform::default(), &mut pixmap.as_mut());
let array: Clamped<&[u8]> = Clamped(pixmap.data());
let context = canvas.get_context("2d").unwrap().unwrap().dyn_into::<CanvasRenderingContext2d>().unwrap();
let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(array, pixmap_size.width(), pixmap_size.height()).expect("Failed to construct ImageData");
context.put_image_data(&image_data, 0.0, 0.0).unwrap();
unreachable!("Attempted to render with Vello when no GPU executor is available");
}
/*
let preamble = "data:image/svg+xml;base64,";
let mut base64_string = String::with_capacity(preamble.len() + array.len() * 4);
base64_string.push_str(preamble);
base64::engine::general_purpose::STANDARD.encode_string(array, &mut base64_string);
let image_data = web_sys::HtmlImageElement::new().unwrap();
image_data.set_src(base64_string.as_str());
wasm_bindgen_futures::JsFuture::from(image_data.decode()).await.unwrap();
context.draw_image_with_html_image_element(&image_data, 0.0, 0.0).unwrap();
*/
let frame = graphene_core::application_io::SurfaceHandleFrame {
surface_handle,
transform: glam::DAffine2::IDENTITY,
@@ -226,90 +193,44 @@ async fn rasterize<_T: GraphicElementRendered + graphene_core::transform::Transf
}
}
// Render with the data node taking in Footprint.
impl<'input, T: 'input + GraphicElementRendered, Data: 'input, Surface: 'input> Node<'input, RenderConfig> for RenderNode<Data, Surface, Footprint>
where
for<'a> Data: Node<'a, Footprint, Output: Future<Output = T> + WasmNotSend>,
for<'a> Surface: Node<'a, (), Output: Future<Output = wgpu_executor::WindowHandle> + WasmNotSend> + 'input,
{
type Output = DynFuture<'input, RenderOutput>;
#[inline]
fn eval(&'input self, render_config: RenderConfig) -> Self::Output {
let footprint = render_config.viewport;
#[cfg(all(any(feature = "resvg", feature = "vello"), target_arch = "wasm32"))]
let RenderConfig { hide_artboards, for_export, .. } = render_config;
#[cfg(all(any(feature = "resvg", feature = "vello"), target_arch = "wasm32"))]
let render_params = RenderParams::new(render_config.view_mode, ImageRenderMode::Base64, None, false, hide_artboards, for_export);
let data_fut = self.data.eval(footprint);
#[cfg(all(any(feature = "resvg", feature = "vello"), target_arch = "wasm32"))]
let surface_fut = self.surface_handle.eval(());
Box::pin(async move {
let data = data_fut.await;
let footprint = render_config.viewport;
let RenderConfig { hide_artboards, for_export, .. } = render_config;
let render_params = RenderParams::new(render_config.view_mode, ImageRenderMode::Base64, None, false, hide_artboards, for_export);
let output_format = render_config.export_format;
match output_format {
ExportFormat::Svg => render_svg(data, SvgRender::new(), render_params, footprint),
#[cfg(all(any(feature = "resvg", feature = "vello"), target_arch = "wasm32"))]
ExportFormat::Canvas => render_canvas(data, SvgRender::new(), render_params, footprint, editor, surface_fut.await),
_ => todo!("Non-SVG render output for {output_format:?}"),
}
})
}
pub struct RenderNode<EditorApi, Data, Surface> {
editor_api: EditorApi,
data: Data,
_surface_handle: Surface,
}
// Render with the data node taking in ().
impl<'input, T: 'input + GraphicElementRendered, Data: 'input, Surface: 'input> Node<'input, RenderConfig> for RenderNode<Data, Surface, ()>
where
for<'a> Data: Node<'a, (), Output: Future<Output = T> + WasmNotSend>,
for<'a> Surface: Node<'a, (), Output: Future<Output = wgpu_executor::WindowHandle> + WasmNotSend> + 'input,
{
type Output = DynFuture<'input, RenderOutput>;
#[node_macro::node_fn(RenderNode)]
async fn render_node<'a: 'input, T: 'input + GraphicElementRendered + WasmNotSend>(
render_config: RenderConfig,
editor_api: &'a WasmEditorApi,
data: impl Node<Footprint, Output = T>,
_surface_handle: impl Node<Footprint, Output = Option<wgpu_executor::WgpuSurface>>,
) -> RenderOutput {
let footprint = render_config.viewport;
#[inline]
fn eval(&'input self, render_config: RenderConfig) -> Self::Output {
#[cfg(all(any(feature = "resvg", feature = "vello"), target_arch = "wasm32"))]
let RenderConfig { hide_artboards, for_export, .. } = render_config;
#[cfg(all(any(feature = "resvg", feature = "vello"), target_arch = "wasm32"))]
let render_params = RenderParams::new(render_config.view_mode, ImageRenderMode::Base64, None, false, hide_artboards, for_export);
let RenderConfig { hide_artboards, for_export, .. } = render_config;
let render_params = RenderParams::new(render_config.view_mode, ImageRenderMode::Base64, None, false, hide_artboards, for_export);
let data_fut = self.data.eval(());
#[cfg(all(any(feature = "resvg", feature = "vello"), target_arch = "wasm32"))]
let surface_fut = self.surface_handle.eval(());
Box::pin(async move {
let data = data_fut.await;
let footprint = render_config.viewport;
let data = self.data.eval(footprint).await;
#[cfg(all(feature = "vello", target_arch = "wasm32"))]
let surface_handle = self._surface_handle.eval(footprint).await;
let use_vello = editor_api.editor_preferences.use_vello();
#[cfg(all(feature = "vello", target_arch = "wasm32"))]
let use_vello = use_vello && surface_handle.is_some();
let RenderConfig { hide_artboards, for_export, .. } = render_config;
let render_params = RenderParams::new(render_config.view_mode, ImageRenderMode::Base64, None, false, hide_artboards, for_export);
let output_format = render_config.export_format;
match output_format {
ExportFormat::Svg => render_svg(data, SvgRender::new(), render_params, footprint),
#[cfg(all(any(feature = "resvg", feature = "vello"), target_arch = "wasm32"))]
ExportFormat::Canvas => render_canvas(data, SvgRender::new(), render_params, footprint, editor, surface_fut.await),
_ => todo!("Non-SVG render output for {output_format:?}"),
let output_format = render_config.export_format;
match output_format {
ExportFormat::Svg => render_svg(data, SvgRender::new(), render_params, footprint),
ExportFormat::Canvas => {
if use_vello && editor_api.application_io.as_ref().unwrap().gpu_executor().is_some() {
#[cfg(all(feature = "vello", target_arch = "wasm32"))]
return render_canvas(render_config, data, editor_api, surface_handle.unwrap()).await;
#[cfg(not(all(feature = "vello", target_arch = "wasm32")))]
render_svg(data, SvgRender::new(), render_params, footprint)
} else {
render_svg(data, SvgRender::new(), render_params, footprint)
}
})
}
}
#[automatically_derived]
impl<Data, Surface, Parameter> RenderNode<Data, Surface, Parameter> {
pub fn new(data: Data, _surface_handle: Surface) -> Self {
Self {
data,
#[cfg(all(any(feature = "resvg", feature = "vello"), target_arch = "wasm32"))]
surface_handle: _surface_handle,
#[cfg(not(all(any(feature = "resvg", feature = "vello"), target_arch = "wasm32")))]
surface_handle: PhantomData,
parameter: PhantomData,
}
_ => todo!("Non-SVG render output for {output_format:?}"),
}
}