Reimplement checkered background rendering (#4034)
* Reimplement background checkerboard rendering
This commit is contained in:
parent
7cd5531730
commit
29f6e686ee
|
|
@ -1,8 +1,7 @@
|
|||
use std::borrow::Cow;
|
||||
use wgpu::PresentMode;
|
||||
|
||||
use crate::window::Window;
|
||||
use crate::wrapper::{TargetTexture, WgpuContext, WgpuExecutor};
|
||||
use crate::wrapper::{WgpuContext, WgpuExecutor};
|
||||
|
||||
#[derive(derivative::Derivative)]
|
||||
#[derivative(Debug)]
|
||||
|
|
@ -19,7 +18,7 @@ pub(crate) struct RenderState {
|
|||
viewport_scale: [f32; 2],
|
||||
viewport_offset: [f32; 2],
|
||||
viewport_texture: Option<std::sync::Arc<wgpu::Texture>>,
|
||||
overlays_texture: Option<TargetTexture>,
|
||||
overlays_texture: Option<std::sync::Arc<wgpu::Texture>>,
|
||||
ui_texture: Option<wgpu::Texture>,
|
||||
bind_group: Option<wgpu::BindGroup>,
|
||||
#[derivative(Debug = "ignore")]
|
||||
|
|
@ -233,11 +232,17 @@ impl RenderState {
|
|||
return;
|
||||
};
|
||||
let size = glam::UVec2::new(viewport_texture.width(), viewport_texture.height());
|
||||
let result = futures::executor::block_on(self.executor.render_vello_scene_to_target_texture(&scene, size, &Default::default(), &mut self.overlays_texture));
|
||||
if let Err(e) = result {
|
||||
tracing::error!("Error rendering overlays: {:?}", e);
|
||||
return;
|
||||
let result = futures::executor::block_on(self.executor.render_vello_scene(&scene, size, &Default::default(), None));
|
||||
match result {
|
||||
Ok(texture) => {
|
||||
self.overlays_texture = Some(texture);
|
||||
}
|
||||
Err(e) => {
|
||||
self.overlays_texture = None;
|
||||
tracing::error!("Error rendering overlays: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
self.update_bindgroup();
|
||||
}
|
||||
|
||||
|
|
@ -314,11 +319,7 @@ impl RenderState {
|
|||
fn update_bindgroup(&mut self) {
|
||||
self.surface_outdated = true;
|
||||
let viewport_texture_view = self.viewport_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let overlays_texture_view = self
|
||||
.overlays_texture
|
||||
.as_ref()
|
||||
.map(|target| Cow::Borrowed(target.view()))
|
||||
.unwrap_or_else(|| Cow::Owned(self.transparent_texture.create_view(&wgpu::TextureViewDescriptor::default())));
|
||||
let overlays_texture_view = self.overlays_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let ui_texture_view = self.ui_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let bind_group = self.context.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
|
|
@ -330,7 +331,7 @@ impl RenderState {
|
|||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::TextureView(overlays_texture_view.as_ref()),
|
||||
resource: wgpu::BindingResource::TextureView(&overlays_texture_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ use message_dispatcher::DesktopWrapperMessageDispatcher;
|
|||
use messages::{DesktopFrontendMessage, DesktopWrapperMessage};
|
||||
|
||||
pub use graphite_editor::consts::{DOUBLE_CLICK_MILLISECONDS, FILE_EXTENSION};
|
||||
pub use wgpu_executor::TargetTexture;
|
||||
pub use wgpu_executor::WgpuContext;
|
||||
pub use wgpu_executor::WgpuContextBuilder;
|
||||
pub use wgpu_executor::WgpuExecutor;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ use crate::messages::portfolio::document::utility_types::network_interface::Node
|
|||
use crate::messages::prelude::*;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::brush::brush_stroke::BrushStroke;
|
||||
use graphene_std::color::Color;
|
||||
use graphene_std::raster::BlendMode;
|
||||
use graphene_std::raster_types::Image;
|
||||
use graphene_std::subpath::Subpath;
|
||||
|
|
|
|||
|
|
@ -417,6 +417,7 @@ impl NodeGraphExecutor {
|
|||
click_targets,
|
||||
clip_targets,
|
||||
vector_data,
|
||||
backgrounds: _,
|
||||
} = render_output.metadata;
|
||||
|
||||
// Run these update state messages immediately
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc<PlatformE
|
|||
let render_node = DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
||||
implementation: DocumentNodeImplementation::Network(NodeNetwork {
|
||||
exports: vec![NodeInput::node(NodeId(4), 0)],
|
||||
exports: vec![NodeInput::node(NodeId(5), 0)],
|
||||
nodes: [
|
||||
DocumentNode {
|
||||
call_argument: concrete!(Context),
|
||||
|
|
@ -40,7 +40,6 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc<PlatformE
|
|||
},
|
||||
..Default::default()
|
||||
},
|
||||
// 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(0), 0)],
|
||||
|
|
@ -71,9 +70,19 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc<PlatformE
|
|||
},
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
call_argument: concrete!(Context),
|
||||
inputs: vec![NodeInput::scope("editor-api"), NodeInput::node(NodeId(3), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(graphene_std::render_node::render_background::IDENTIFIER),
|
||||
context_features: graphene_std::ContextDependencies {
|
||||
extract: ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS,
|
||||
inject: ContextFeatures::empty(),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
call_argument: concrete!(graphene_std::application_io::RenderConfig),
|
||||
inputs: vec![NodeInput::node(NodeId(3), 0)],
|
||||
inputs: vec![NodeInput::node(NodeId(4), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(graphene_std::render_node::create_context::IDENTIFIER),
|
||||
context_features: graphene_std::ContextDependencies {
|
||||
// We add the extract index annotation here to force the compiler to add a context nullification node before this node so the render context is properly nullified so the render cache node can do its's work
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ impl Default for Footprint {
|
|||
impl Footprint {
|
||||
pub const DEFAULT: Self = Self {
|
||||
transform: DAffine2::IDENTITY,
|
||||
resolution: UVec2::new(1920, 1080),
|
||||
resolution: UVec2::ONE,
|
||||
quality: RenderQuality::Full,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ use crate::render_ext::RenderExt;
|
|||
use crate::to_peniko::BlendModeExt;
|
||||
use core_types::CacheHash;
|
||||
use core_types::blending::BlendMode;
|
||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||
use core_types::color::{Alpha, Color};
|
||||
use core_types::bounds::BoundingBox;
|
||||
use core_types::bounds::RenderBoundingBox;
|
||||
use core_types::color::Color;
|
||||
use core_types::math::quad::Quad;
|
||||
use core_types::render_complexity::RenderComplexity;
|
||||
use core_types::table::{Table, TableRow};
|
||||
|
|
@ -32,46 +33,6 @@ use std::sync::{Arc, LazyLock};
|
|||
use vector_types::gradient::GradientSpreadMethod;
|
||||
use vello::*;
|
||||
|
||||
/// Cached 16x16 transparency checkerboard image data (two 8x8 cells of #ffffff and #cccccc).
|
||||
static CHECKERBOARD_IMAGE_DATA: LazyLock<Arc<Vec<u8>>> = LazyLock::new(|| {
|
||||
const SIZE: u32 = 16;
|
||||
const HALF: u32 = 8;
|
||||
|
||||
let mut data = vec![0_u8; (SIZE * SIZE * 4) as usize];
|
||||
for y in 0..SIZE {
|
||||
for x in 0..SIZE {
|
||||
let is_light = ((x / HALF) + (y / HALF)).is_multiple_of(2);
|
||||
let value = if is_light { 0xff } else { 0xcc };
|
||||
let index = ((y * SIZE + x) * 4) as usize;
|
||||
data[index] = value;
|
||||
data[index + 1] = value;
|
||||
data[index + 2] = value;
|
||||
data[index + 3] = 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
Arc::new(data)
|
||||
});
|
||||
|
||||
/// Creates a 16x16 tiling transparency checkerboard brush for Vello.
|
||||
pub fn checkerboard_brush() -> peniko::Brush {
|
||||
peniko::Brush::Image(peniko::ImageBrush {
|
||||
image: peniko::ImageData {
|
||||
data: peniko::Blob::new(CHECKERBOARD_IMAGE_DATA.clone()),
|
||||
format: peniko::ImageFormat::Rgba8,
|
||||
width: 16,
|
||||
height: 16,
|
||||
alpha_type: peniko::ImageAlphaType::Alpha,
|
||||
},
|
||||
sampler: peniko::ImageSampler {
|
||||
x_extend: peniko::Extend::Repeat,
|
||||
y_extend: peniko::Extend::Repeat,
|
||||
quality: peniko::ImageQuality::Low, // Nearest-neighbor sampling for crisp edges
|
||||
alpha: 1.,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
enum MaskType {
|
||||
|
|
@ -125,15 +86,17 @@ impl SvgRender {
|
|||
pub fn format_svg(&mut self, bounds_min: DVec2, bounds_max: DVec2) {
|
||||
let (x, y) = bounds_min.into();
|
||||
let (size_x, size_y) = (bounds_max - bounds_min).into();
|
||||
let defs = &self.svg_defs;
|
||||
let svg_header = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:graphite="https://graphite.art" viewBox="{x} {y} {size_x} {size_y}"><defs>{defs}</defs>"#,);
|
||||
let svg_header = format!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:graphite="https://graphite.art" viewBox="{x} {y} {size_x} {size_y}"><defs>{defs}</defs>"#,
|
||||
defs = &self.svg_defs
|
||||
);
|
||||
self.svg_defs = String::new();
|
||||
self.svg.insert(0, svg_header.into());
|
||||
self.svg.push("</svg>".into());
|
||||
}
|
||||
|
||||
/// Wraps the SVG with `<svg><g transform="...">...</g></svg>`, which allows for rotation
|
||||
pub fn wrap_with_transform(&mut self, transform: DAffine2, size: Option<DVec2>) {
|
||||
let defs = &self.svg_defs;
|
||||
let view_box = size
|
||||
.map(|size| format!("viewBox=\"0 0 {} {}\" width=\"{}\" height=\"{}\"", size.x, size.y, size.x, size.y))
|
||||
.unwrap_or_default();
|
||||
|
|
@ -141,7 +104,11 @@ impl SvgRender {
|
|||
let matrix = format_transform_matrix(transform);
|
||||
let transform = if matrix.is_empty() { String::new() } else { format!(r#" transform="{matrix}""#) };
|
||||
|
||||
let svg_header = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:graphite="https://graphite.art" {view_box}><defs>{defs}</defs><g{transform}>"#);
|
||||
let svg_header = format!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:graphite="https://graphite.art" {view_box}><defs>{defs}</defs><g{transform}>"#,
|
||||
defs = &self.svg_defs
|
||||
);
|
||||
self.svg_defs = String::new();
|
||||
self.svg.insert(0, svg_header.into());
|
||||
self.svg.push("</g></svg>".into());
|
||||
}
|
||||
|
|
@ -186,6 +153,34 @@ impl SvgRender {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct SvgRenderOutput {
|
||||
pub svg: String,
|
||||
pub svg_defs: String,
|
||||
pub image_data: HashMap<CacheHashWrapper<Image<Color>>, u64>,
|
||||
}
|
||||
|
||||
impl From<&SvgRenderOutput> for SvgRender {
|
||||
fn from(value: &SvgRenderOutput) -> Self {
|
||||
Self {
|
||||
svg: vec![value.svg.clone().into()],
|
||||
svg_defs: value.svg_defs.clone(),
|
||||
transform: DAffine2::IDENTITY,
|
||||
image_data: value.image_data.clone(),
|
||||
indent: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SvgRender> for SvgRenderOutput {
|
||||
fn from(val: SvgRender) -> Self {
|
||||
Self {
|
||||
svg: val.svg.to_svg_string(),
|
||||
svg_defs: val.svg_defs,
|
||||
image_data: val.image_data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SvgRender {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
|
|
@ -215,8 +210,6 @@ pub struct RenderParams {
|
|||
pub scale: f64,
|
||||
pub render_output_type: RenderOutputType,
|
||||
pub thumbnail: bool,
|
||||
/// Don't render the rectangle for an artboard to allow exporting with a transparent background.
|
||||
pub hide_artboards: bool,
|
||||
/// Are we exporting
|
||||
pub for_export: bool,
|
||||
/// Are we generating a mask in this render pass? Used to see if fill should be multiplied with alpha.
|
||||
|
|
@ -334,6 +327,7 @@ pub struct RenderMetadata {
|
|||
pub click_targets: HashMap<NodeId, Vec<Arc<ClickTarget>>>,
|
||||
pub clip_targets: HashSet<NodeId>,
|
||||
pub vector_data: HashMap<NodeId, Arc<Vector>>,
|
||||
pub backgrounds: Vec<Background>,
|
||||
}
|
||||
|
||||
impl RenderMetadata {
|
||||
|
|
@ -354,6 +348,7 @@ impl RenderMetadata {
|
|||
click_targets,
|
||||
clip_targets,
|
||||
vector_data,
|
||||
backgrounds,
|
||||
} = self;
|
||||
upstream_footprints.extend(other.upstream_footprints.iter());
|
||||
local_transforms.extend(other.local_transforms.iter());
|
||||
|
|
@ -361,9 +356,22 @@ impl RenderMetadata {
|
|||
click_targets.extend(other.click_targets.iter().map(|(k, v)| (*k, v.clone())));
|
||||
clip_targets.extend(other.clip_targets.iter());
|
||||
vector_data.extend(other.vector_data.iter().map(|(id, data)| (*id, data.clone())));
|
||||
|
||||
// TODO: Find a better non O(n^2) way to merge backgrounds
|
||||
for background in &other.backgrounds {
|
||||
if !backgrounds.contains(background) {
|
||||
backgrounds.push(background.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Background {
|
||||
pub location: DVec2,
|
||||
pub dimensions: DVec2,
|
||||
}
|
||||
|
||||
// TODO: Rename to "Graphical"
|
||||
pub trait Render: BoundingBox + RenderComplexity {
|
||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams);
|
||||
|
|
@ -526,42 +534,17 @@ impl Render for Table<Table<Graphic>> {
|
|||
let width = dimensions.x.abs();
|
||||
let height = dimensions.y.abs();
|
||||
|
||||
// Rectangle for the artboard
|
||||
if !render_params.hide_artboards {
|
||||
// Transparency checkerboard behind the artboard background (viewport only)
|
||||
let show_checkerboard = background.alpha() < 1. && render_params.to_canvas();
|
||||
if show_checkerboard && render_params.viewport_zoom > 0. {
|
||||
let checker_id = format!("checkered-artboard-{}", generate_uuid());
|
||||
let cell_size = 8. / render_params.viewport_zoom;
|
||||
let pattern_size = cell_size * 2.;
|
||||
|
||||
// Anchor pattern at this artboard's top-left corner (x, y), not the document origin
|
||||
let _ = write!(
|
||||
&mut render.svg_defs,
|
||||
r##"<pattern id="{checker_id}" x="{x}" y="{y}" width="{pattern_size}" height="{pattern_size}" patternUnits="userSpaceOnUse"><rect width="{pattern_size}" height="{pattern_size}" fill="#fff" /><rect x="{cell_size}" y="0" width="{cell_size}" height="{cell_size}" fill="#ccc" /><rect x="0" y="{cell_size}" width="{cell_size}" height="{cell_size}" fill="#ccc" /></pattern>"##
|
||||
);
|
||||
|
||||
render.leaf_tag("rect", |attributes| {
|
||||
attributes.push("x", x.to_string());
|
||||
attributes.push("y", y.to_string());
|
||||
attributes.push("width", width.to_string());
|
||||
attributes.push("height", height.to_string());
|
||||
attributes.push("fill", format!("url(#{checker_id})"));
|
||||
});
|
||||
// Background
|
||||
render.leaf_tag("rect", |attributes| {
|
||||
attributes.push("fill", format!("#{}", background.to_rgb_hex_srgb_from_gamma()));
|
||||
if background.a() < 1. {
|
||||
attributes.push("fill-opacity", ((background.a() * 1000.).round() / 1000.).to_string());
|
||||
}
|
||||
|
||||
// Background
|
||||
render.leaf_tag("rect", |attributes| {
|
||||
attributes.push("fill", format!("#{}", background.to_rgb_hex_srgb_from_gamma()));
|
||||
if background.a() < 1. {
|
||||
attributes.push("fill-opacity", ((background.a() * 1000.).round() / 1000.).to_string());
|
||||
}
|
||||
attributes.push("x", x.to_string());
|
||||
attributes.push("y", y.to_string());
|
||||
attributes.push("width", width.to_string());
|
||||
attributes.push("height", height.to_string());
|
||||
});
|
||||
}
|
||||
attributes.push("x", x.to_string());
|
||||
attributes.push("y", y.to_string());
|
||||
attributes.push("width", width.to_string());
|
||||
attributes.push("height", height.to_string());
|
||||
});
|
||||
|
||||
// Artwork
|
||||
render.parent_tag(
|
||||
|
|
@ -607,26 +590,12 @@ impl Render for Table<Table<Graphic>> {
|
|||
let [a, b] = [location, location + dimensions];
|
||||
let rect = kurbo::Rect::new(a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y));
|
||||
|
||||
// Render background
|
||||
if !render_params.hide_artboards {
|
||||
let artboard_transform = kurbo::Affine::new(transform.to_cols_array());
|
||||
let artboard_transform = kurbo::Affine::new(transform.to_cols_array());
|
||||
|
||||
// Transparency checkerboard behind the artboard background (viewport only)
|
||||
let show_checkerboard = background.alpha() < 1. && render_params.to_canvas();
|
||||
if show_checkerboard && render_params.viewport_zoom > 0. {
|
||||
// Anchor pattern at THIS artboard's top-left corner
|
||||
// brush_transform is an image placement transform: it maps brush pixel coords → shape coords
|
||||
// scale(1/zoom) sets each brush pixel to 1/zoom document units (constant CSS size after viewport transform)
|
||||
// then_translate places the brush origin at the artboard corner
|
||||
let brush_transform = kurbo::Affine::scale(1. / render_params.viewport_zoom).then_translate(kurbo::Vec2::new(rect.x0, rect.y0));
|
||||
scene.fill(peniko::Fill::NonZero, artboard_transform, &checkerboard_brush(), Some(brush_transform), &rect);
|
||||
}
|
||||
|
||||
let color = peniko::Color::new([background.r(), background.g(), background.b(), background.a()]);
|
||||
scene.push_layer(peniko::Fill::NonZero, peniko::Mix::Normal, 1., artboard_transform, &rect);
|
||||
scene.fill(peniko::Fill::NonZero, artboard_transform, color, None, &rect);
|
||||
scene.pop_layer();
|
||||
}
|
||||
let color = peniko::Color::new([background.r(), background.g(), background.b(), background.a()]);
|
||||
scene.push_layer(peniko::Fill::NonZero, peniko::Mix::Normal, 1., artboard_transform, &rect);
|
||||
scene.fill(peniko::Fill::NonZero, artboard_transform, color, None, &rect);
|
||||
scene.pop_layer();
|
||||
|
||||
if clip {
|
||||
scene.push_clip_layer(peniko::Fill::NonZero, kurbo::Affine::new(transform.to_cols_array()), &rect);
|
||||
|
|
@ -661,6 +630,8 @@ impl Render for Table<Table<Graphic>> {
|
|||
}
|
||||
}
|
||||
|
||||
metadata.backgrounds.push(Background { location, dimensions });
|
||||
|
||||
let mut child_footprint = footprint;
|
||||
child_footprint.transform *= DAffine2::from_translation(location);
|
||||
content.collect_metadata(metadata, child_footprint, None);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
struct CompositeUniforms {
|
||||
transform_x: vec2<f32>,
|
||||
transform_y: vec2<f32>,
|
||||
transform_translation: vec2<f32>,
|
||||
rect_min: vec2<f32>,
|
||||
rect_max: vec2<f32>,
|
||||
viewport_size: vec2<f32>,
|
||||
pattern_origin: vec2<f32>,
|
||||
checker_size: f32,
|
||||
_pad: f32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0)
|
||||
var<uniform> uniforms: CompositeUniforms;
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) document_position: vec2<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
||||
let document_corners = array<vec2<f32>, 6>(
|
||||
uniforms.rect_min,
|
||||
vec2<f32>(uniforms.rect_max.x, uniforms.rect_min.y),
|
||||
vec2<f32>(uniforms.rect_min.x, uniforms.rect_max.y),
|
||||
vec2<f32>(uniforms.rect_min.x, uniforms.rect_max.y),
|
||||
vec2<f32>(uniforms.rect_max.x, uniforms.rect_min.y),
|
||||
uniforms.rect_max,
|
||||
);
|
||||
let document_position = document_corners[vertex_index];
|
||||
|
||||
let transformed = uniforms.transform_x * document_position.x + uniforms.transform_y * document_position.y + uniforms.transform_translation;
|
||||
let normalized = transformed / uniforms.viewport_size;
|
||||
let clip = vec2<f32>(normalized.x * 2.0 - 1.0, 1.0 - normalized.y * 2.0);
|
||||
|
||||
var out: VertexOutput;
|
||||
out.position = vec4<f32>(clip, 0.0, 1.0);
|
||||
out.document_position = document_position;
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let tile = floor((in.document_position - uniforms.pattern_origin) / uniforms.checker_size);
|
||||
let parity = i32(tile.x + tile.y) & 1;
|
||||
let luminance = vec3<f32>(select(1.0, 0.8, parity == 1));
|
||||
return vec4<f32>(luminance, 1.0);
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
struct CompositeUniforms {
|
||||
transform_x: vec2<f32>,
|
||||
transform_y: vec2<f32>,
|
||||
transform_translation: vec2<f32>,
|
||||
rect_min: vec2<f32>,
|
||||
rect_max: vec2<f32>,
|
||||
viewport_size: vec2<f32>,
|
||||
pattern_origin: vec2<f32>,
|
||||
checker_size: f32,
|
||||
_pad: f32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0)
|
||||
var<uniform> uniforms: CompositeUniforms;
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) document_position: vec2<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
||||
let positions = array<vec2<f32>, 3>(
|
||||
vec2<f32>(-1.0, -1.0),
|
||||
vec2<f32>(-1.0, 3.0),
|
||||
vec2<f32>( 3.0, -1.0),
|
||||
);
|
||||
let position = positions[vertex_index];
|
||||
|
||||
let screen_position = vec2<f32>((position.x + 1.0) * 0.5 * uniforms.viewport_size.x, (1.0 - position.y) * 0.5 * uniforms.viewport_size.y);
|
||||
let document_position = uniforms.transform_x * screen_position.x + uniforms.transform_y * screen_position.y + uniforms.transform_translation;
|
||||
|
||||
var out: VertexOutput;
|
||||
out.position = vec4<f32>(position, 0.0, 1.0);
|
||||
out.document_position = document_position;
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let tile = floor((in.document_position - uniforms.pattern_origin) / uniforms.checker_size);
|
||||
let parity = i32(tile.x + tile.y) & 1;
|
||||
let luminance = vec3<f32>(select(1.0, 0.8, parity == 1));
|
||||
return vec4<f32>(luminance, 1.0);
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
@group(0) @binding(0)
|
||||
var foreground_sampler: sampler;
|
||||
|
||||
@group(0) @binding(1)
|
||||
var foreground_texture: texture_2d<f32>;
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) tex_coord: vec2<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
||||
let positions = array<vec2<f32>, 3>(
|
||||
vec2<f32>(-1.0, -1.0),
|
||||
vec2<f32>(-1.0, 3.0),
|
||||
vec2<f32>( 3.0, -1.0),
|
||||
);
|
||||
|
||||
let tex_coords = array<vec2<f32>, 3>(
|
||||
vec2<f32>(0.0, 1.0),
|
||||
vec2<f32>(0.0, -1.0),
|
||||
vec2<f32>(2.0, 1.0),
|
||||
);
|
||||
|
||||
var vertex_out: VertexOutput;
|
||||
vertex_out.position = vec4<f32>(positions[vertex_index], 0.0, 1.0);
|
||||
vertex_out.tex_coord = tex_coords[vertex_index];
|
||||
return vertex_out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(fragment_in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return textureSample(foreground_texture, foreground_sampler, fragment_in.tex_coord);
|
||||
}
|
||||
|
|
@ -0,0 +1,344 @@
|
|||
use glam::{Affine2, Vec2};
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
pub struct BackgroundCompositor {
|
||||
checker_rect_pipeline: wgpu::RenderPipeline,
|
||||
checker_viewport_pipeline: wgpu::RenderPipeline,
|
||||
fullscreen_pipeline: wgpu::RenderPipeline,
|
||||
checker_bind_group_layout: wgpu::BindGroupLayout,
|
||||
fullscreen_bind_group_layout: wgpu::BindGroupLayout,
|
||||
sampler: wgpu::Sampler,
|
||||
}
|
||||
|
||||
impl BackgroundCompositor {
|
||||
pub fn new(device: &wgpu::Device) -> Self {
|
||||
let format = wgpu::TextureFormat::Rgba8Unorm;
|
||||
let checker_rect_shader = device.create_shader_module(wgpu::include_wgsl!("checker_rect.wgsl"));
|
||||
let checker_viewport_shader = device.create_shader_module(wgpu::include_wgsl!("checker_viewport.wgsl"));
|
||||
let fullscreen_shader = device.create_shader_module(wgpu::include_wgsl!("fullscreen.wgsl"));
|
||||
|
||||
let checker_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("background_checker_bind_group_layout"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
});
|
||||
|
||||
let checker_rect_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("background_checker_rect_pipeline_layout"),
|
||||
bind_group_layouts: &[&checker_bind_group_layout],
|
||||
immediate_size: 0,
|
||||
});
|
||||
|
||||
let checker_viewport_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("background_checker_viewport_pipeline_layout"),
|
||||
bind_group_layouts: &[&checker_bind_group_layout],
|
||||
immediate_size: 0,
|
||||
});
|
||||
|
||||
let fullscreen_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("background_fullscreen_bind_group_layout"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
multisampled: false,
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let fullscreen_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("background_fullscreen_pipeline_layout"),
|
||||
bind_group_layouts: &[&fullscreen_bind_group_layout],
|
||||
immediate_size: 0,
|
||||
});
|
||||
|
||||
let checker_rect_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("background_checker_rect_pipeline"),
|
||||
layout: Some(&checker_rect_pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &checker_rect_shader,
|
||||
entry_point: Some("vs_main"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[],
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &checker_rect_shader,
|
||||
entry_point: Some("fs_main"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
let checker_viewport_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("background_checker_viewport_pipeline"),
|
||||
layout: Some(&checker_viewport_pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &checker_viewport_shader,
|
||||
entry_point: Some("vs_main"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[],
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &checker_viewport_shader,
|
||||
entry_point: Some("fs_main"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
let fullscreen_blend = wgpu::BlendState {
|
||||
color: wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::SrcAlpha,
|
||||
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
|
||||
operation: wgpu::BlendOperation::Add,
|
||||
},
|
||||
alpha: wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::One,
|
||||
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
|
||||
operation: wgpu::BlendOperation::Add,
|
||||
},
|
||||
};
|
||||
|
||||
let fullscreen_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("background_fullscreen_pipeline"),
|
||||
layout: Some(&fullscreen_pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &fullscreen_shader,
|
||||
entry_point: Some("vs_main"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[],
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &fullscreen_shader,
|
||||
entry_point: Some("fs_main"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: Some(fullscreen_blend),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("background_fullscreen_sampler"),
|
||||
mag_filter: wgpu::FilterMode::Linear,
|
||||
min_filter: wgpu::FilterMode::Linear,
|
||||
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
Self {
|
||||
checker_rect_pipeline,
|
||||
checker_viewport_pipeline,
|
||||
fullscreen_pipeline,
|
||||
checker_bind_group_layout,
|
||||
fullscreen_bind_group_layout,
|
||||
sampler,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn composite(&self, context: &crate::WgpuContext, foreground: &wgpu::Texture, output: &wgpu::Texture, backgrounds: &[rendering::Background], document_to_screen: Affine2, zoom: f32) {
|
||||
if zoom <= 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let device = &context.device;
|
||||
let queue = &context.queue;
|
||||
|
||||
let checker_size_doc = 8.0 / zoom;
|
||||
let screen_to_document = document_to_screen.inverse();
|
||||
let viewport_size = output.size();
|
||||
let viewport_size = Vec2::new(viewport_size.width as f32, viewport_size.height as f32);
|
||||
|
||||
let output_view = output.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let foreground_view = foreground.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let checker_draws = if backgrounds.is_empty() {
|
||||
vec![(
|
||||
3,
|
||||
self.create_checker_bind_group(device, CompositeUniforms::fullscreen(viewport_size, screen_to_document, checker_size_doc)),
|
||||
)]
|
||||
} else {
|
||||
backgrounds
|
||||
.iter()
|
||||
.filter_map(|background| {
|
||||
let a = background.location.as_vec2();
|
||||
let b = (background.location + background.dimensions).as_vec2();
|
||||
|
||||
let min = a.min(b);
|
||||
let max = a.max(b);
|
||||
|
||||
if max.x <= min.x || max.y <= min.y {
|
||||
return None;
|
||||
}
|
||||
|
||||
let uniforms = CompositeUniforms::rect(min, max, document_to_screen, viewport_size, checker_size_doc);
|
||||
Some((6, self.create_checker_bind_group(device, uniforms)))
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
let fullscreen_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("background_fullscreen_bind_group"),
|
||||
layout: &self.fullscreen_bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::Sampler(&self.sampler),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::TextureView(&foreground_view),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("background_encoder") });
|
||||
|
||||
{
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("background_pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &output_view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
|
||||
if backgrounds.is_empty() {
|
||||
pass.set_pipeline(&self.checker_viewport_pipeline);
|
||||
for (vertex_count, bind_group) in &checker_draws {
|
||||
pass.set_bind_group(0, bind_group, &[]);
|
||||
pass.draw(0..*vertex_count, 0..1);
|
||||
}
|
||||
} else {
|
||||
pass.set_pipeline(&self.checker_rect_pipeline);
|
||||
for (vertex_count, bind_group) in &checker_draws {
|
||||
pass.set_bind_group(0, bind_group, &[]);
|
||||
pass.draw(0..*vertex_count, 0..1);
|
||||
}
|
||||
}
|
||||
|
||||
pass.set_pipeline(&self.fullscreen_pipeline);
|
||||
pass.set_bind_group(0, &fullscreen_bind_group, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
|
||||
queue.submit(std::iter::once(encoder.finish()));
|
||||
}
|
||||
|
||||
fn create_checker_bind_group(&self, device: &wgpu::Device, uniforms: CompositeUniforms) -> wgpu::BindGroup {
|
||||
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("background_checker_uniforms"),
|
||||
contents: bytemuck::bytes_of(&uniforms),
|
||||
usage: wgpu::BufferUsages::UNIFORM,
|
||||
});
|
||||
|
||||
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("background_checker_bind_group"),
|
||||
layout: &self.checker_bind_group_layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: buffer.as_entire_binding(),
|
||||
}],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
struct CompositeUniforms {
|
||||
transform_x: [f32; 2],
|
||||
transform_y: [f32; 2],
|
||||
transform_translation: [f32; 2],
|
||||
rect_min: [f32; 2],
|
||||
rect_max: [f32; 2],
|
||||
viewport_size: [f32; 2],
|
||||
pattern_origin: [f32; 2],
|
||||
checker_size: f32,
|
||||
_pad: f32,
|
||||
}
|
||||
|
||||
impl CompositeUniforms {
|
||||
fn fullscreen(viewport_size: Vec2, screen_to_document: Affine2, checker_size_doc: f32) -> Self {
|
||||
Self::new(screen_to_document, Vec2::ZERO, Vec2::ZERO, viewport_size, Vec2::ZERO, checker_size_doc)
|
||||
}
|
||||
|
||||
fn rect(rect_min: Vec2, rect_max: Vec2, document_to_screen: Affine2, viewport_size: Vec2, checker_size_doc: f32) -> Self {
|
||||
Self::new(document_to_screen, rect_min, rect_max, viewport_size, rect_min, checker_size_doc)
|
||||
}
|
||||
|
||||
fn new(transform: Affine2, rect_min: Vec2, rect_max: Vec2, viewport_size: Vec2, pattern_origin: Vec2, checker_size: f32) -> Self {
|
||||
Self {
|
||||
transform_x: transform.matrix2.x_axis.to_array(),
|
||||
transform_y: transform.matrix2.y_axis.to_array(),
|
||||
transform_translation: transform.translation.to_array(),
|
||||
rect_min: rect_min.to_array(),
|
||||
rect_max: rect_max.to_array(),
|
||||
viewport_size: viewport_size.to_array(),
|
||||
pattern_origin: pattern_origin.to_array(),
|
||||
checker_size,
|
||||
_pad: 0.,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,20 @@
|
|||
mod background; // TODO: Think about where to place this. Likely inlined in the node. Requires refactor of wgpu pipline usage.
|
||||
mod context;
|
||||
mod resample;
|
||||
pub mod shader_runtime;
|
||||
mod texture_cache;
|
||||
pub mod texture_conversion;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::background::BackgroundCompositor;
|
||||
use crate::resample::Resampler;
|
||||
use crate::shader_runtime::ShaderRuntime;
|
||||
use crate::texture_cache::TextureCache;
|
||||
use anyhow::Result;
|
||||
use core_types::Color;
|
||||
use futures::lock::Mutex;
|
||||
use glam::UVec2;
|
||||
use glam::{Affine2, UVec2};
|
||||
use graphene_application_io::{ApplicationIo, EditorApi};
|
||||
use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
|
||||
use wgpu::{Origin3d, TextureAspect};
|
||||
|
|
@ -18,11 +25,15 @@ pub use rendering::RenderContext;
|
|||
pub use wgpu::Backends as WgpuBackends;
|
||||
pub use wgpu::Features as WgpuFeatures;
|
||||
|
||||
const TEXTURE_CACHE_SIZE: u64 = 256 * 1024 * 1024; // 256 MiB
|
||||
|
||||
#[derive(dyn_any::DynAny)]
|
||||
pub struct WgpuExecutor {
|
||||
pub context: WgpuContext,
|
||||
texture_cache: Mutex<TextureCache>,
|
||||
vello_renderer: Mutex<Renderer>,
|
||||
resampler: Resampler,
|
||||
background_compositor: BackgroundCompositor,
|
||||
pub shader_runtime: ShaderRuntime,
|
||||
}
|
||||
|
||||
|
|
@ -38,105 +49,55 @@ impl<'a, T: ApplicationIo<Executor = WgpuExecutor>> From<&'a EditorApi<T>> for &
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TargetTexture {
|
||||
texture: wgpu::Texture,
|
||||
view: wgpu::TextureView,
|
||||
size: UVec2,
|
||||
}
|
||||
|
||||
impl TargetTexture {
|
||||
/// Creates a new TargetTexture with the specified size.
|
||||
pub fn new(device: &wgpu::Device, size: UVec2) -> Self {
|
||||
let size = size.max(UVec2::ONE);
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: None,
|
||||
size: wgpu::Extent3d {
|
||||
width: size.x,
|
||||
height: size.y,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
|
||||
format: VELLO_SURFACE_FORMAT,
|
||||
view_formats: &[],
|
||||
});
|
||||
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
Self { texture, view, size }
|
||||
}
|
||||
|
||||
/// Ensures the texture has the specified size, creating a new one if needed.
|
||||
/// This allows reusing the same texture across frames when the size hasn't changed.
|
||||
pub fn ensure_size(&mut self, device: &wgpu::Device, size: UVec2) {
|
||||
let size = size.max(UVec2::ONE);
|
||||
if self.size == size {
|
||||
return;
|
||||
}
|
||||
|
||||
*self = Self::new(device, size);
|
||||
}
|
||||
|
||||
/// Returns a reference to the texture view for rendering.
|
||||
pub fn view(&self) -> &wgpu::TextureView {
|
||||
&self.view
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying texture.
|
||||
pub fn texture(&self) -> &wgpu::Texture {
|
||||
&self.texture
|
||||
}
|
||||
}
|
||||
|
||||
const VELLO_SURFACE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||
|
||||
impl WgpuExecutor {
|
||||
pub async fn render_vello_scene_to_texture(&self, scene: &Scene, size: UVec2, context: &RenderContext) -> Result<wgpu::Texture> {
|
||||
let mut output = None;
|
||||
self.render_vello_scene_to_target_texture(scene, size, context, &mut output).await?;
|
||||
Ok(output.unwrap().texture)
|
||||
}
|
||||
pub async fn render_vello_scene(&self, scene: &Scene, size: UVec2, context: &RenderContext, background: Option<Color>) -> Result<Arc<wgpu::Texture>> {
|
||||
let texture = self.request_texture(size).await;
|
||||
|
||||
pub async fn render_vello_scene_to_target_texture(&self, scene: &Scene, size: UVec2, context: &RenderContext, output: &mut Option<TargetTexture>) -> Result<()> {
|
||||
// Initialize (lazily) if this is the first call
|
||||
if output.is_none() {
|
||||
*output = Some(TargetTexture::new(&self.context.device, size));
|
||||
}
|
||||
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
if let Some(target_texture) = output.as_mut() {
|
||||
target_texture.ensure_size(&self.context.device, size);
|
||||
let [r, g, b, a] = background.unwrap_or(Color::TRANSPARENT).to_rgba8();
|
||||
let render_params = RenderParams {
|
||||
base_color: vello::peniko::Color::from_rgba8(r, g, b, a),
|
||||
width: size.x,
|
||||
height: size.y,
|
||||
antialiasing_method: AaConfig::Msaa16,
|
||||
};
|
||||
|
||||
let render_params = RenderParams {
|
||||
base_color: vello::peniko::Color::from_rgba8(0, 0, 0, 0),
|
||||
width: size.x,
|
||||
height: size.y,
|
||||
antialiasing_method: AaConfig::Msaa16,
|
||||
};
|
||||
|
||||
{
|
||||
let mut renderer = self.vello_renderer.lock().await;
|
||||
for (image_brush, texture) in context.resource_overrides.iter() {
|
||||
let texture_view = wgpu::TexelCopyTextureInfoBase {
|
||||
texture: texture.clone(),
|
||||
mip_level: 0,
|
||||
origin: Origin3d::ZERO,
|
||||
aspect: TextureAspect::All,
|
||||
};
|
||||
renderer.override_image(&image_brush.image, Some(texture_view));
|
||||
}
|
||||
renderer.render_to_texture(&self.context.device, &self.context.queue, scene, target_texture.view(), &render_params)?;
|
||||
for (image_brush, _) in context.resource_overrides.iter() {
|
||||
renderer.override_image(&image_brush.image, None);
|
||||
}
|
||||
{
|
||||
let mut renderer = self.vello_renderer.lock().await;
|
||||
for (image_brush, texture) in context.resource_overrides.iter() {
|
||||
let texture_view = wgpu::TexelCopyTextureInfoBase {
|
||||
texture: texture.clone(),
|
||||
mip_level: 0,
|
||||
origin: Origin3d::ZERO,
|
||||
aspect: TextureAspect::All,
|
||||
};
|
||||
renderer.override_image(&image_brush.image, Some(texture_view));
|
||||
}
|
||||
renderer.render_to_texture(&self.context.device, &self.context.queue, scene, &texture_view, &render_params)?;
|
||||
for (image_brush, _) in context.resource_overrides.iter() {
|
||||
renderer.override_image(&image_brush.image, None);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
Ok(texture)
|
||||
}
|
||||
|
||||
pub fn resample_texture(&self, source: &wgpu::Texture, target_size: UVec2, transform: &glam::DAffine2) -> wgpu::Texture {
|
||||
self.resampler.resample(&self.context, source, target_size, transform)
|
||||
pub async fn resample_texture(&self, source: &wgpu::Texture, size: UVec2, transform: &glam::DAffine2) -> Arc<wgpu::Texture> {
|
||||
let out = self.request_texture(size).await;
|
||||
self.resampler.resample(&self.context, source, transform, &out);
|
||||
out
|
||||
}
|
||||
|
||||
pub async fn composite_background(&self, foreground: &wgpu::Texture, backgrounds: &[rendering::Background], document_to_screen: Affine2, zoom: f32) -> Arc<wgpu::Texture> {
|
||||
let size = foreground.size();
|
||||
let output = self.request_texture(UVec2::new(size.width, size.height)).await;
|
||||
self.background_compositor.composite(&self.context, foreground, &output, backgrounds, document_to_screen, zoom);
|
||||
output
|
||||
}
|
||||
|
||||
pub async fn request_texture(&self, size: UVec2) -> Arc<wgpu::Texture> {
|
||||
self.texture_cache.lock().await.request_texture(&self.context.device, size)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -158,13 +119,19 @@ impl WgpuExecutor {
|
|||
.map_err(|e| anyhow::anyhow!("Failed to create Vello renderer: {:?}", e))
|
||||
.ok()?;
|
||||
|
||||
let texture_cache = TextureCache::new(TEXTURE_CACHE_SIZE);
|
||||
|
||||
let resampler = Resampler::new(&context.device);
|
||||
let background_compositor = BackgroundCompositor::new(&context.device);
|
||||
let shader_runtime = ShaderRuntime::new(&context);
|
||||
|
||||
Some(Self {
|
||||
shader_runtime: ShaderRuntime::new(&context),
|
||||
context,
|
||||
resampler,
|
||||
texture_cache: texture_cache.into(),
|
||||
vello_renderer: vello_renderer.into(),
|
||||
resampler,
|
||||
background_compositor,
|
||||
shader_runtime,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::WgpuContext;
|
||||
use glam::{DAffine2, UVec2, Vec2};
|
||||
use glam::{DAffine2, Vec2};
|
||||
|
||||
pub struct Resampler {
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
|
|
@ -74,29 +74,11 @@ impl Resampler {
|
|||
Resampler { pipeline, bind_group_layout }
|
||||
}
|
||||
|
||||
pub fn resample(&self, context: &WgpuContext, source: &wgpu::Texture, target_size: UVec2, transform: &DAffine2) -> wgpu::Texture {
|
||||
let device = &context.device;
|
||||
let queue = &context.queue;
|
||||
|
||||
let output_texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("resample_output"),
|
||||
size: wgpu::Extent3d {
|
||||
width: target_size.x.max(1),
|
||||
height: target_size.y.max(1),
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
pub fn resample(&self, context: &WgpuContext, source: &wgpu::Texture, transform: &DAffine2, output: &wgpu::Texture) {
|
||||
let source_view = source.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let output_view = output.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let params_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
let params_buffer = context.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("resample_params"),
|
||||
size: 32,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
|
|
@ -104,9 +86,9 @@ impl Resampler {
|
|||
});
|
||||
|
||||
let params_data = [transform.matrix2.x_axis.as_vec2(), transform.matrix2.y_axis.as_vec2(), transform.translation.as_vec2(), Vec2::ZERO];
|
||||
queue.write_buffer(¶ms_buffer, 0, bytemuck::cast_slice(¶ms_data));
|
||||
context.queue.write_buffer(¶ms_buffer, 0, bytemuck::cast_slice(¶ms_data));
|
||||
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
let bind_group = context.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("resample_bind_group"),
|
||||
layout: &self.bind_group_layout,
|
||||
entries: &[
|
||||
|
|
@ -121,7 +103,7 @@ impl Resampler {
|
|||
],
|
||||
});
|
||||
|
||||
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("resample_encoder") });
|
||||
let mut encoder = context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("resample_encoder") });
|
||||
|
||||
{
|
||||
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
|
|
@ -143,8 +125,6 @@ impl Resampler {
|
|||
render_pass.draw(0..3, 0..1);
|
||||
}
|
||||
|
||||
queue.submit([encoder.finish()]);
|
||||
|
||||
output_texture
|
||||
context.queue.submit([encoder.finish()]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
use glam::UVec2;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(crate) struct TextureCache {
|
||||
/// Always sorted oldest-first by insertion/last-use order.
|
||||
textures: VecDeque<Arc<wgpu::Texture>>,
|
||||
max_free_bytes: u64,
|
||||
}
|
||||
|
||||
impl TextureCache {
|
||||
pub fn new(max_free_bytes: u64) -> Self {
|
||||
Self {
|
||||
textures: VecDeque::new(),
|
||||
max_free_bytes,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_texture(&mut self, device: &wgpu::Device, size: UVec2) -> Arc<wgpu::Texture> {
|
||||
let size = size.max(UVec2::ONE);
|
||||
|
||||
if let Some(pos) = self
|
||||
.textures
|
||||
.iter()
|
||||
.position(|texture| UVec2::new(texture.width(), texture.height()) == size && Arc::strong_count(texture) == 1)
|
||||
{
|
||||
let entry = self.textures.remove(pos).unwrap();
|
||||
let texture = entry.clone();
|
||||
self.textures.push_back(entry);
|
||||
return texture;
|
||||
}
|
||||
|
||||
let incoming_bytes = size.x as u64 * size.y as u64 * 4;
|
||||
self.evict_until_fits(incoming_bytes);
|
||||
|
||||
let texture = Arc::new(device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some(&format!("cached_texture_{}x{}", size.x, size.y)),
|
||||
size: wgpu::Extent3d {
|
||||
width: size.x,
|
||||
height: size.y,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
usage: wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
}));
|
||||
|
||||
self.textures.push_back(texture.clone());
|
||||
|
||||
texture
|
||||
}
|
||||
|
||||
fn total_free_bytes(&self) -> u64 {
|
||||
self.textures
|
||||
.iter()
|
||||
.filter(|texture| Arc::strong_count(texture) == 1)
|
||||
.map(|texture| texture.memory_size_estimate())
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn evict_until_fits(&mut self, incoming_bytes: u64) {
|
||||
let mut free_bytes = self.total_free_bytes();
|
||||
let max_free_bytes = self.max_free_bytes;
|
||||
|
||||
if free_bytes + incoming_bytes <= max_free_bytes {
|
||||
return;
|
||||
}
|
||||
|
||||
self.textures.retain(|texture| {
|
||||
if free_bytes + incoming_bytes <= max_free_bytes {
|
||||
return true;
|
||||
}
|
||||
if Arc::strong_count(texture) == 1 {
|
||||
free_bytes -= texture.memory_size_estimate();
|
||||
texture.destroy();
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
trait TextureMemoryCostEstimateExt {
|
||||
fn memory_size_estimate(&self) -> u64;
|
||||
}
|
||||
|
||||
impl TextureMemoryCostEstimateExt for wgpu::Texture {
|
||||
fn memory_size_estimate(&self) -> u64 {
|
||||
self.width() as u64 * self.height() as u64 * 4
|
||||
}
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ pub async fn pixel_preview<'a: 'n>(
|
|||
let transform = DAffine2::from_translation(-upstream_min) * footprint.transform.inverse() * DAffine2::from_scale(logical_resolution);
|
||||
|
||||
let exec = editor_api.application_io.as_ref().unwrap().gpu_executor().unwrap();
|
||||
let resampled = exec.resample_texture(source_texture.as_ref(), physical_resolution, &transform);
|
||||
let resampled = exec.resample_texture(source_texture.as_ref(), physical_resolution, &transform).await;
|
||||
|
||||
result.data = RenderOutputType::Texture(resampled.into());
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractAnimationTime, E
|
|||
use glam::{DAffine2, DVec2, IVec2, UVec2};
|
||||
use graph_craft::application_io::PlatformEditorApi;
|
||||
use graph_craft::document::value::RenderOutput;
|
||||
use graphene_application_io::ApplicationIo;
|
||||
use graphene_application_io::{ApplicationIo, ImageTexture};
|
||||
use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams};
|
||||
use std::collections::HashSet;
|
||||
use std::hash::Hash;
|
||||
|
|
@ -26,7 +26,7 @@ pub struct TileCoord {
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachedRegion {
|
||||
pub texture: wgpu::Texture,
|
||||
pub texture: ImageTexture,
|
||||
pub texture_size: UVec2,
|
||||
pub tiles: Vec<TileCoord>,
|
||||
pub metadata: rendering::RenderMetadata,
|
||||
|
|
@ -41,7 +41,6 @@ pub struct CacheKey {
|
|||
pub device_scale: u64,
|
||||
pub zoom: u64,
|
||||
pub rotation: u64,
|
||||
pub hide_artboards: bool,
|
||||
pub for_export: bool,
|
||||
pub for_mask: bool,
|
||||
pub thumbnail: bool,
|
||||
|
|
@ -60,7 +59,6 @@ impl CacheKey {
|
|||
device_scale: f64,
|
||||
zoom: f64,
|
||||
rotation: f64,
|
||||
hide_artboards: bool,
|
||||
for_export: bool,
|
||||
for_mask: bool,
|
||||
thumbnail: bool,
|
||||
|
|
@ -87,7 +85,6 @@ impl CacheKey {
|
|||
device_scale: device_scale.to_bits(),
|
||||
zoom: zoom.to_bits(),
|
||||
rotation: quantized_rotation.to_bits(),
|
||||
hide_artboards,
|
||||
for_export,
|
||||
for_mask,
|
||||
thumbnail,
|
||||
|
|
@ -100,23 +97,27 @@ impl CacheKey {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, dyn_any::DynAny, Debug)]
|
||||
pub struct TileCache(Arc<Mutex<TileCacheImpl>>);
|
||||
|
||||
impl TileCache {
|
||||
pub fn query(&self, viewport_bounds: &AxisAlignedBbox, cache_key: &CacheKey, max_region_area: u32) -> CacheQuery {
|
||||
self.0.lock().unwrap().query(viewport_bounds, cache_key, max_region_area)
|
||||
}
|
||||
|
||||
pub fn store_regions(&self, regions: Vec<CachedRegion>) {
|
||||
self.0.lock().unwrap().store_regions(regions);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct TileCacheImpl {
|
||||
regions: Vec<CachedRegion>,
|
||||
timestamp: u64,
|
||||
total_memory: usize,
|
||||
cache_key: CacheKey,
|
||||
texture_cache_resolution: UVec2,
|
||||
/// Pool of textures of the same size: `texture_cache_resolution`.
|
||||
/// Reusing textures reduces the wgpu allocation pressure,
|
||||
/// which is a problem on web since we have to wait for
|
||||
/// the browser to garbage collect unused textures, eating up memory.
|
||||
texture_cache: Vec<Arc<wgpu::Texture>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, dyn_any::DynAny, Debug)]
|
||||
pub struct TileCache(Arc<Mutex<TileCacheImpl>>);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderRegion {
|
||||
pub tiles: Vec<TileCoord>,
|
||||
|
|
@ -205,7 +206,6 @@ impl TileCacheImpl {
|
|||
while self.total_memory > MAX_CACHE_MEMORY_BYTES && !self.regions.is_empty() {
|
||||
if let Some((oldest_idx, _)) = self.regions.iter().enumerate().min_by_key(|(_, r)| r.last_access) {
|
||||
let removed = self.regions.remove(oldest_idx);
|
||||
removed.texture.destroy();
|
||||
self.total_memory = self.total_memory.saturating_sub(removed.memory_size);
|
||||
} else {
|
||||
break;
|
||||
|
|
@ -214,56 +214,9 @@ impl TileCacheImpl {
|
|||
}
|
||||
|
||||
fn invalidate_all(&mut self) {
|
||||
for region in &self.regions {
|
||||
region.texture.destroy();
|
||||
}
|
||||
self.regions.clear();
|
||||
self.total_memory = 0;
|
||||
}
|
||||
|
||||
pub fn request_texture(&mut self, size: UVec2, device: &wgpu::Device) -> Arc<wgpu::Texture> {
|
||||
if self.texture_cache_resolution != size {
|
||||
self.texture_cache_resolution = size;
|
||||
self.texture_cache.clear();
|
||||
}
|
||||
self.texture_cache.truncate(5);
|
||||
for texture in &self.texture_cache {
|
||||
if Arc::strong_count(texture) == 1 {
|
||||
return Arc::clone(texture);
|
||||
}
|
||||
}
|
||||
let texture = Arc::new(device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("viewport_output"),
|
||||
size: wgpu::Extent3d {
|
||||
width: size.x,
|
||||
height: size.y,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
}));
|
||||
self.texture_cache.push(texture.clone());
|
||||
|
||||
texture
|
||||
}
|
||||
}
|
||||
|
||||
impl TileCache {
|
||||
pub fn query(&self, viewport_bounds: &AxisAlignedBbox, cache_key: &CacheKey, max_region_area: u32) -> CacheQuery {
|
||||
self.0.lock().unwrap().query(viewport_bounds, cache_key, max_region_area)
|
||||
}
|
||||
|
||||
pub fn store_regions(&self, regions: Vec<CachedRegion>) {
|
||||
self.0.lock().unwrap().store_regions(regions);
|
||||
}
|
||||
|
||||
pub fn request_texture(&self, size: UVec2, device: &wgpu::Device) -> Arc<wgpu::Texture> {
|
||||
self.0.lock().unwrap().request_texture(size, device)
|
||||
}
|
||||
}
|
||||
|
||||
fn group_into_regions(tiles: &[TileCoord], max_region_area: u32) -> Vec<RenderRegion> {
|
||||
|
|
@ -411,7 +364,6 @@ pub async fn render_output_cache<'a: 'n>(
|
|||
device_scale,
|
||||
zoom,
|
||||
rotation,
|
||||
render_params.hide_artboards,
|
||||
render_params.for_export,
|
||||
render_params.for_mask,
|
||||
render_params.thumbnail,
|
||||
|
|
@ -454,10 +406,9 @@ pub async fn render_output_cache<'a: 'n>(
|
|||
|
||||
let exec = editor_api.application_io.as_ref().unwrap().gpu_executor().unwrap();
|
||||
|
||||
let device = &exec.context.device;
|
||||
let output_texture = tile_cache.request_texture(physical_resolution, device);
|
||||
let output_texture = exec.request_texture(physical_resolution).await;
|
||||
|
||||
let combined_metadata = composite_cached_regions(&all_regions, output_texture.as_ref(), &device_origin_offset, &footprint.transform, exec);
|
||||
let combined_metadata = composite_cached_regions(&all_regions, &output_texture, &device_origin_offset, &footprint.transform, exec);
|
||||
|
||||
RenderOutput {
|
||||
data: RenderOutputType::Texture(output_texture.into()),
|
||||
|
|
@ -496,7 +447,7 @@ where
|
|||
let region_ctx = OwnedContextImpl::from(ctx).with_footprint(region_footprint).with_vararg(Box::new(region_params)).into_context();
|
||||
let mut result = render_fn(region_ctx).await;
|
||||
|
||||
let RenderOutputType::Texture(rendered_texture) = result.data else {
|
||||
let RenderOutputType::Texture(texture) = result.data else {
|
||||
unreachable!("render_missing_region: expected texture output from Vello render");
|
||||
};
|
||||
|
||||
|
|
@ -506,7 +457,7 @@ where
|
|||
let memory_size = (region_pixel_size.x * region_pixel_size.y) as usize * BYTES_PER_PIXEL;
|
||||
|
||||
CachedRegion {
|
||||
texture: rendered_texture.as_ref().clone(),
|
||||
texture,
|
||||
texture_size: region_pixel_size,
|
||||
tiles: region.tiles.clone(),
|
||||
metadata: result.metadata,
|
||||
|
|
@ -552,7 +503,7 @@ fn composite_cached_regions(
|
|||
if width > 0 && height > 0 {
|
||||
encoder.copy_texture_to_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: ®ion.texture,
|
||||
texture: region.texture.as_ref(),
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d { x: src_x, y: src_y, z: 0 },
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
|
|
|
|||
|
|
@ -7,12 +7,10 @@ pub use graph_craft::application_io::*;
|
|||
use graph_craft::document::value::RenderOutput;
|
||||
pub use graph_craft::document::value::RenderOutputType;
|
||||
use graphene_application_io::{ApplicationIo, ExportFormat, RenderConfig};
|
||||
use graphic_types::raster_types::Image;
|
||||
use graphic_types::raster_types::{CPU, Raster};
|
||||
use graphic_types::{Graphic, Vector};
|
||||
use rendering::{Render, RenderOutputType as RenderOutputTypeRequest, RenderParams, RenderSvgSegmentList, SvgRender, checkerboard_brush};
|
||||
use rendering::{RenderMetadata, SvgSegment};
|
||||
use std::collections::HashMap;
|
||||
use rendering::{Render, RenderMetadata, RenderOutputType as RenderOutputTypeRequest, RenderParams, SvgRender, SvgRenderOutput};
|
||||
use std::fmt::Write;
|
||||
use std::sync::Arc;
|
||||
use vector_types::GradientStops;
|
||||
use wgpu_executor::RenderContext;
|
||||
|
|
@ -20,19 +18,15 @@ use wgpu_executor::RenderContext;
|
|||
// Re-export render_output_cache from render_cache module
|
||||
pub use crate::render_cache::render_output_cache;
|
||||
|
||||
/// List of (canvas id, image data) pairs for embedding images as canvases in the final SVG string.
|
||||
type ImageData = HashMap<core_types::graphene_hash::CacheHashWrapper<Image<Color>>, u64>;
|
||||
|
||||
#[derive(Clone, dyn_any::DynAny)]
|
||||
pub enum RenderIntermediateType {
|
||||
Vello(Arc<(vello::Scene, RenderContext)>),
|
||||
Svg(Arc<(String, ImageData, String)>),
|
||||
Svg(Arc<SvgRenderOutput>),
|
||||
}
|
||||
#[derive(Clone, dyn_any::DynAny)]
|
||||
pub struct RenderIntermediate {
|
||||
pub(crate) ty: RenderIntermediateType,
|
||||
pub(crate) metadata: RenderMetadata,
|
||||
pub(crate) contains_artboard: bool,
|
||||
}
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
|
|
@ -60,8 +54,6 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
|
|||
let footprint = Footprint::default();
|
||||
let mut metadata = RenderMetadata::default();
|
||||
data.collect_metadata(&mut metadata, footprint, None);
|
||||
let contains_artboard = data.contains_artboard();
|
||||
|
||||
match &render_params.render_output_type {
|
||||
RenderOutputTypeRequest::Vello => {
|
||||
let mut scene = vello::Scene::new();
|
||||
|
|
@ -72,7 +64,6 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
|
|||
RenderIntermediate {
|
||||
ty: RenderIntermediateType::Vello(Arc::new((scene, context))),
|
||||
metadata,
|
||||
contains_artboard,
|
||||
}
|
||||
}
|
||||
RenderOutputTypeRequest::Svg => {
|
||||
|
|
@ -81,14 +72,185 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
|
|||
data.render_svg(&mut render, render_params);
|
||||
|
||||
RenderIntermediate {
|
||||
ty: RenderIntermediateType::Svg(Arc::new((render.svg.to_svg_string(), render.image_data, render.svg_defs.clone()))),
|
||||
ty: RenderIntermediateType::Svg(Arc::new(render.into())),
|
||||
metadata,
|
||||
contains_artboard,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, editor_api: &'a PlatformEditorApi, data: RenderIntermediate) -> RenderOutput {
|
||||
let footprint = ctx.footprint();
|
||||
let render_params = ctx
|
||||
.vararg(0)
|
||||
.expect("Did not find var args")
|
||||
.downcast_ref::<RenderParams>()
|
||||
.expect("Downcasting render params yielded invalid type");
|
||||
let mut render_params = render_params.clone();
|
||||
render_params.footprint = *footprint;
|
||||
|
||||
let RenderIntermediate { ty, mut metadata } = data;
|
||||
metadata.apply_transform(footprint.transform);
|
||||
|
||||
let data = match (render_params.render_output_type, ty) {
|
||||
(RenderOutputTypeRequest::Svg, RenderIntermediateType::Svg(data)) => {
|
||||
let logical_resolution = render_params.footprint.resolution.as_dvec2() / render_params.scale;
|
||||
|
||||
let mut render = SvgRender::from(data.as_ref());
|
||||
render.wrap_with_transform(render_params.footprint.transform, Some(logical_resolution));
|
||||
|
||||
let output = SvgRenderOutput::from(render);
|
||||
assert!(output.svg_defs.is_empty());
|
||||
|
||||
RenderOutputType::Svg {
|
||||
svg: output.svg,
|
||||
image_data: output.image_data.into_iter().map(|(image, id)| (id, image.0)).collect(),
|
||||
}
|
||||
}
|
||||
(RenderOutputTypeRequest::Vello, RenderIntermediateType::Vello(data)) => {
|
||||
let Some(exec) = editor_api.application_io.as_ref().unwrap().gpu_executor() else {
|
||||
unreachable!("Attempted to render with Vello when no GPU executor is available");
|
||||
};
|
||||
let (scene, context) = data.as_ref();
|
||||
let scale = render_params.scale;
|
||||
let physical_resolution = render_params.footprint.resolution;
|
||||
|
||||
let scale_transform = glam::DAffine2::from_scale(glam::DVec2::splat(scale));
|
||||
let footprint_transform = scale_transform * render_params.footprint.transform;
|
||||
let footprint_transform_vello = vello::kurbo::Affine::new(footprint_transform.to_cols_array());
|
||||
|
||||
let mut transformed_scene = vello::Scene::new();
|
||||
transformed_scene.append(scene, Some(footprint_transform_vello));
|
||||
|
||||
// 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.
|
||||
//
|
||||
// `!is_finite()` rather than `== f32::INFINITY`: `scene.append` composes the child's `Affine::scale(INFINITY)` with
|
||||
// the viewport rotation, leaving `matrix[0] = cos(θ) * INFINITY`. In the (90°, 270°) tilt range cos is negative so
|
||||
// the result is `-INFINITY`, which the old equality check missed; Vello then rasterized a unit rect with non-finite
|
||||
// vertices, dropping the gradient and tanking performance. `!is_finite()` also covers NaN as a guard against future
|
||||
// code paths where `matrix[0]` could land on `0 * INFINITY`.
|
||||
let scaled_infinite_transform = vello::kurbo::Affine::scale_non_uniform(physical_resolution.x as f64, physical_resolution.y as f64);
|
||||
for transform in transformed_scene.encoding_mut().transforms.iter_mut() {
|
||||
if !transform.matrix[0].is_finite() {
|
||||
*transform = vello_encoding::Transform::from_kurbo(&scaled_infinite_transform);
|
||||
}
|
||||
}
|
||||
|
||||
let texture = exec
|
||||
.render_vello_scene(&transformed_scene, physical_resolution, context, None)
|
||||
.await
|
||||
.expect("Failed to render Vello scene");
|
||||
RenderOutputType::Texture(texture.into())
|
||||
}
|
||||
_ => unreachable!("Render node did not receive its requested data type"),
|
||||
};
|
||||
|
||||
RenderOutput { data, metadata }
|
||||
}
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
async fn render_background<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, editor_api: &'a PlatformEditorApi, data: RenderOutput) -> RenderOutput {
|
||||
let footprint = ctx.footprint();
|
||||
let render_params = ctx
|
||||
.vararg(0)
|
||||
.expect("Did not find var args")
|
||||
.downcast_ref::<RenderParams>()
|
||||
.expect("Downcasting render params yielded invalid type");
|
||||
|
||||
if !render_params.to_canvas() {
|
||||
return data;
|
||||
}
|
||||
|
||||
let RenderOutput { data: foreground_data, metadata } = data;
|
||||
let mut render_params = render_params.clone();
|
||||
render_params.footprint = *footprint;
|
||||
|
||||
let data = match foreground_data {
|
||||
RenderOutputType::Texture(foreground_texture) => {
|
||||
if let Some(exec) = editor_api.application_io.as_ref().unwrap().gpu_executor() {
|
||||
let doc_to_screen = (glam::DAffine2::from_scale(glam::DVec2::splat(render_params.scale)) * render_params.footprint.transform).as_affine2();
|
||||
let blended = exec
|
||||
.composite_background(foreground_texture.as_ref(), &metadata.backgrounds, doc_to_screen, render_params.viewport_zoom as f32)
|
||||
.await;
|
||||
|
||||
RenderOutputType::Texture(blended.into())
|
||||
} else {
|
||||
RenderOutputType::Texture(foreground_texture)
|
||||
}
|
||||
}
|
||||
RenderOutputType::Svg {
|
||||
svg: foreground_svg,
|
||||
image_data: foreground_images,
|
||||
} => {
|
||||
let mut render = SvgRender::new();
|
||||
|
||||
if render_params.viewport_zoom > 0. {
|
||||
let draw_checkerboard = |render: &mut SvgRender, rect: vello::kurbo::Rect, pattern_origin: glam::DVec2, checker_id_prefix: &str| {
|
||||
let checker_id = format!("{checker_id_prefix}-{}", generate_uuid());
|
||||
let cell_size = 8. / render_params.viewport_zoom;
|
||||
let pattern_size = cell_size * 2.;
|
||||
|
||||
write!(
|
||||
&mut render.svg_defs,
|
||||
r##"<pattern id="{checker_id}" x="{}" y="{}" width="{pattern_size}" height="{pattern_size}" patternUnits="userSpaceOnUse"><rect width="{pattern_size}" height="{pattern_size}" fill="#ffffff" /><rect x="{cell_size}" y="0" width="{cell_size}" height="{cell_size}" fill="#cccccc" /><rect x="0" y="{cell_size}" width="{cell_size}" height="{cell_size}" fill="#cccccc" /></pattern>"##,
|
||||
pattern_origin.x,
|
||||
pattern_origin.y,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
render.leaf_tag("rect", |attributes| {
|
||||
attributes.push("x", rect.x0.to_string());
|
||||
attributes.push("y", rect.y0.to_string());
|
||||
attributes.push("width", rect.width().to_string());
|
||||
attributes.push("height", rect.height().to_string());
|
||||
attributes.push("fill", format!("url(#{checker_id})"));
|
||||
});
|
||||
};
|
||||
|
||||
if metadata.backgrounds.is_empty() {
|
||||
if render_params.scale > 0. {
|
||||
let logical_resolution = render_params.footprint.resolution.as_dvec2() / render_params.scale;
|
||||
let logical_footprint = Footprint {
|
||||
resolution: logical_resolution.round().as_uvec2().max(glam::UVec2::ONE),
|
||||
..render_params.footprint
|
||||
};
|
||||
let bounds = logical_footprint.viewport_bounds_in_local_space();
|
||||
let min = bounds.start.floor();
|
||||
let max = bounds.end.ceil();
|
||||
|
||||
if min.is_finite() && max.is_finite() {
|
||||
let rect = vello::kurbo::Rect::new(min.x, min.y, max.x, max.y);
|
||||
draw_checkerboard(&mut render, rect, glam::DVec2::ZERO, "checkered-viewport");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for background in &metadata.backgrounds {
|
||||
let [a, b] = [background.location, background.location + background.dimensions];
|
||||
let rect = vello::kurbo::Rect::new(a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y));
|
||||
draw_checkerboard(&mut render, rect, glam::DVec2::new(rect.x0, rect.y0), "checkered-artboard");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let logical_resolution = render_params.footprint.resolution.as_dvec2() / render_params.scale;
|
||||
render.wrap_with_transform(render_params.footprint.transform, Some(logical_resolution));
|
||||
|
||||
let background = SvgRenderOutput::from(render);
|
||||
assert!(background.svg_defs.is_empty());
|
||||
|
||||
let svg = format!("{}{}", background.svg, foreground_svg);
|
||||
let image_data = foreground_images;
|
||||
|
||||
RenderOutputType::Svg { svg, image_data }
|
||||
}
|
||||
_ => unreachable!("Render background node received unsupported render output type"),
|
||||
};
|
||||
|
||||
RenderOutput { data, metadata }
|
||||
}
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
async fn create_context<'a: 'n>(
|
||||
// Context injections are defined in the wrap_network_in_scope function
|
||||
|
|
@ -104,10 +266,8 @@ async fn create_context<'a: 'n>(
|
|||
|
||||
let render_params = RenderParams {
|
||||
render_mode: render_config.render_mode,
|
||||
hide_artboards: false,
|
||||
for_export: render_config.for_export,
|
||||
render_output_type,
|
||||
footprint: Footprint::default(),
|
||||
scale: render_config.scale,
|
||||
viewport_zoom: footprint.scale_magnitudes().x,
|
||||
..Default::default()
|
||||
|
|
@ -123,133 +283,3 @@ async fn create_context<'a: 'n>(
|
|||
|
||||
data.eval(ctx).await
|
||||
}
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, editor_api: &'a PlatformEditorApi, data: RenderIntermediate) -> RenderOutput {
|
||||
let footprint = ctx.footprint();
|
||||
let render_params = ctx
|
||||
.vararg(0)
|
||||
.expect("Did not find var args")
|
||||
.downcast_ref::<RenderParams>()
|
||||
.expect("Downcasting render params yielded invalid type");
|
||||
let mut render_params = render_params.clone();
|
||||
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);
|
||||
|
||||
let data = match (render_params.render_output_type, &ty) {
|
||||
(RenderOutputTypeRequest::Svg, RenderIntermediateType::Svg(svg_data)) => {
|
||||
let mut rendering = SvgRender::new();
|
||||
|
||||
// Infinite canvas background (no artboards)
|
||||
if !contains_artboard && !render_params.hide_artboards {
|
||||
let show_checkerboard = render_params.to_canvas();
|
||||
if show_checkerboard && render_params.viewport_zoom > 0. {
|
||||
// Checkerboard pattern anchored at the document origin, tiling at 8x8 viewport pixels
|
||||
let checker_id = format!("checkered-canvas-{}", generate_uuid());
|
||||
let cell_size = 8. / render_params.viewport_zoom;
|
||||
let pattern_size = cell_size * 2.;
|
||||
|
||||
// Compute the axis-aligned bounding box of all four viewport corners in document space,
|
||||
// which is necessary when the view is rotated so the rect fully covers the visible area
|
||||
let inverse_transform = footprint.transform.inverse();
|
||||
let corners = [
|
||||
inverse_transform.transform_point2(glam::DVec2::ZERO),
|
||||
inverse_transform.transform_point2(glam::DVec2::new(logical_resolution.x, 0.)),
|
||||
inverse_transform.transform_point2(glam::DVec2::new(0., logical_resolution.y)),
|
||||
inverse_transform.transform_point2(logical_resolution),
|
||||
];
|
||||
let bb_min = corners.iter().fold(glam::DVec2::MAX, |acc, &c| acc.min(c));
|
||||
let bb_max = corners.iter().fold(glam::DVec2::MIN, |acc, &c| acc.max(c));
|
||||
|
||||
rendering.leaf_tag("rect", |attributes| {
|
||||
attributes.push("x", bb_min.x.to_string());
|
||||
attributes.push("y", bb_min.y.to_string());
|
||||
attributes.push("width", (bb_max.x - bb_min.x).to_string());
|
||||
attributes.push("height", (bb_max.y - bb_min.y).to_string());
|
||||
attributes.push("fill", format!("url(#{checker_id})"));
|
||||
});
|
||||
|
||||
// Pattern defs will be appended after the intermediate defs are copied below
|
||||
rendering.svg_defs = format!(
|
||||
r##"<pattern id="{checker_id}" x="0" y="0" width="{pattern_size}" height="{pattern_size}" patternUnits="userSpaceOnUse"><rect width="{pattern_size}" height="{pattern_size}" fill="#fff"/><rect x="{cell_size}" y="0" width="{cell_size}" height="{cell_size}" fill="#ccc"/><rect x="0" y="{cell_size}" width="{cell_size}" height="{cell_size}" fill="#ccc"/></pattern>"##,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let existing_defs = rendering.svg_defs.clone();
|
||||
rendering.svg.push(SvgSegment::from(svg_data.0.clone()));
|
||||
rendering.image_data = svg_data.1.clone();
|
||||
rendering.svg_defs = format!("{existing_defs}{}", svg_data.2);
|
||||
|
||||
rendering.wrap_with_transform(footprint.transform, Some(logical_resolution));
|
||||
RenderOutputType::Svg {
|
||||
svg: rendering.svg.to_svg_string(),
|
||||
image_data: rendering.image_data.into_iter().map(|(image, id)| (id, image.0)).collect(),
|
||||
}
|
||||
}
|
||||
(RenderOutputTypeRequest::Vello, RenderIntermediateType::Vello(vello_data)) => {
|
||||
let Some(exec) = editor_api.application_io.as_ref().unwrap().gpu_executor() else {
|
||||
unreachable!("Attempted to render with Vello when no GPU executor is available");
|
||||
};
|
||||
let (child, context) = Arc::as_ref(vello_data);
|
||||
|
||||
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());
|
||||
|
||||
let mut scene = vello::Scene::new();
|
||||
|
||||
// Infinite canvas checkerboard (when no artboards are present)
|
||||
let show_checkerboard = !render_params.for_export && !contains_artboard && !render_params.hide_artboards;
|
||||
if show_checkerboard && scale > 0. && render_params.viewport_zoom > 0. {
|
||||
// Compute the axis-aligned bounding box of all four viewport corners in document space,
|
||||
// which is necessary so the rect fully covers the visible area when the canvas is tilted
|
||||
let inverse_footprint = footprint_transform.inverse();
|
||||
let corners = [
|
||||
inverse_footprint.transform_point2(glam::DVec2::ZERO),
|
||||
inverse_footprint.transform_point2(glam::DVec2::new(physical_resolution.x as f64, 0.)),
|
||||
inverse_footprint.transform_point2(glam::DVec2::new(0., physical_resolution.y as f64)),
|
||||
inverse_footprint.transform_point2(physical_resolution.as_dvec2()),
|
||||
];
|
||||
let bb_min = corners.iter().fold(glam::DVec2::MAX, |acc, &c| acc.min(c));
|
||||
let bb_max = corners.iter().fold(glam::DVec2::MIN, |acc, &c| acc.max(c));
|
||||
let doc_rect = vello::kurbo::Rect::new(bb_min.x, bb_min.y, bb_max.x, bb_max.y);
|
||||
|
||||
// Draw in document space, transformed to screen by footprint_transform (includes rotation)
|
||||
// Brush maps each pixel to 1/viewport_zoom document units, giving constant 8px cells
|
||||
let brush_transform = vello::kurbo::Affine::scale(1. / render_params.viewport_zoom);
|
||||
scene.fill(vello::peniko::Fill::NonZero, footprint_transform_vello, &checkerboard_brush(), Some(brush_transform), &doc_rect);
|
||||
}
|
||||
|
||||
scene.append(child, Some(footprint_transform_vello));
|
||||
|
||||
// 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.
|
||||
//
|
||||
// `!is_finite()` rather than `== f32::INFINITY`: `scene.append` composes the child's `Affine::scale(INFINITY)` with
|
||||
// the viewport rotation, leaving `matrix[0] = cos(θ) * INFINITY`. In the (90°, 270°) tilt range cos is negative so
|
||||
// the result is `-INFINITY`, which the old equality check missed; Vello then rasterized a unit rect with non-finite
|
||||
// vertices, dropping the gradient and tanking performance. `!is_finite()` also covers NaN as a guard against future
|
||||
// code paths where `matrix[0]` could land on `0 * INFINITY`.
|
||||
let scaled_infinite_transform = vello::kurbo::Affine::scale_non_uniform(physical_resolution.x as f64, physical_resolution.y as f64);
|
||||
for transform in scene.encoding_mut().transforms.iter_mut() {
|
||||
if !transform.matrix[0].is_finite() {
|
||||
*transform = vello_encoding::Transform::from_kurbo(&scaled_infinite_transform);
|
||||
}
|
||||
}
|
||||
|
||||
let texture = Arc::new(exec.render_vello_scene_to_texture(&scene, physical_resolution, context).await.expect("Failed to render Vello scene"));
|
||||
|
||||
RenderOutputType::Texture(texture.into())
|
||||
}
|
||||
_ => unreachable!("Render node did not receive its requested data type"),
|
||||
};
|
||||
RenderOutput { data, metadata }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue