mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Reimplement checkered background rendering (#4034)
* Reimplement background checkerboard rendering
This commit is contained in:
@@ -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);
|
||||
}
|
||||
344
node-graph/libraries/wgpu-executor/src/background/mod.rs
Normal file
344
node-graph/libraries/wgpu-executor/src/background/mod.rs
Normal 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.,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()]);
|
||||
}
|
||||
}
|
||||
|
||||
95
node-graph/libraries/wgpu-executor/src/texture_cache.rs
Normal file
95
node-graph/libraries/wgpu-executor/src/texture_cache.rs
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user