mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 23:08:05 +08:00
Lay groundwork for directly rendering to the canvas without a cpu roundrip (#1291)
* Add Texture handle type * Add Texture View to shader inputs * Implement basic rendering pipeline * Render first texture using render pipeline * Fix output color space * Precompute the rendering pipeline * Move gpu context creation to editor api * Port gpu-executor nodes to node registry * Fix canvas nodes and make code compile for non wasm targets * Pin wasm-bindgen version * Disable miri temoporarily for better ci times * Fix formatting * Remove unsafe block * Bump wasm-pack version * Bump wasm-bindgen version * Add gpu feature guard for push node * Make Into node async
This commit is contained in:
committed by
Keavon Chambers
parent
0c93a62d55
commit
45b04f4eb9
@@ -32,7 +32,7 @@ async = ["async-trait", "alloc"]
|
||||
nightly = []
|
||||
alloc = ["dyn-any", "bezier-rs", "once_cell"]
|
||||
type_id_logging = []
|
||||
wasm = ["wasm-bindgen", "web-sys", "js-sys", "std"]
|
||||
wasm = ["web-sys"]
|
||||
|
||||
[dependencies]
|
||||
dyn-any = { path = "../../libraries/dyn-any", features = [
|
||||
@@ -77,10 +77,4 @@ js-sys = { version = "0.3.55", optional = true }
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.4"
|
||||
optional = true
|
||||
features = [
|
||||
"Window",
|
||||
"CanvasRenderingContext2d",
|
||||
"ImageData",
|
||||
"Document",
|
||||
"HtmlCanvasElement",
|
||||
]
|
||||
features = ["HtmlCanvasElement"]
|
||||
|
||||
@@ -45,7 +45,7 @@ impl<S> From<SurfaceHandleFrame<S>> for SurfaceFrame {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SurfaceHandle<Surface> {
|
||||
pub surface_id: SurfaceId,
|
||||
pub surface: Surface,
|
||||
@@ -55,7 +55,7 @@ unsafe impl<T: 'static> StaticType for SurfaceHandle<T> {
|
||||
type Static = SurfaceHandle<T>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SurfaceHandleFrame<Surface> {
|
||||
pub surface_handle: Arc<SurfaceHandle<Surface>>,
|
||||
pub transform: DAffine2,
|
||||
@@ -87,12 +87,18 @@ impl<'a, Surface> Drop for SurfaceHandle<'a, Surface> {
|
||||
|
||||
pub trait ApplicationIo {
|
||||
type Surface;
|
||||
type Executor;
|
||||
fn create_surface(&self) -> SurfaceHandle<Self::Surface>;
|
||||
fn destroy_surface(&self, surface_id: SurfaceId);
|
||||
fn gpu_executor(&self) -> Option<&Self::Executor> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ApplicationIo> ApplicationIo for &T {
|
||||
type Surface = T::Surface;
|
||||
type Executor = T::Executor;
|
||||
|
||||
fn create_surface(&self) -> SurfaceHandle<T::Surface> {
|
||||
(**self).create_surface()
|
||||
}
|
||||
@@ -100,6 +106,10 @@ impl<T: ApplicationIo> ApplicationIo for &T {
|
||||
fn destroy_surface(&self, surface_id: SurfaceId) {
|
||||
(**self).destroy_surface(surface_id)
|
||||
}
|
||||
|
||||
fn gpu_executor(&self) -> Option<&T::Executor> {
|
||||
(**self).gpu_executor()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EditorApi<'a, Io> {
|
||||
@@ -162,6 +172,3 @@ impl ExtractImageFrame {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
pub mod wasm_application_io;
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
use std::{cell::RefCell, collections::HashMap};
|
||||
|
||||
use super::{ApplicationIo, SurfaceHandle, SurfaceHandleFrame, SurfaceId};
|
||||
use crate::{
|
||||
raster::{color::SRGBA8, ImageFrame},
|
||||
Node,
|
||||
};
|
||||
use alloc::sync::Arc;
|
||||
use dyn_any::StaticType;
|
||||
use js_sys::{Object, Reflect};
|
||||
use wasm_bindgen::{Clamped, JsCast, JsValue};
|
||||
use web_sys::{window, CanvasRenderingContext2d, HtmlCanvasElement};
|
||||
|
||||
pub struct Canvas(CanvasRenderingContext2d);
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WasmApplicationIo {
|
||||
ids: RefCell<u64>,
|
||||
canvases: RefCell<HashMap<SurfaceId, CanvasRenderingContext2d>>,
|
||||
}
|
||||
|
||||
impl WasmApplicationIo {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl StaticType for WasmApplicationIo {
|
||||
type Static = WasmApplicationIo;
|
||||
}
|
||||
|
||||
pub type WasmEditorApi<'a> = super::EditorApi<'a, WasmApplicationIo>;
|
||||
|
||||
impl ApplicationIo for WasmApplicationIo {
|
||||
type Surface = CanvasRenderingContext2d;
|
||||
|
||||
fn create_surface(&self) -> SurfaceHandle<Self::Surface> {
|
||||
let wrapper = || {
|
||||
let document = window().expect("should have a window in this context").document().expect("window should have a document");
|
||||
|
||||
let canvas: HtmlCanvasElement = document.create_element("canvas")?.dyn_into::<HtmlCanvasElement>()?;
|
||||
// TODO: replace "2d" with "bitmaprenderer" once we switch to ImageBitmap (lives on gpu) from ImageData (lives on cpu)
|
||||
let context = canvas.get_context("2d").unwrap().unwrap().dyn_into::<CanvasRenderingContext2d>().unwrap();
|
||||
let mut guard = self.ids.borrow_mut();
|
||||
let id = SurfaceId(*guard);
|
||||
*guard += 1;
|
||||
self.canvases.borrow_mut().insert(id, context.clone());
|
||||
// store the canvas in the global scope so it doesn't get garbage collected
|
||||
|
||||
let window = window().expect("should have a window in this context");
|
||||
let window = Object::from(window);
|
||||
|
||||
let image_canvases_key = JsValue::from_str("imageCanvases");
|
||||
|
||||
let mut canvases = Reflect::get(&window, &image_canvases_key);
|
||||
if canvases.is_err() {
|
||||
Reflect::set(&JsValue::from(web_sys::window().unwrap()), &image_canvases_key, &Object::new()).unwrap();
|
||||
canvases = Reflect::get(&window, &image_canvases_key);
|
||||
}
|
||||
|
||||
// Convert key and value to JsValue
|
||||
let js_key = JsValue::from_str(format!("canvas{}", id.0).as_str());
|
||||
let js_value = JsValue::from(context.clone());
|
||||
|
||||
let canvases = Object::from(canvases.unwrap());
|
||||
|
||||
// Use Reflect API to set property
|
||||
Reflect::set(&canvases, &js_key, &js_value)?;
|
||||
Ok::<_, JsValue>(SurfaceHandle { surface_id: id, surface: context })
|
||||
};
|
||||
|
||||
wrapper().expect("should be able to set canvas in global scope")
|
||||
}
|
||||
|
||||
fn destroy_surface(&self, surface_id: SurfaceId) {
|
||||
self.canvases.borrow_mut().remove(&surface_id);
|
||||
|
||||
let window = window().expect("should have a window in this context");
|
||||
let window = Object::from(window);
|
||||
|
||||
let image_canvases_key = JsValue::from_str("imageCanvases");
|
||||
|
||||
let wrapper = || {
|
||||
if let Ok(canvases) = Reflect::get(&window, &image_canvases_key) {
|
||||
// Convert key and value to JsValue
|
||||
let js_key = JsValue::from_str(format!("canvas{}", surface_id.0).as_str());
|
||||
|
||||
// Use Reflect API to set property
|
||||
Reflect::delete_property(&canvases.into(), &js_key)?;
|
||||
}
|
||||
Ok::<_, JsValue>(())
|
||||
};
|
||||
|
||||
wrapper().expect("should be able to set canvas in global scope")
|
||||
}
|
||||
}
|
||||
|
||||
pub type WasmSurfaceHandle = SurfaceHandle<CanvasRenderingContext2d>;
|
||||
pub type WasmSurfaceHandleFrame = SurfaceHandleFrame<CanvasRenderingContext2d>;
|
||||
|
||||
pub struct CreateSurfaceNode {}
|
||||
|
||||
#[node_macro::node_fn(CreateSurfaceNode)]
|
||||
fn create_surface_node<'a: 'input>(editor: WasmEditorApi<'a>) -> Arc<SurfaceHandle<CanvasRenderingContext2d>> {
|
||||
editor.application_io.create_surface().into()
|
||||
}
|
||||
|
||||
pub struct DrawImageFrameNode<Surface> {
|
||||
surface_handle: Surface,
|
||||
}
|
||||
|
||||
#[node_macro::node_fn(DrawImageFrameNode)]
|
||||
async fn draw_image_frame_node<'a: 'input>(image: ImageFrame<SRGBA8>, surface_handle: Arc<SurfaceHandle<CanvasRenderingContext2d>>) -> SurfaceHandleFrame<CanvasRenderingContext2d> {
|
||||
let image_data = image.image.data;
|
||||
let array: Clamped<&[u8]> = Clamped(bytemuck::cast_slice(image_data.as_slice()));
|
||||
if image.image.width > 0 && image.image.height > 0 {
|
||||
let canvas = surface_handle.surface.canvas().expect("Failed to get canvas");
|
||||
canvas.set_width(image.image.width);
|
||||
canvas.set_height(image.image.height);
|
||||
let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(array, image.image.width, image.image.height).expect("Failed to construct ImageData");
|
||||
surface_handle.surface.put_image_data(&image_data, 0.0, 0.0).unwrap();
|
||||
}
|
||||
SurfaceHandleFrame {
|
||||
surface_handle,
|
||||
transform: image.transform,
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ pub mod value;
|
||||
#[cfg(feature = "gpu")]
|
||||
pub mod gpu;
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
pub mod memo;
|
||||
pub mod storage;
|
||||
|
||||
@@ -34,6 +35,7 @@ pub use graphic_element::*;
|
||||
#[cfg(feature = "alloc")]
|
||||
pub mod vector;
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
pub mod application_io;
|
||||
|
||||
pub mod quantization;
|
||||
@@ -146,6 +148,9 @@ impl<'i, I: 'i, O: 'i> Node<'i, I> for Pin<&'i (dyn NodeIO<'i, I, Output = O> +
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
pub use crate::application_io::{ExtractImageFrame, SurfaceFrame, SurfaceId};
|
||||
#[cfg(feature = "wasm")]
|
||||
pub use application_io::{wasm_application_io, wasm_application_io::WasmEditorApi as EditorApi};
|
||||
pub type WasmSurfaceHandle = application_io::SurfaceHandle<web_sys::HtmlCanvasElement>;
|
||||
#[cfg(feature = "wasm")]
|
||||
pub type WasmSurfaceHandleFrame = application_io::SurfaceHandleFrame<web_sys::HtmlCanvasElement>;
|
||||
|
||||
@@ -4,8 +4,8 @@ use core::future::Future;
|
||||
#[cfg(feature = "alloc")]
|
||||
use alloc::sync::Arc;
|
||||
use core::cell::Cell;
|
||||
use core::marker::PhantomData;
|
||||
use core::pin::Pin;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
// Caches the output of a given Node and acts as a proxy
|
||||
#[derive(Default)]
|
||||
@@ -103,8 +103,6 @@ impl<'i, T: 'i + Clone> Node<'i, Option<T>> for LetNode<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::marker::Unpin for LetNode<T> {}
|
||||
|
||||
impl<T> LetNode<T> {
|
||||
pub fn new() -> LetNode<T> {
|
||||
LetNode { cache: Default::default() }
|
||||
|
||||
@@ -211,7 +211,7 @@ pub struct IntoNode<I, O> {
|
||||
_o: PhantomData<O>,
|
||||
}
|
||||
#[node_macro::node_fn(IntoNode<_I, _O>)]
|
||||
fn into<_I, _O>(input: _I) -> _O
|
||||
async fn into<_I, _O>(input: _I) -> _O
|
||||
where
|
||||
_I: Into<_O>,
|
||||
{
|
||||
|
||||
@@ -88,7 +88,7 @@ impl BrushCacheImpl {
|
||||
|
||||
impl Hash for BrushCacheImpl {
|
||||
// Zero hash.
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {}
|
||||
fn hash<H: core::hash::Hasher>(&self, _state: &mut H) {}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
mod font_cache;
|
||||
mod to_path;
|
||||
|
||||
use crate::EditorApi;
|
||||
use crate::application_io::EditorApi;
|
||||
pub use font_cache::*;
|
||||
use node_macro::node_fn;
|
||||
pub use to_path::*;
|
||||
@@ -15,7 +15,7 @@ pub struct TextGenerator<Text, FontName, Size> {
|
||||
}
|
||||
|
||||
#[node_fn(TextGenerator)]
|
||||
fn generate_text<'a: 'input>(editor: EditorApi<'a>, text: String, font_name: Font, font_size: f64) -> crate::vector::VectorData {
|
||||
fn generate_text<'a: 'input, T>(editor: EditorApi<'a, T>, text: String, font_name: Font, font_size: f64) -> crate::vector::VectorData {
|
||||
let buzz_face = editor.font_cache.get(&font_name).map(|data| load_face(data));
|
||||
crate::vector::VectorData::from_subpaths(to_path(&text, buzz_face, font_size, None))
|
||||
}
|
||||
|
||||
@@ -108,6 +108,10 @@ pub enum Type {
|
||||
Future(Box<Type>),
|
||||
}
|
||||
|
||||
unsafe impl StaticType for Type {
|
||||
type Static = Self;
|
||||
}
|
||||
|
||||
impl Type {
|
||||
pub fn is_generic(&self) -> bool {
|
||||
matches!(self, Type::Generic(_))
|
||||
|
||||
Reference in New Issue
Block a user