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:
parent
6e66c79392
commit
a932eaedcf
|
|
@ -4,7 +4,6 @@ use winit::window::{Window as WinitWindow, WindowAttributes};
|
||||||
|
|
||||||
use crate::consts::APP_NAME;
|
use crate::consts::APP_NAME;
|
||||||
use crate::event::AppEventScheduler;
|
use crate::event::AppEventScheduler;
|
||||||
use crate::window::mac::NativeWindowImpl;
|
|
||||||
use crate::wrapper::messages::MenuItem;
|
use crate::wrapper::messages::MenuItem;
|
||||||
|
|
||||||
pub(crate) trait NativeWindow {
|
pub(crate) trait NativeWindow {
|
||||||
|
|
@ -37,7 +36,7 @@ pub(crate) struct Window {
|
||||||
|
|
||||||
impl Window {
|
impl Window {
|
||||||
pub(crate) fn init() {
|
pub(crate) fn init() {
|
||||||
NativeWindowImpl::init();
|
native::NativeWindowImpl::init();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new(event_loop: &dyn ActiveEventLoop, app_event_scheduler: AppEventScheduler) -> Self {
|
pub(crate) fn new(event_loop: &dyn ActiveEventLoop, app_event_scheduler: AppEventScheduler) -> Self {
|
||||||
|
|
|
||||||
|
|
@ -882,70 +882,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||||
properties: None,
|
properties: None,
|
||||||
},
|
},
|
||||||
#[cfg(feature = "gpu")]
|
#[cfg(feature = "gpu")]
|
||||||
DocumentNodeDefinition {
|
|
||||||
identifier: "Create GPU Surface",
|
|
||||||
category: "Debug: GPU",
|
|
||||||
node_template: NodeTemplate {
|
|
||||||
document_node: DocumentNode {
|
|
||||||
implementation: DocumentNodeImplementation::Network(NodeNetwork {
|
|
||||||
exports: vec![NodeInput::node(NodeId(1), 0)],
|
|
||||||
nodes: [
|
|
||||||
DocumentNode {
|
|
||||||
inputs: vec![NodeInput::scope("editor-api")],
|
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(wgpu_executor::create_gpu_surface::IDENTIFIER),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
DocumentNode {
|
|
||||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
|
||||||
implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(id, node)| (NodeId(id as u64), node))
|
|
||||||
.collect(),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
persistent_node_metadata: DocumentNodePersistentMetadata {
|
|
||||||
output_names: vec!["GPU Surface".to_string()],
|
|
||||||
network_metadata: Some(NodeNetworkMetadata {
|
|
||||||
persistent_metadata: NodeNetworkPersistentMetadata {
|
|
||||||
node_metadata: [
|
|
||||||
DocumentNodeMetadata {
|
|
||||||
persistent_metadata: DocumentNodePersistentMetadata {
|
|
||||||
display_name: "Create GPU Surface".to_string(),
|
|
||||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
DocumentNodeMetadata {
|
|
||||||
persistent_metadata: DocumentNodePersistentMetadata {
|
|
||||||
display_name: "Cache".to_string(),
|
|
||||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 0)),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(id, node)| (NodeId(id as u64), node))
|
|
||||||
.collect(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
description: Cow::Borrowed("TODO"),
|
|
||||||
properties: None,
|
|
||||||
},
|
|
||||||
#[cfg(feature = "gpu")]
|
|
||||||
DocumentNodeDefinition {
|
DocumentNodeDefinition {
|
||||||
identifier: "Upload Texture",
|
identifier: "Upload Texture",
|
||||||
category: "Debug: GPU",
|
category: "Debug: GPU",
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ use crate::messages::prelude::*;
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||||
use crate::messages::tool::common_functionality::utility_functions::make_path_editable_is_allowed;
|
use crate::messages::tool::common_functionality::utility_functions::make_path_editable_is_allowed;
|
||||||
use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType};
|
use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType};
|
||||||
|
use crate::messages::viewport::ToPhysical;
|
||||||
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
|
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
|
||||||
use derivative::*;
|
use derivative::*;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
|
|
@ -364,12 +365,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
let node_to_inspect = self.node_to_inspect();
|
let node_to_inspect = self.node_to_inspect();
|
||||||
|
|
||||||
let scale = viewport.scale();
|
let scale = viewport.scale();
|
||||||
let resolution = viewport.size().into_dvec2().round().as_uvec2();
|
// Use exact physical dimensions from browser (via ResizeObserver's devicePixelContentBoxSize)
|
||||||
|
let physical_resolution = viewport.size().to_physical().into_dvec2().round().as_uvec2();
|
||||||
|
|
||||||
if let Ok(message) = self.executor.submit_node_graph_evaluation(
|
if let Ok(message) = self.executor.submit_node_graph_evaluation(
|
||||||
self.documents.get_mut(document_id).expect("Tried to render non-existent document"),
|
self.documents.get_mut(document_id).expect("Tried to render non-existent document"),
|
||||||
*document_id,
|
*document_id,
|
||||||
resolution,
|
physical_resolution,
|
||||||
scale,
|
scale,
|
||||||
timing_information,
|
timing_information,
|
||||||
node_to_inspect,
|
node_to_inspect,
|
||||||
|
|
@ -970,11 +972,12 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||||
};
|
};
|
||||||
|
|
||||||
let scale = viewport.scale();
|
let scale = viewport.scale();
|
||||||
let resolution = viewport.size().into_dvec2().round().as_uvec2();
|
// Use exact physical dimensions from browser (via ResizeObserver's devicePixelContentBoxSize)
|
||||||
|
let physical_resolution = viewport.size().to_physical().into_dvec2().round().as_uvec2();
|
||||||
|
|
||||||
let result = self
|
let result = self
|
||||||
.executor
|
.executor
|
||||||
.submit_node_graph_evaluation(document, document_id, resolution, scale, timing_information, node_to_inspect, ignore_hash);
|
.submit_node_graph_evaluation(document, document_id, physical_resolution, scale, timing_information, node_to_inspect, ignore_hash);
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Err(description) => {
|
Err(description) => {
|
||||||
|
|
|
||||||
|
|
@ -421,8 +421,8 @@ impl NodeGraphExecutor {
|
||||||
let matrix = format_transform_matrix(frame.transform);
|
let matrix = format_transform_matrix(frame.transform);
|
||||||
let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") };
|
let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") };
|
||||||
let svg = format!(
|
let svg = format!(
|
||||||
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="{}"></div></foreignObject></svg>"#,
|
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="{}" data-is-viewport="true"></div></foreignObject></svg>"#,
|
||||||
frame.resolution.x, frame.resolution.y, frame.surface_id.0
|
frame.resolution.x, frame.resolution.y, frame.surface_id.0,
|
||||||
);
|
);
|
||||||
self.last_svg_canvas = Some(frame);
|
self.last_svg_canvas = Some(frame);
|
||||||
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
|
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,10 @@ pub struct NodeRuntime {
|
||||||
/// The current renders of the thumbnails for layer nodes.
|
/// The current renders of the thumbnails for layer nodes.
|
||||||
thumbnail_renders: HashMap<NodeId, Vec<SvgSegment>>,
|
thumbnail_renders: HashMap<NodeId, Vec<SvgSegment>>,
|
||||||
vector_modify: HashMap<NodeId, Vector>,
|
vector_modify: HashMap<NodeId, Vector>,
|
||||||
|
|
||||||
|
/// Cached surface for WASM viewport rendering (reused across frames)
|
||||||
|
#[cfg(all(target_family = "wasm", feature = "gpu"))]
|
||||||
|
wasm_viewport_surface: Option<wgpu_executor::WgpuSurface>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Messages passed from the editor thread to the node runtime thread.
|
/// Messages passed from the editor thread to the node runtime thread.
|
||||||
|
|
@ -131,6 +135,8 @@ impl NodeRuntime {
|
||||||
thumbnail_renders: Default::default(),
|
thumbnail_renders: Default::default(),
|
||||||
vector_modify: Default::default(),
|
vector_modify: Default::default(),
|
||||||
inspect_state: None,
|
inspect_state: None,
|
||||||
|
#[cfg(all(target_family = "wasm", feature = "gpu"))]
|
||||||
|
wasm_viewport_surface: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -259,6 +265,82 @@ impl NodeRuntime {
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
#[cfg(all(target_family = "wasm", feature = "gpu"))]
|
||||||
|
Ok(TaggedValue::RenderOutput(RenderOutput {
|
||||||
|
data: RenderOutputType::Texture(image_texture),
|
||||||
|
metadata,
|
||||||
|
})) if !render_config.for_export => {
|
||||||
|
// On WASM, for viewport rendering, blit the texture to a surface and return a CanvasFrame
|
||||||
|
let app_io = self.editor_api.application_io.as_ref().unwrap();
|
||||||
|
let executor = app_io.gpu_executor().expect("GPU executor should be available when we receive a texture");
|
||||||
|
|
||||||
|
// Get or create the cached surface
|
||||||
|
if self.wasm_viewport_surface.is_none() {
|
||||||
|
let surface_handle = app_io.create_window();
|
||||||
|
let wasm_surface = executor
|
||||||
|
.create_surface(graphene_std::wasm_application_io::WasmSurfaceHandle {
|
||||||
|
surface: surface_handle.surface.clone(),
|
||||||
|
window_id: surface_handle.window_id,
|
||||||
|
})
|
||||||
|
.expect("Failed to create surface");
|
||||||
|
self.wasm_viewport_surface = Some(Arc::new(wasm_surface));
|
||||||
|
}
|
||||||
|
|
||||||
|
let surface = self.wasm_viewport_surface.as_ref().unwrap();
|
||||||
|
|
||||||
|
// Use logical resolution for CSS sizing, physical resolution for the actual surface/texture
|
||||||
|
let physical_resolution = render_config.viewport.resolution;
|
||||||
|
let logical_resolution = physical_resolution.as_dvec2() / render_config.scale;
|
||||||
|
|
||||||
|
// Blit the texture to the surface
|
||||||
|
let mut encoder = executor.context.device.create_command_encoder(&vello::wgpu::CommandEncoderDescriptor {
|
||||||
|
label: Some("Texture to Surface Blit"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure the surface at physical resolution (for HiDPI displays)
|
||||||
|
let surface_inner = &surface.surface.inner;
|
||||||
|
let surface_caps = surface_inner.get_capabilities(&executor.context.adapter);
|
||||||
|
surface_inner.configure(
|
||||||
|
&executor.context.device,
|
||||||
|
&vello::wgpu::SurfaceConfiguration {
|
||||||
|
usage: vello::wgpu::TextureUsages::RENDER_ATTACHMENT | vello::wgpu::TextureUsages::COPY_DST,
|
||||||
|
format: vello::wgpu::TextureFormat::Rgba8Unorm,
|
||||||
|
width: physical_resolution.x,
|
||||||
|
height: physical_resolution.y,
|
||||||
|
present_mode: surface_caps.present_modes[0],
|
||||||
|
alpha_mode: vello::wgpu::CompositeAlphaMode::Opaque,
|
||||||
|
view_formats: vec![],
|
||||||
|
desired_maximum_frame_latency: 2,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let surface_texture = surface_inner.get_current_texture().expect("Failed to get surface texture");
|
||||||
|
|
||||||
|
// Blit the rendered texture to the surface
|
||||||
|
surface.surface.blitter.copy(
|
||||||
|
&executor.context.device,
|
||||||
|
&mut encoder,
|
||||||
|
&image_texture.texture.create_view(&vello::wgpu::TextureViewDescriptor::default()),
|
||||||
|
&surface_texture.texture.create_view(&vello::wgpu::TextureViewDescriptor::default()),
|
||||||
|
);
|
||||||
|
|
||||||
|
executor.context.queue.submit([encoder.finish()]);
|
||||||
|
surface_texture.present();
|
||||||
|
|
||||||
|
let frame = graphene_std::application_io::SurfaceFrame {
|
||||||
|
surface_id: surface.window_id,
|
||||||
|
resolution: logical_resolution,
|
||||||
|
transform: glam::DAffine2::IDENTITY,
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
Ok(TaggedValue::RenderOutput(RenderOutput {
|
||||||
|
data: RenderOutputType::CanvasFrame(frame),
|
||||||
|
metadata,
|
||||||
|
})),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
Ok(TaggedValue::RenderOutput(RenderOutput {
|
Ok(TaggedValue::RenderOutput(RenderOutput {
|
||||||
data: RenderOutputType::Texture(texture),
|
data: RenderOutputType::Texture(texture),
|
||||||
metadata,
|
metadata,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext, onMount, tick } from "svelte";
|
import { getContext, onMount, onDestroy, tick } from "svelte";
|
||||||
|
|
||||||
import type { Editor } from "@graphite/editor";
|
import type { Editor } from "@graphite/editor";
|
||||||
import {
|
import {
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
import type { DocumentState } from "@graphite/state-providers/document";
|
import type { DocumentState } from "@graphite/state-providers/document";
|
||||||
import { textInputCleanup } from "@graphite/utility-functions/keyboard-entry";
|
import { textInputCleanup } from "@graphite/utility-functions/keyboard-entry";
|
||||||
import { extractPixelData, rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization";
|
import { extractPixelData, rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization";
|
||||||
import { updateBoundsOfViewports as updateViewport } from "@graphite/utility-functions/viewports";
|
import { setupViewportResizeObserver, cleanupViewportResizeObserver } from "@graphite/utility-functions/viewports";
|
||||||
|
|
||||||
import EyedropperPreview, { ZOOM_WINDOW_DIMENSIONS } from "@graphite/components/floating-menus/EyedropperPreview.svelte";
|
import EyedropperPreview, { ZOOM_WINDOW_DIMENSIONS } from "@graphite/components/floating-menus/EyedropperPreview.svelte";
|
||||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||||
|
|
@ -203,9 +203,18 @@
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let canvas = (window as any).imageCanvases[canvasName];
|
let canvas = (window as any).imageCanvases[canvasName];
|
||||||
|
|
||||||
if (canvasName !== "0" && canvas.parentElement) {
|
// Get logical dimensions from foreignObject parent (set by backend)
|
||||||
var newCanvas = window.document.createElement("canvas");
|
const foreignObject = placeholder.parentElement;
|
||||||
var context = newCanvas.getContext("2d");
|
if (!foreignObject) return;
|
||||||
|
const logicalWidth = parseFloat(foreignObject.getAttribute("width") || "0");
|
||||||
|
const logicalHeight = parseFloat(foreignObject.getAttribute("height") || "0");
|
||||||
|
|
||||||
|
// Clone canvas for repeated instances (layers that appear multiple times)
|
||||||
|
// Viewport canvas is marked with data-is-viewport and should never be cloned
|
||||||
|
const isViewport = placeholder.hasAttribute("data-is-viewport");
|
||||||
|
if (!isViewport && canvas.parentElement) {
|
||||||
|
const newCanvas = window.document.createElement("canvas");
|
||||||
|
const context = newCanvas.getContext("2d");
|
||||||
|
|
||||||
newCanvas.width = canvas.width;
|
newCanvas.width = canvas.width;
|
||||||
newCanvas.height = canvas.height;
|
newCanvas.height = canvas.height;
|
||||||
|
|
@ -215,6 +224,10 @@
|
||||||
canvas = newCanvas;
|
canvas = newCanvas;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set CSS size to logical resolution (for correct display size)
|
||||||
|
canvas.style.width = `${logicalWidth}px`;
|
||||||
|
canvas.style.height = `${logicalHeight}px`;
|
||||||
|
|
||||||
placeholder.replaceWith(canvas);
|
placeholder.replaceWith(canvas);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -393,8 +406,8 @@
|
||||||
rulerHorizontal?.resize();
|
rulerHorizontal?.resize();
|
||||||
rulerVertical?.resize();
|
rulerVertical?.resize();
|
||||||
|
|
||||||
// Send the new bounds of the viewports to the backend
|
// Note: Viewport bounds are now sent to the backend by the ResizeObserver in viewports.ts
|
||||||
if (viewport.parentElement) updateViewport(editor);
|
// which provides pixel-perfect physical dimensions via devicePixelContentBoxSize
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|
@ -473,14 +486,21 @@
|
||||||
displayRemoveEditableTextbox();
|
displayRemoveEditableTextbox();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Once this component is mounted, we want to resend the document bounds to the backend via the resize event handler which does that
|
// Setup ResizeObserver for pixel-perfect viewport tracking with physical dimensions
|
||||||
window.dispatchEvent(new Event("resize"));
|
// This must happen in onMount to ensure the viewport container element exists
|
||||||
|
setupViewportResizeObserver(editor);
|
||||||
|
|
||||||
|
// Also observe the inner viewport for canvas sizing and ruler updates
|
||||||
const viewportResizeObserver = new ResizeObserver(() => {
|
const viewportResizeObserver = new ResizeObserver(() => {
|
||||||
updateViewportInfo();
|
updateViewportInfo();
|
||||||
});
|
});
|
||||||
if (viewport) viewportResizeObserver.observe(viewport);
|
if (viewport) viewportResizeObserver.observe(viewport);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
// Cleanup the viewport resize observer
|
||||||
|
cleanupViewportResizeObserver();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LayoutCol class="document" on:dragover={(e) => e.preventDefault()} on:drop={dropFile}>
|
<LayoutCol class="document" on:dragover={(e) => e.preventDefault()} on:drop={dropFile}>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode }
|
||||||
import { operatingSystem } from "@graphite/utility-functions/platform";
|
import { operatingSystem } from "@graphite/utility-functions/platform";
|
||||||
import { extractPixelData } from "@graphite/utility-functions/rasterization";
|
import { extractPixelData } from "@graphite/utility-functions/rasterization";
|
||||||
import { stripIndents } from "@graphite/utility-functions/strip-indents";
|
import { stripIndents } from "@graphite/utility-functions/strip-indents";
|
||||||
import { updateBoundsOfViewports } from "@graphite/utility-functions/viewports";
|
|
||||||
|
|
||||||
const BUTTON_LEFT = 0;
|
const BUTTON_LEFT = 0;
|
||||||
const BUTTON_MIDDLE = 1;
|
const BUTTON_MIDDLE = 1;
|
||||||
|
|
@ -43,7 +42,6 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const listeners: { target: EventListenerTarget; eventName: EventName; action: (event: any) => void; options?: AddEventListenerOptions }[] = [
|
const listeners: { target: EventListenerTarget; eventName: EventName; action: (event: any) => void; options?: AddEventListenerOptions }[] = [
|
||||||
{ target: window, eventName: "resize", action: () => updateBoundsOfViewports(editor) },
|
|
||||||
{ target: window, eventName: "beforeunload", action: (e: BeforeUnloadEvent) => onBeforeUnload(e) },
|
{ target: window, eventName: "beforeunload", action: (e: BeforeUnloadEvent) => onBeforeUnload(e) },
|
||||||
{ target: window, eventName: "keyup", action: (e: KeyboardEvent) => onKeyUp(e) },
|
{ target: window, eventName: "keyup", action: (e: KeyboardEvent) => onKeyUp(e) },
|
||||||
{ target: window, eventName: "keydown", action: (e: KeyboardEvent) => onKeyDown(e) },
|
{ target: window, eventName: "keydown", action: (e: KeyboardEvent) => onKeyDown(e) },
|
||||||
|
|
@ -529,8 +527,6 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||||
|
|
||||||
// Bind the event listeners
|
// Bind the event listeners
|
||||||
bindListeners();
|
bindListeners();
|
||||||
// Resize on creation
|
|
||||||
updateBoundsOfViewports(editor);
|
|
||||||
|
|
||||||
// Return the destructor
|
// Return the destructor
|
||||||
return unbindListeners;
|
return unbindListeners;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,57 @@
|
||||||
import { type Editor } from "@graphite/editor";
|
import { type Editor } from "@graphite/editor";
|
||||||
|
|
||||||
export function updateBoundsOfViewports(editor: Editor) {
|
let resizeObserver: ResizeObserver | undefined;
|
||||||
const viewports = Array.from(window.document.querySelectorAll("[data-viewport-container]"));
|
|
||||||
|
|
||||||
|
export function setupViewportResizeObserver(editor: Editor) {
|
||||||
|
// Clean up existing observer if any
|
||||||
|
if (resizeObserver) {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewports = Array.from(window.document.querySelectorAll("[data-viewport-container]"));
|
||||||
if (viewports.length <= 0) return;
|
if (viewports.length <= 0) return;
|
||||||
|
|
||||||
const bounds = viewports[0].getBoundingClientRect();
|
const viewport = viewports[0] as HTMLElement;
|
||||||
const scale = window.devicePixelRatio || 1;
|
|
||||||
|
|
||||||
editor.handle.updateViewport(bounds.x, bounds.y, bounds.width, bounds.height, scale);
|
resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
const devicePixelRatio = window.devicePixelRatio || 1;
|
||||||
|
|
||||||
|
// Get exact device pixel dimensions from the browser
|
||||||
|
// Use devicePixelContentBoxSize for pixel-perfect rendering with fallback for Safari
|
||||||
|
let physicalWidth: number;
|
||||||
|
let physicalHeight: number;
|
||||||
|
|
||||||
|
if (entry.devicePixelContentBoxSize && entry.devicePixelContentBoxSize.length > 0) {
|
||||||
|
// Modern browsers (Chrome, Firefox): get exact device pixels from the browser
|
||||||
|
physicalWidth = entry.devicePixelContentBoxSize[0].inlineSize;
|
||||||
|
physicalHeight = entry.devicePixelContentBoxSize[0].blockSize;
|
||||||
|
} else {
|
||||||
|
// Fallback for Safari: calculate from contentBoxSize and devicePixelRatio
|
||||||
|
physicalWidth = entry.contentBoxSize[0].inlineSize * devicePixelRatio;
|
||||||
|
physicalHeight = entry.contentBoxSize[0].blockSize * devicePixelRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the logical size which corresponds to the physical size
|
||||||
|
const logicalWidth = physicalWidth / devicePixelRatio;
|
||||||
|
const logicalHeight = physicalHeight / devicePixelRatio;
|
||||||
|
|
||||||
|
// Get viewport position
|
||||||
|
const bounds = entry.target.getBoundingClientRect();
|
||||||
|
|
||||||
|
// TODO: Consider passing physical sizes as well to eliminate pixel inaccuracies since width and height could be rounded differently
|
||||||
|
const scale = physicalWidth / logicalWidth;
|
||||||
|
|
||||||
|
editor.handle.updateViewport(bounds.x, bounds.y, logicalWidth, logicalHeight, scale);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.observe(viewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cleanupViewportResizeObserver() {
|
||||||
|
if (resizeObserver) {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
resizeObserver = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@ use graph_craft::proto::{NodeConstructor, TypeErasedBox};
|
||||||
use graphene_std::Artboard;
|
use graphene_std::Artboard;
|
||||||
use graphene_std::Context;
|
use graphene_std::Context;
|
||||||
use graphene_std::Graphic;
|
use graphene_std::Graphic;
|
||||||
#[cfg(feature = "gpu")]
|
|
||||||
use graphene_std::any::DowncastBothNode;
|
|
||||||
use graphene_std::any::DynAnyNode;
|
use graphene_std::any::DynAnyNode;
|
||||||
use graphene_std::application_io::{ImageTexture, SurfaceFrame};
|
use graphene_std::application_io::{ImageTexture, SurfaceFrame};
|
||||||
use graphene_std::brush::brush_cache::BrushCache;
|
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 => 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 => graphene_std::text::TextAlign]),
|
||||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => RenderIntermediate]),
|
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
|
// CONVERT NODES
|
||||||
|
|
|
||||||
|
|
@ -26,21 +26,10 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc<WasmEdito
|
||||||
// .document_node;
|
// .document_node;
|
||||||
|
|
||||||
let render_node = DocumentNode {
|
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 {
|
implementation: DocumentNodeImplementation::Network(NodeNetwork {
|
||||||
exports: vec![NodeInput::node(NodeId(4), 0)],
|
exports: vec![NodeInput::node(NodeId(2), 0)],
|
||||||
nodes: [
|
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 {
|
DocumentNode {
|
||||||
call_argument: concrete!(Context),
|
call_argument: concrete!(Context),
|
||||||
inputs: vec![NodeInput::import(core_types::Type::Fn(Box::new(concrete!(Context)), Box::new(generic!(T))), 0)],
|
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
|
// Keep this in sync with the protonode in valid_input_types
|
||||||
DocumentNode {
|
DocumentNode {
|
||||||
call_argument: concrete!(Context),
|
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),
|
implementation: DocumentNodeImplementation::ProtoNode(graphene_std::render_node::render::IDENTIFIER),
|
||||||
context_features: graphene_std::ContextDependencies {
|
context_features: graphene_std::ContextDependencies {
|
||||||
extract: ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS,
|
extract: ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS,
|
||||||
|
|
@ -64,7 +53,7 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc<WasmEdito
|
||||||
},
|
},
|
||||||
DocumentNode {
|
DocumentNode {
|
||||||
call_argument: concrete!(graphene_std::application_io::RenderConfig),
|
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),
|
implementation: DocumentNodeImplementation::ProtoNode(graphene_std::render_node::create_context::IDENTIFIER),
|
||||||
context_features: graphene_std::ContextDependencies {
|
context_features: graphene_std::ContextDependencies {
|
||||||
extract: ContextFeatures::empty(),
|
extract: ContextFeatures::empty(),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use core_types::transform::Footprint;
|
use core_types::transform::Footprint;
|
||||||
use dyn_any::{DynAny, StaticType, StaticTypeSized};
|
use dyn_any::{DynAny, StaticType, StaticTypeSized};
|
||||||
use glam::{DAffine2, UVec2};
|
use glam::{DAffine2, DVec2, UVec2};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
@ -23,7 +23,8 @@ impl std::fmt::Display for SurfaceId {
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct SurfaceFrame {
|
pub struct SurfaceFrame {
|
||||||
pub surface_id: SurfaceId,
|
pub surface_id: SurfaceId,
|
||||||
pub resolution: UVec2,
|
/// Logical resolution in CSS pixels (used for foreignObject dimensions)
|
||||||
|
pub resolution: DVec2,
|
||||||
pub transform: DAffine2,
|
pub transform: DAffine2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,10 +102,11 @@ impl Size for ImageTexture {
|
||||||
|
|
||||||
impl<S: Size> From<SurfaceHandleFrame<S>> for SurfaceFrame {
|
impl<S: Size> From<SurfaceHandleFrame<S>> for SurfaceFrame {
|
||||||
fn from(x: SurfaceHandleFrame<S>) -> Self {
|
fn from(x: SurfaceHandleFrame<S>) -> Self {
|
||||||
|
let size = x.surface_handle.surface.size();
|
||||||
Self {
|
Self {
|
||||||
surface_id: x.surface_handle.window_id,
|
surface_id: x.surface_handle.window_id,
|
||||||
transform: x.transform,
|
transform: x.transform,
|
||||||
resolution: x.surface_handle.surface.size(),
|
resolution: size.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use core_types::{Color, Context, Ctx, ExtractFootprint, OwnedContextImpl, WasmNo
|
||||||
use graph_craft::document::value::RenderOutput;
|
use graph_craft::document::value::RenderOutput;
|
||||||
pub use graph_craft::document::value::RenderOutputType;
|
pub use graph_craft::document::value::RenderOutputType;
|
||||||
pub use graph_craft::wasm_application_io::*;
|
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::Artboard;
|
||||||
use graphic_types::Graphic;
|
use graphic_types::Graphic;
|
||||||
use graphic_types::Vector;
|
use graphic_types::Vector;
|
||||||
|
|
@ -120,12 +120,7 @@ async fn create_context<'a: 'n>(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category(""))]
|
#[node_macro::node(category(""))]
|
||||||
async fn render<'a: 'n>(
|
async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, editor_api: &'a WasmEditorApi, data: RenderIntermediate) -> RenderOutput {
|
||||||
ctx: impl Ctx + ExtractFootprint + ExtractVarArgs,
|
|
||||||
editor_api: &'a WasmEditorApi,
|
|
||||||
data: RenderIntermediate,
|
|
||||||
_surface_handle: impl Node<Context<'static>, Output = Option<wgpu_executor::WgpuSurface>>,
|
|
||||||
) -> RenderOutput {
|
|
||||||
let footprint = ctx.footprint();
|
let footprint = ctx.footprint();
|
||||||
let render_params = ctx
|
let render_params = ctx
|
||||||
.vararg(0)
|
.vararg(0)
|
||||||
|
|
@ -136,6 +131,10 @@ async fn render<'a: 'n>(
|
||||||
render_params.footprint = *footprint;
|
render_params.footprint = *footprint;
|
||||||
let render_params = &render_params;
|
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;
|
let RenderIntermediate { ty, mut metadata, contains_artboard } = data;
|
||||||
metadata.apply_transform(footprint.transform);
|
metadata.apply_transform(footprint.transform);
|
||||||
|
|
||||||
|
|
@ -146,8 +145,8 @@ async fn render<'a: 'n>(
|
||||||
rendering.leaf_tag("rect", |attributes| {
|
rendering.leaf_tag("rect", |attributes| {
|
||||||
attributes.push("x", "0");
|
attributes.push("x", "0");
|
||||||
attributes.push("y", "0");
|
attributes.push("y", "0");
|
||||||
attributes.push("width", footprint.resolution.x.to_string());
|
attributes.push("width", logical_resolution.x.to_string());
|
||||||
attributes.push("height", footprint.resolution.y.to_string());
|
attributes.push("height", logical_resolution.y.to_string());
|
||||||
let matrix = format_transform_matrix(footprint.transform.inverse());
|
let matrix = format_transform_matrix(footprint.transform.inverse());
|
||||||
if !matrix.is_empty() {
|
if !matrix.is_empty() {
|
||||||
attributes.push("transform", matrix);
|
attributes.push("transform", matrix);
|
||||||
|
|
@ -159,7 +158,7 @@ async fn render<'a: 'n>(
|
||||||
rendering.image_data = svg_data.1.clone();
|
rendering.image_data = svg_data.1.clone();
|
||||||
rendering.svg_defs = svg_data.2.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 {
|
RenderOutputType::Svg {
|
||||||
svg: rendering.svg.to_svg_string(),
|
svg: rendering.svg.to_svg_string(),
|
||||||
image_data: rendering.image_data,
|
image_data: rendering.image_data,
|
||||||
|
|
@ -171,15 +170,6 @@ async fn render<'a: 'n>(
|
||||||
};
|
};
|
||||||
let (child, context) = Arc::as_ref(vello_data);
|
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 scale_transform = glam::DAffine2::from_scale(glam::DVec2::splat(scale));
|
||||||
let footprint_transform = scale_transform * footprint.transform;
|
let footprint_transform = scale_transform * footprint.transform;
|
||||||
let footprint_transform_vello = vello::kurbo::Affine::new(footprint_transform.to_cols_array());
|
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();
|
let mut scene = vello::Scene::new();
|
||||||
scene.append(child, Some(footprint_transform_vello));
|
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
|
// 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
|
// 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();
|
let encoding = scene.encoding_mut();
|
||||||
for transform in encoding.transforms.iter_mut() {
|
for transform in encoding.transforms.iter_mut() {
|
||||||
if transform.matrix[0] == f32::INFINITY {
|
if transform.matrix[0] == f32::INFINITY {
|
||||||
|
|
@ -204,25 +192,12 @@ async fn render<'a: 'n>(
|
||||||
background = Color::WHITE;
|
background = Color::WHITE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(surface_handle) = surface_handle {
|
let texture = exec
|
||||||
exec.render_vello_scene(&scene, &surface_handle, resolution, context, background)
|
.render_vello_scene_to_texture(&scene, physical_resolution, context, background)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to render Vello scene");
|
.expect("Failed to render Vello scene");
|
||||||
|
|
||||||
let frame = SurfaceFrame {
|
RenderOutputType::Texture(ImageTexture { texture })
|
||||||
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 })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => unreachable!("Render node did not receive its requested data type"),
|
_ => unreachable!("Render node did not receive its requested data type"),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ pub mod texture_conversion;
|
||||||
|
|
||||||
use crate::shader_runtime::ShaderRuntime;
|
use crate::shader_runtime::ShaderRuntime;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use core_types::{Color, Ctx};
|
use core_types::Color;
|
||||||
use dyn_any::StaticType;
|
use dyn_any::StaticType;
|
||||||
use futures::lock::Mutex;
|
use futures::lock::Mutex;
|
||||||
use glam::UVec2;
|
use glam::UVec2;
|
||||||
|
|
@ -13,7 +13,7 @@ pub use rendering::RenderContext;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
|
use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
|
||||||
use wgpu::util::TextureBlitter;
|
use wgpu::util::TextureBlitter;
|
||||||
use wgpu::{Origin3d, SurfaceConfiguration, TextureAspect};
|
use wgpu::{Origin3d, TextureAspect};
|
||||||
|
|
||||||
pub use context::Context as WgpuContext;
|
pub use context::Context as WgpuContext;
|
||||||
pub use context::ContextBuilder as WgpuContextBuilder;
|
pub use context::ContextBuilder as WgpuContextBuilder;
|
||||||
|
|
@ -66,41 +66,6 @@ unsafe impl StaticType for Surface {
|
||||||
const VELLO_SURFACE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
const VELLO_SURFACE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||||
|
|
||||||
impl WgpuExecutor {
|
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> {
|
pub async fn render_vello_scene_to_texture(&self, scene: &Scene, size: UVec2, context: &RenderContext, background: Color) -> Result<wgpu::Texture> {
|
||||||
let mut output = None;
|
let mut output = None;
|
||||||
self.render_vello_scene_to_target_texture(scene, size, context, background, &mut output).await?;
|
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>>;
|
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()?))
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue