Reimplement checkered background rendering (#4034)

* Reimplement background checkerboard rendering
This commit is contained in:
Timon
2026-05-01 18:45:32 +02:00
parent 7cd5531730
commit 29f6e686ee
17 changed files with 936 additions and 459 deletions

View File

@@ -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,
};

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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.,
}
}
}

View File

@@ -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,
})
}
}

View File

@@ -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(&params_buffer, 0, bytemuck::cast_slice(&params_data));
context.queue.write_buffer(&params_buffer, 0, bytemuck::cast_slice(&params_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()]);
}
}

View File

@@ -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
}
}