blob: e0da40ae6ce6f17041a62ba50812e991cc3eb312 [file] [log] [blame] [edit]
// Copyright 2023 The Vello authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
mod validate;
use crate::{
debug::validate::{validate_line_soup, LineEndpoint},
engine::{BindType, ImageProxy, Recording, ResourceProxy, ShaderId},
render::CapturedBuffers,
wgpu_engine::WgpuEngine,
DebugDownloads, RenderParams,
};
use {
bytemuck::{offset_of, Pod, Zeroable},
peniko::Color,
vello_encoding::{BumpAllocators, LineSoup, PathBbox},
};
#[derive(Copy, Clone)]
pub struct DebugLayers(u8);
// TODO: Currently all layers require read-back of the BumpAllocators buffer. This isn't strictly
// necessary for layers other than `VALIDATION`. The debug visualizations use the bump buffer only
// to obtain various instance counts for draws and these could instead get written out to an
// indirect draw buffer. OTOH `VALIDATION` should always require readback since we want to be able
// to run the same CPU-side tests for both CPU and GPU shaders.
impl DebugLayers {
/// Visualize the bounding box of every path.
pub const BOUNDING_BOXES: DebugLayers = DebugLayers(1 << 0);
/// Visualize the post-flattening line segments using line primitives.
pub const LINESOUP_SEGMENTS: DebugLayers = DebugLayers(1 << 1);
/// Visualize the post-flattening line endpoints.
pub const LINESOUP_POINTS: DebugLayers = DebugLayers(1 << 2);
/// Enable validation of internal buffer contents and visualize errors. Validation tests are
/// run on the CPU and require buffer contents to be read-back.
///
/// Supported validation tests:
///
/// - Watertightness: validate that every line segment within a path is connected without
/// any gaps. Line endpoints that don't precisely overlap another endpoint get visualized
/// as red circles and logged to stderr.
///
pub const VALIDATION: DebugLayers = DebugLayers(1 << 3);
pub const fn from_bits(bits: u8) -> Self {
Self(bits)
}
pub const fn none() -> Self {
Self(0)
}
pub const fn all() -> Self {
Self(
Self::BOUNDING_BOXES.0
| Self::LINESOUP_SEGMENTS.0
| Self::LINESOUP_POINTS.0
| Self::VALIDATION.0,
)
}
pub fn is_empty(&self) -> bool {
self.0 == 0
}
pub fn check_bits(&self, mask: DebugLayers) -> bool {
self.0 & mask.0 == mask.0
}
pub fn toggle(&mut self, mask: DebugLayers) {
self.0 ^= mask.0
}
}
impl std::ops::BitOr for DebugLayers {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
Self(self.0 | rhs.0)
}
}
pub(crate) struct DebugRenderer {
// `clear_tint` slightly darkens the output from the vello renderer to make the debug overlays
// more distinguishable.
clear_tint: ShaderId,
bboxes: ShaderId,
linesoup: ShaderId,
linesoup_points: ShaderId,
unpaired_points: ShaderId,
}
impl DebugRenderer {
pub fn new(
device: &wgpu::Device,
target_format: wgpu::TextureFormat,
engine: &mut WgpuEngine,
) -> Self {
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("debug layers"),
source: wgpu::ShaderSource::Wgsl(SHADERS.into()),
});
let clear_tint = engine.add_render_shader(
device,
"clear-tint",
&module,
"full_screen_quad_vert",
"solid_color_frag",
wgpu::PrimitiveTopology::TriangleStrip,
wgpu::ColorTargetState {
format: target_format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent::OVER,
}),
write_mask: wgpu::ColorWrites::ALL,
},
None,
&[],
);
let bboxes = engine.add_render_shader(
device,
"bbox-debug",
&module,
"bbox_vert",
"solid_color_frag",
wgpu::PrimitiveTopology::LineStrip,
wgpu::ColorTargetState {
format: target_format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
},
// This mirrors the layout of the PathBbox structure.
Some(wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<PathBbox>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Sint32x2,
offset: offset_of!(PathBbox, x0) as u64,
shader_location: 0,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Sint32x2,
offset: offset_of!(PathBbox, x1) as u64,
shader_location: 1,
},
],
}),
&[(BindType::Uniform, wgpu::ShaderStages::VERTEX)],
);
let linesoup = engine.add_render_shader(
device,
"linesoup-debug",
&module,
"linesoup_vert",
"solid_color_frag",
wgpu::PrimitiveTopology::LineList,
wgpu::ColorTargetState {
format: target_format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
},
// This mirrors the layout of the LineSoup structure.
Some(wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<LineSoup>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x2,
offset: offset_of!(LineSoup, p0) as u64,
shader_location: 0,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x2,
offset: offset_of!(LineSoup, p1) as u64,
shader_location: 1,
},
],
}),
&[(BindType::Uniform, wgpu::ShaderStages::VERTEX)],
);
let linesoup_points = engine.add_render_shader(
device,
"linepoints-debug",
&module,
"linepoints_vert",
"sdf_circle_frag",
wgpu::PrimitiveTopology::TriangleStrip,
wgpu::ColorTargetState {
format: target_format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent::OVER,
}),
write_mask: wgpu::ColorWrites::ALL,
},
// This mirrors the layout of the LineSoup structure. The pipeline only processes the
// first point of each line. Since all points should be paired, this is enough to
// render all points. All unpaired points alone get drawn by the `unpaired_points`
// pipeline, so no point should get missed.
Some(wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<LineSoup>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x2,
offset: offset_of!(LineSoup, p0) as u64,
shader_location: 0,
}],
}),
&[
(BindType::Uniform, wgpu::ShaderStages::VERTEX),
(
BindType::Uniform,
wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
),
],
);
let unpaired_points = engine.add_render_shader(
device,
"linepoints-debug",
&module,
"linepoints_vert",
"sdf_circle_frag",
wgpu::PrimitiveTopology::TriangleStrip,
wgpu::ColorTargetState {
format: target_format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent::OVER,
}),
write_mask: wgpu::ColorWrites::ALL,
},
// This mirrors the layout of the LineSoup structure.
Some(wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<LineEndpoint>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x2,
offset: offset_of!(LineEndpoint, x) as u64,
shader_location: 0,
}],
}),
&[
(BindType::Uniform, wgpu::ShaderStages::VERTEX),
(
BindType::Uniform,
wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
),
],
);
Self {
clear_tint,
bboxes,
linesoup,
linesoup_points,
unpaired_points,
}
}
pub fn render(
&self,
recording: &mut Recording,
target: ImageProxy,
captured: &CapturedBuffers,
bump: &BumpAllocators,
params: &RenderParams,
downloads: &DebugDownloads,
) {
if params.debug.is_empty() {
return;
}
let (unpaired_pts_len, unpaired_pts_buf) =
if params.debug.check_bits(DebugLayers::VALIDATION) {
// TODO: have this write directly to a GPU buffer?
let unpaired_pts: Vec<LineEndpoint> =
validate_line_soup(bytemuck::cast_slice(&downloads.lines.get_mapped_range()));
if unpaired_pts.is_empty() {
(0, None)
} else {
(
unpaired_pts.len(),
Some(
recording
.upload("unpaired points", bytemuck::cast_slice(&unpaired_pts[..])),
),
)
}
} else {
(0, None)
};
let uniforms = Uniforms {
width: params.width,
height: params.height,
};
let uniforms_buf =
ResourceProxy::Buf(recording.upload_uniform("uniforms", bytemuck::bytes_of(&uniforms)));
let linepoints_uniforms = [
LinepointsUniforms::new(Color::CYAN, 10.),
LinepointsUniforms::new(Color::RED, 80.),
];
let linepoints_uniforms_buf = recording.upload_uniform(
"linepoints uniforms",
bytemuck::bytes_of(&linepoints_uniforms),
);
recording.draw::<[ResourceProxy; 0]>(self.clear_tint, 1, 4, None, [], target, None);
if params.debug.check_bits(DebugLayers::BOUNDING_BOXES) {
recording.draw(
self.bboxes,
captured.sizes.path_bboxes.len(),
5,
Some(captured.path_bboxes),
[uniforms_buf],
target,
None,
);
}
if params.debug.check_bits(DebugLayers::LINESOUP_SEGMENTS) {
recording.draw(
self.linesoup,
bump.lines,
2,
Some(captured.lines),
[uniforms_buf],
target,
None,
);
}
if params.debug.check_bits(DebugLayers::LINESOUP_POINTS) {
recording.draw(
self.linesoup_points,
bump.lines,
4,
Some(captured.lines),
[
uniforms_buf,
ResourceProxy::BufRange {
proxy: linepoints_uniforms_buf,
offset: 0,
size: std::mem::size_of::<LinepointsUniforms>() as u64,
},
],
target,
None,
);
}
if let Some(unpaired_pts_buf) = unpaired_pts_buf {
recording.draw(
self.unpaired_points,
unpaired_pts_len.try_into().unwrap(),
4,
Some(unpaired_pts_buf),
[
uniforms_buf,
ResourceProxy::BufRange {
proxy: linepoints_uniforms_buf,
offset: std::mem::size_of::<LinepointsUniforms>() as u64,
size: std::mem::size_of::<LinepointsUniforms>() as u64,
},
],
target,
None,
);
recording.free_buf(unpaired_pts_buf);
}
recording.free_resource(uniforms_buf);
recording.free_buf(linepoints_uniforms_buf);
}
}
#[derive(Copy, Clone, Zeroable, Pod)]
#[repr(C)]
struct Uniforms {
width: u32,
height: u32,
}
#[derive(Copy, Clone, Zeroable, Pod)]
#[repr(C)]
struct LinepointsUniforms {
point_color: [f32; 3],
point_size: f32,
// Uniform parameters for individual SDF point draws are stored in a single buffer.
// This 240 byte padding is here to bring the element ffset alignment of 256 bytes.
// (see https://www.w3.org/TR/webgpu/#dom-supported-limits-minuniformbufferoffsetalignment)
_pad0: [u32; 30],
_pad1: [u32; 30],
}
impl LinepointsUniforms {
fn new(color: Color, point_size: f32) -> Self {
Self {
point_color: [
color.r as f32 / 255.,
color.g as f32 / 255.,
color.b as f32 / 255.,
],
point_size,
_pad0: [0; 30],
_pad1: [0; 30],
}
}
}
const SHADERS: &str = r#"
// Map from y-down normalized coordinates to NDC:
fn map_to_ndc(p: vec2f) -> vec4f {
return vec4(vec2(1., -1.) * (2. * p - vec2(1.)), 0., 1.);
}
alias QuadVertices = array<vec2f, 4>;
var<private> quad_vertices: QuadVertices = QuadVertices(
vec2<f32>(0., 1.),
vec2<f32>(0., 0.),
vec2<f32>(1., 0.),
vec2<f32>(1., 1.),
);
var<private> quad_fill_indices: array<u32, 4> = array<u32, 4>(0u, 3u, 1u, 2u);
struct Uniforms {
width: u32,
height: u32,
}
@binding(0) @group(0) var<uniform> uniforms: Uniforms;
struct VSOut {
@builtin(position) pos: vec4f,
@location(0) color: vec4f,
}
////////////
@vertex
fn full_screen_quad_vert(@builtin(vertex_index) vid: u32) -> VSOut {
let p = quad_vertices[quad_fill_indices[vid]];
return VSOut(map_to_ndc(p), vec4(0., 0., 0., 0.5));
}
////////////
struct BboxIn {
@location(0) p0: vec2i,
@location(1) p1: vec2i,
}
@vertex
fn bbox_vert(@builtin(vertex_index) vid: u32, bbox: BboxIn) -> VSOut {
let ul = vec2f(f32(bbox.p0.x), f32(bbox.p0.y));
let br = vec2f(f32(bbox.p1.x), f32(bbox.p1.y));
let dim = br - ul;
let p = (ul + dim * quad_vertices[vid % 4u]) / vec2f(f32(uniforms.width), f32(uniforms.height));
return VSOut(map_to_ndc(p), vec4(0., 1., 0., 1.));
}
////////////
struct LinesoupIn {
@location(0) p0: vec2f,
@location(1) p1: vec2f,
}
@vertex
fn linesoup_vert(@builtin(vertex_index) vid: u32, line: LinesoupIn) -> VSOut {
let p = select(line.p0, line.p1, vid == 1u) / vec2f(f32(uniforms.width), f32(uniforms.height));
return VSOut(map_to_ndc(p), vec4(0.7, 0.5, 0., 1.));
}
////////////
struct LinepointsUniforms {
point_color: vec3f,
point_size: f32,
}
@binding(1) @group(0) var<uniform> linepoints_uniforms: LinepointsUniforms;
struct SDFCircleOut {
@builtin(position) pos: vec4f,
// Unpremultiplied color of the circle.
@location(0) color: vec3f,
// The 2D position of the pixel fragment relative to the center of the quad. The quad edges
// are at coordinates (±1, 0) and (0, ±1).
@location(1) quad_relative: vec2f,
}
@vertex
fn linepoints_vert(@builtin(vertex_index) vid: u32, @location(0) point: vec2f) -> SDFCircleOut {
let quad_corner = quad_vertices[quad_fill_indices[vid]] - vec2(0.5);
let rect_dim = vec2(linepoints_uniforms.point_size);
let p = (point + rect_dim * quad_corner) / vec2(f32(uniforms.width), f32(uniforms.height));
return SDFCircleOut(
map_to_ndc(p),
linepoints_uniforms.point_color,
// Normalize the corners of the quad such that they form a vector of length √2. This should
// align the edge fragments to ±1. The post-interpolation values of `quad_relative` will
// then form a distance field that can represent a circle of radius 1 within the quad
// (where the distance is relative to the center of the circle).
normalize(quad_corner) * sqrt(2.),
);
}
@fragment
fn solid_color_frag(in: VSOut) -> @location(0) vec4f {
return in.color;
}
@fragment
fn sdf_circle_frag(in: SDFCircleOut) -> @location(0) vec4f {
// Draw an antialiased circle with a fading margin as a visual effect. `THRESHOLD` is the
// distance from the center of the circle to the edge where the fade begins.
let THRESHOLD = 0.6;
let d = saturate(length(in.quad_relative));
let alpha = select(1., 1. - smoothstep(THRESHOLD, 1., d), d > THRESHOLD);
return vec4(in.color.rgb, alpha);
}
"#;