use super::commands::WgpuDrawCommand;
use super::raster::{align_to, rasterize_draw_commands_rgba8};
use super::shaders::ShaderModule;
use super::types::Rgba8;
use crate::render::gpu::{GpuCapability, GpuRenderer};
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::mpsc;
use std::time::Duration;
const GPU_READBACK_TIMEOUT: Duration = Duration::from_secs(10);
struct UniformArena {
buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
capacity: u64,
stride: u64,
}
struct TargetCache {
width: u32,
height: u32,
texture: wgpu::Texture,
view: wgpu::TextureView,
}
struct VertexCache {
buffer: wgpu::Buffer,
capacity: u64,
}
struct ReadbackCache {
buffer: wgpu::Buffer,
size: u64,
}
struct BlurTargets {
width: u32,
height: u32,
source: wgpu::Texture,
source_view: wgpu::TextureView,
scratch_view: wgpu::TextureView,
horizontal: wgpu::BindGroup,
vertical: wgpu::BindGroup,
horizontal_params: wgpu::Buffer,
vertical_params: wgpu::Buffer,
}
struct TransformedUniform {
buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
}
#[cfg(feature = "3d")]
struct DepthTarget {
width: u32,
height: u32,
view: wgpu::TextureView,
}
fn create_matrix_uniform(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
label: &str,
) -> TransformedUniform {
let buffer_label = format!("{label}_ubo");
let bind_group_label = format!("{label}_bind_group");
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(buffer_label.as_str()),
size: 64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(bind_group_label.as_str()),
layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &buffer,
offset: 0,
size: wgpu::BufferSize::new(64),
}),
}],
});
TransformedUniform { buffer, bind_group }
}
#[derive(Default)]
struct RenderCache {
uniform: Option<UniformArena>,
target: Option<TargetCache>,
upload_target: Option<TargetCache>,
vertices: Option<VertexCache>,
readback: Option<ReadbackCache>,
blur: Option<BlurTargets>,
transformed_uniform: Option<TransformedUniform>,
#[cfg(feature = "3d")]
depth_target: Option<DepthTarget>,
#[cfg(feature = "3d")]
mesh_uniform: Option<TransformedUniform>,
#[cfg(feature = "3d")]
mesh_indices: Option<VertexCache>,
}
fn grown_buffer_capacity(current: u64, needed: u64, limit: u64) -> u64 {
let grown = current.saturating_add(current / 2).max(needed).max(4);
let rounded = grown.checked_add(65_535).map(|size| size / 65_536 * 65_536).unwrap_or(limit);
rounded.min(limit).max(needed.max(4))
}
impl RenderCache {
fn uniform_stride(device: &wgpu::Device) -> u64 {
let alignment = device.limits().min_uniform_buffer_offset_alignment as u64;
alignment.max(16).next_multiple_of(alignment.max(1))
}
fn ensure_uniform(
&mut self,
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
slots: usize,
) -> Result<(), String> {
let stride = Self::uniform_stride(device);
let needed = stride
.checked_mul(slots.max(1) as u64)
.ok_or_else(|| format!("{slots} colour slots overflow the uniform buffer size"))?;
let needs_new = self.uniform.as_ref().map(|arena| arena.capacity < needed).unwrap_or(true);
if needs_new {
if needed > device.limits().max_buffer_size {
return Err(format!("{needed} uniform bytes exceed the device buffer limit"));
}
let capacity = grown_buffer_capacity(
self.uniform.as_ref().map_or(0, |arena| arena.capacity),
needed,
device.limits().max_buffer_size,
);
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("rect_color_ubo"),
size: capacity,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("rect_bind_group"),
layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &buffer,
offset: 0,
size: wgpu::BufferSize::new(16),
}),
}],
});
self.uniform = Some(UniformArena { buffer, bind_group, capacity, stride });
}
Ok(())
}
fn ensure_target(&mut self, device: &wgpu::Device, width: u32, height: u32) {
let matches = self
.target
.as_ref()
.map(|target| target.width == width && target.height == height)
.unwrap_or(false);
if !matches {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("wgpu_offscreen_target"),
size: wgpu::Extent3d { width, height, 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,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
self.target = Some(TargetCache { width, height, texture, view });
}
}
fn ensure_upload_target(&mut self, device: &wgpu::Device, width: u32, height: u32) {
let matches = self
.upload_target
.as_ref()
.map(|target| target.width == width && target.height == height)
.unwrap_or(false);
if !matches {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("rw_wgpu_offscreen_texture"),
size: wgpu::Extent3d { width, height, 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::COPY_DST,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
self.upload_target = Some(TargetCache { width, height, texture, view });
}
}
fn ensure_vertices(&mut self, device: &wgpu::Device, bytes: u64) {
let needs_new = self.vertices.as_ref().map(|cache| cache.capacity < bytes).unwrap_or(true);
if needs_new {
let capacity = grown_buffer_capacity(
self.vertices.as_ref().map_or(0, |cache| cache.capacity),
bytes,
device.limits().max_buffer_size,
);
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("rect_vertex_buffer"),
size: capacity,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.vertices = Some(VertexCache { buffer, capacity });
}
}
fn ensure_readback(&mut self, device: &wgpu::Device, size: u64) {
let needs_new = self.readback.as_ref().map(|cache| cache.size < size).unwrap_or(true);
if needs_new {
let capacity = grown_buffer_capacity(
self.readback.as_ref().map_or(0, |cache| cache.size),
size,
device.limits().max_buffer_size,
);
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("wgpu_readback"),
size: capacity,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
self.readback = Some(ReadbackCache { buffer, size: capacity });
}
}
fn ensure_blur_targets(
&mut self,
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
width: u32,
height: u32,
) {
let matches = self
.blur
.as_ref()
.map(|targets| targets.width == width && targets.height == height)
.unwrap_or(false);
if matches {
return;
}
let make_texture = |label: &str| {
device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::COPY_DST,
view_formats: &[],
})
};
let source = make_texture("wgpu_blur_source");
let scratch = make_texture("wgpu_blur_scratch");
let source_view = source.create_view(&wgpu::TextureViewDescriptor::default());
let scratch_view = scratch.create_view(&wgpu::TextureViewDescriptor::default());
let make_params = |label: &str| {
device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size: 16,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
};
let horizontal_params = make_params("wgpu_blur_horizontal_params");
let vertical_params = make_params("wgpu_blur_vertical_params");
let make_bind_group = |label: &str, view: &wgpu::TextureView, buffer: &wgpu::Buffer| {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(label),
layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer,
offset: 0,
size: wgpu::BufferSize::new(16),
}),
},
],
})
};
let horizontal = make_bind_group("wgpu_blur_horizontal", &source_view, &horizontal_params);
let vertical = make_bind_group("wgpu_blur_vertical", &scratch_view, &vertical_params);
self.blur = Some(BlurTargets {
width,
height,
source,
source_view,
scratch_view,
horizontal,
vertical,
horizontal_params,
vertical_params,
});
}
fn ensure_transformed_uniform(
&mut self,
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
) {
if self.transformed_uniform.is_some() {
return;
}
self.transformed_uniform = Some(create_matrix_uniform(device, layout, "transformed_mvp"));
}
#[cfg(feature = "3d")]
fn ensure_mesh_uniform(&mut self, device: &wgpu::Device, layout: &wgpu::BindGroupLayout) {
if self.mesh_uniform.is_some() {
return;
}
self.mesh_uniform = Some(create_matrix_uniform(device, layout, "mesh_mvp"));
}
#[cfg(feature = "3d")]
fn ensure_depth_target(&mut self, device: &wgpu::Device, width: u32, height: u32) {
let matches = self
.depth_target
.as_ref()
.map(|target| target.width == width && target.height == height)
.unwrap_or(false);
if matches {
return;
}
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("mesh_depth_texture"),
size: wgpu::Extent3d {
width: width.max(1),
height: height.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth32Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
self.depth_target = Some(DepthTarget { width, height, view });
}
#[cfg(feature = "3d")]
fn ensure_mesh_indices(&mut self, device: &wgpu::Device, bytes: u64) {
if self.mesh_indices.as_ref().map(|cache| cache.capacity >= bytes).unwrap_or(false) {
return;
}
let capacity = bytes.max(256).next_power_of_two();
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("mesh_indices"),
size: capacity,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.mesh_indices = Some(VertexCache { buffer, capacity });
}
}
fn rgba_buffer_len(width: u32, height: u32) -> Result<usize, String> {
if width == 0 || height == 0 {
return Err(format!("width/height must be > 0, got {width}x{height}"));
}
let pixels = (width as usize)
.checked_mul(height as usize)
.ok_or_else(|| format!("image {width}x{height} pixel count overflows usize"))?;
let bytes = pixels
.checked_mul(4)
.ok_or_else(|| format!("image {width}x{height} RGBA size overflows usize"))?;
if bytes > u32::MAX as usize {
return Err(format!(
"image {width}x{height} needs {bytes} bytes, which exceeds the largest supported \
framebuffer ({} bytes)",
u32::MAX
));
}
Ok(bytes)
}
fn padded_row_bytes(width: u32) -> Result<u32, String> {
let unpadded = width
.checked_mul(4)
.ok_or_else(|| format!("row byte width for {width} pixels overflows u32"))?;
Ok(align_to(unpadded, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT))
}
fn readback_frame_bytes(width: u32, height: u32) -> Result<u64, String> {
let padded = padded_row_bytes(width)?;
(padded as u64)
.checked_mul(height as u64)
.ok_or_else(|| format!("readback buffer for {width}x{height} overflows u64"))
}
fn tint_blurred_frame(frame: &mut [u8], tint: Rgba8, weight: f32) {
if !weight.is_finite() {
return;
}
let weight = weight.clamp(0.0, 1.0);
let tint_alpha = (tint.a as f32 / 255.0) * weight;
if tint_alpha <= 0.0 {
return;
}
let tint_rgb = [tint.r as f32 / 255.0, tint.g as f32 / 255.0, tint.b as f32 / 255.0];
for pixel in frame.as_chunks_mut::<4>().0 {
let dst_alpha = pixel[3] as f32 / 255.0;
let out_alpha = tint_alpha + dst_alpha * (1.0 - tint_alpha);
if out_alpha <= f32::EPSILON {
pixel.fill(0);
continue;
}
for channel in 0..3 {
let dst = pixel[channel] as f32 / 255.0;
let blended =
(tint_rgb[channel] * tint_alpha + dst * dst_alpha * (1.0 - tint_alpha)) / out_alpha;
pixel[channel] = (blended * 255.0).round().clamp(0.0, 255.0) as u8;
}
pixel[3] = (out_alpha * 255.0).round().clamp(0.0, 255.0) as u8;
}
}
#[cfg(feature = "3d")]
#[allow(clippy::needless_range_loop)] fn mvp_column_major_bytes(m: &crate::render::math3d::Mat4) -> [u8; 64] {
let mut bytes = [0u8; 64];
for column in 0..4 {
for row in 0..4 {
let value = m.m[row * 4 + column];
let start = (column * 4 + row) * 4;
bytes[start..start + 4].copy_from_slice(&value.to_le_bytes());
}
}
bytes
}
const INSTANCE_BYTES: usize = 48;
const MAX_INSTANCES_PER_DRAW: usize = 4096;
fn ortho_mvp_bytes(width: u32, height: u32) -> [u8; 64] {
let w = width as f32;
let h = height as f32;
let values =
[2.0 / w, 0.0, 0.0, 0.0, 0.0, -2.0 / h, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, -1.0, 1.0, 0.0, 1.0];
let mut bytes = [0u8; 64];
for (index, value) in values.iter().enumerate() {
bytes[index * 4..index * 4 + 4].copy_from_slice(&value.to_le_bytes());
}
bytes
}
fn instanced_quad_bytes(quads: &[([crate::core::Point; 4], [f64; 4])]) -> Result<Vec<u8>, String> {
let capacity = quads
.len()
.checked_mul(INSTANCE_BYTES)
.ok_or_else(|| format!("{} quads overflow the instance buffer size", quads.len()))?;
let mut bytes = Vec::with_capacity(capacity);
for (corners, colour) in quads {
for point in corners {
bytes.extend_from_slice(&(point.x as f32).to_le_bytes());
bytes.extend_from_slice(&(point.y as f32).to_le_bytes());
}
for channel in colour {
bytes.extend_from_slice(&((*channel).clamp(0.0, 1.0) as f32).to_le_bytes());
}
}
Ok(bytes)
}
#[cfg(feature = "3d")]
fn mesh_vertex_bytes(mesh: &crate::render::mesh3d::Mesh3D) -> Vec<u8> {
let mut bytes = Vec::with_capacity(mesh.vertices().len() * 32);
for vertex in mesh.vertices() {
for value in [vertex.position.x, vertex.position.y, vertex.position.z, 1.0] {
bytes.extend_from_slice(&value.to_le_bytes());
}
for channel in [vertex.color.r, vertex.color.g, vertex.color.b, vertex.color.a] {
bytes.extend_from_slice(&((channel as f32) / 255.0).to_le_bytes());
}
}
bytes
}
#[cfg(feature = "3d")]
fn mesh_index_bytes(mesh: &crate::render::mesh3d::Mesh3D) -> Vec<u8> {
let mut bytes = Vec::with_capacity(mesh.indices().len() * 4);
for index in mesh.indices() {
bytes.extend_from_slice(&index.to_le_bytes());
}
bytes
}
pub struct WgpuRenderer {
device: wgpu::Device,
queue: wgpu::Queue,
clear_pipeline: wgpu::RenderPipeline,
rect_pipeline: wgpu::RenderPipeline,
blur_pipeline: wgpu::RenderPipeline,
bind_group_layout: wgpu::BindGroupLayout,
blur_bind_group_layout: wgpu::BindGroupLayout,
transformed_pipeline: wgpu::RenderPipeline,
transformed_bind_group_layout: wgpu::BindGroupLayout,
#[cfg(feature = "3d")]
mesh_pipeline: wgpu::RenderPipeline,
shader_cache: HashMap<String, wgpu::ShaderModule>,
cache: RefCell<RenderCache>,
}
impl WgpuRenderer {
pub fn new() -> Result<Self, String> {
pollster::block_on(Self::new_async())
}
async fn new_async() -> Result<Self, String> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: crate::gpu::backend_ladder::instance_backends(),
flags: wgpu::InstanceFlags::default(),
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
backend_options: wgpu::BackendOptions::default(),
display: None,
});
let (adapter, tier) = crate::gpu::backend_ladder::select_adapter_with_gl_fallback(
&instance,
wgpu::PowerPreference::HighPerformance,
None,
)
.await
.ok_or_else(|| {
match crate::gpu::backend_ladder::backends_from_env() {
Some(pinned) => format!(
"wgpu adapter request failed: WGPU_BACKEND pinned {pinned:?}, \
which this host does not provide"
),
None => "wgpu adapter request failed on every backend tier".to_string(),
}
})?;
let adapter_info = adapter.get_info();
if tier.is_degraded() {
log::warn!(
"[wgpu] using degraded backend tier: {} ({:?}, adapter '{}')",
tier.label(),
adapter_info.backend,
adapter_info.name,
);
} else {
log::info!(
"[wgpu] using {} backend ({:?}, adapter '{}')",
tier.label(),
adapter_info.backend,
adapter_info.name,
);
}
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("rw_wgpu_device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
memory_hints: wgpu::MemoryHints::Performance,
experimental_features: wgpu::ExperimentalFeatures::disabled(),
trace: wgpu::Trace::Off,
})
.await
.map_err(|error| {
format!(
"the GPU device could not be requested: {error}; the adapter may have been \
lost or the requested limits unsupported"
)
})?;
let clear_vs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("clear_vs"),
source: wgpu::ShaderSource::Wgsl(super::shaders::FULLSCREEN_QUAD_VS.into()),
});
let clear_fs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("clear_fs"),
source: wgpu::ShaderSource::Wgsl(super::shaders::CLEAR_FS.into()),
});
let rect_vs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("rect_vs"),
source: wgpu::ShaderSource::Wgsl(super::shaders::FILL_RECT_VS.into()),
});
let rect_fs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("rect_fs"),
source: wgpu::ShaderSource::Wgsl(super::shaders::FILL_RECT_FS.into()),
});
let transformed_vs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("rect_vs_transformed"),
source: wgpu::ShaderSource::Wgsl(super::shaders::FILL_RECT_VS_TRANSFORMED.into()),
});
let instanced_fs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("rect_fs_instanced"),
source: wgpu::ShaderSource::Wgsl(super::shaders::FILL_RECT_INSTANCED_FS.into()),
});
#[cfg(feature = "3d")]
let mesh_vs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("mesh3d_vs"),
source: wgpu::ShaderSource::Wgsl(super::shaders::MESH3D_VS.into()),
});
#[cfg(feature = "3d")]
let mesh_fs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("mesh3d_fs"),
source: wgpu::ShaderSource::Wgsl(super::shaders::MESH3D_FS.into()),
});
let blur_vs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("blur_vs"),
source: wgpu::ShaderSource::Wgsl(super::shaders::BLUR_VS.into()),
});
let blur_fs_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("blur_fs"),
source: wgpu::ShaderSource::Wgsl(super::shaders::BLUR_FS.into()),
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("uniform_color_bind_group_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: Some(wgpu::BufferSize::new(16).unwrap()),
},
count: None,
}],
});
let transformed_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("transformed_mvp_bind_group_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: Some(wgpu::BufferSize::new(64).unwrap()),
},
count: None,
}],
});
let clear_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("clear_pipeline_layout"),
bind_group_layouts: &[Some(&bind_group_layout)],
immediate_size: 0,
});
let clear_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("clear_pipeline"),
layout: Some(&clear_pipeline_layout),
vertex: wgpu::VertexState {
module: &clear_vs_module,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &clear_fs_module,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8Unorm,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: Some(wgpu::Face::Back),
unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
});
let rect_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("rect_pipeline_layout"),
bind_group_layouts: &[Some(&bind_group_layout)],
immediate_size: 0,
});
let rect_vertex_buffer_layout = wgpu::VertexBufferLayout {
array_stride: 8, step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x2,
offset: 0,
shader_location: 0,
}],
};
let rect_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("rect_pipeline"),
layout: Some(&rect_pipeline_layout),
vertex: wgpu::VertexState {
module: &rect_vs_module,
entry_point: Some("vs_main"),
buffers: &[Some(rect_vertex_buffer_layout)],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &rect_fs_module,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8Unorm,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Cw,
cull_mode: Some(wgpu::Face::Back),
unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
});
let blur_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("blur_bind_group_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: Some(wgpu::BufferSize::new(16).unwrap()),
},
count: None,
},
],
});
let blur_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("blur_pipeline_layout"),
bind_group_layouts: &[Some(&blur_bind_group_layout)],
immediate_size: 0,
});
let blur_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("blur_pipeline"),
layout: Some(&blur_pipeline_layout),
vertex: wgpu::VertexState {
module: &blur_vs_module,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &blur_fs_module,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8Unorm,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
});
let transformed_pipeline = {
let transformed_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("rect_transformed_pipeline_layout"),
bind_group_layouts: &[Some(&transformed_bind_group_layout)],
immediate_size: 0,
});
let instance_buffer_layout = wgpu::VertexBufferLayout {
array_stride: INSTANCE_BYTES as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x4,
offset: 0,
shader_location: 0,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x4,
offset: 16,
shader_location: 1,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x4,
offset: 32,
shader_location: 2,
},
],
};
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("rect_instanced_pipeline"),
layout: Some(&transformed_pipeline_layout),
vertex: wgpu::VertexState {
module: &transformed_vs_module,
entry_point: Some("vs_main"),
buffers: &[Some(instance_buffer_layout)],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &instanced_fs_module,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8Unorm,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Cw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
})
};
#[cfg(feature = "3d")]
let mesh_pipeline = {
let mesh_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("mesh_pipeline_layout"),
bind_group_layouts: &[Some(&transformed_bind_group_layout)],
immediate_size: 0,
});
let vertex_buffer_layout = wgpu::VertexBufferLayout {
array_stride: 32,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x4,
offset: 0,
shader_location: 0,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x4,
offset: 16,
shader_location: 1,
},
],
};
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("mesh_pipeline"),
layout: Some(&mesh_pipeline_layout),
vertex: wgpu::VertexState {
module: &mesh_vs_module,
entry_point: Some("vs_main"),
buffers: &[Some(vertex_buffer_layout)],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &mesh_fs_module,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8Unorm,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::Less),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
})
};
let mut shader_cache = HashMap::new();
shader_cache.insert("clear_vs".to_string(), clear_vs_module);
shader_cache.insert("clear_fs".to_string(), clear_fs_module);
shader_cache.insert("rect_vs".to_string(), rect_vs_module);
shader_cache.insert("rect_fs".to_string(), rect_fs_module);
shader_cache.insert("blur_vs".to_string(), blur_vs_module);
shader_cache.insert("blur_fs".to_string(), blur_fs_module);
shader_cache.insert("rect_vs_transformed".to_string(), transformed_vs_module);
shader_cache.insert("rect_fs_instanced".to_string(), instanced_fs_module);
#[cfg(feature = "3d")]
shader_cache.insert("mesh3d_vs".to_string(), mesh_vs_module);
#[cfg(feature = "3d")]
shader_cache.insert("mesh3d_fs".to_string(), mesh_fs_module);
Ok(Self {
device,
queue,
clear_pipeline,
rect_pipeline,
blur_pipeline,
bind_group_layout,
blur_bind_group_layout,
transformed_pipeline,
transformed_bind_group_layout,
#[cfg(feature = "3d")]
mesh_pipeline,
shader_cache,
cache: RefCell::new(RenderCache::default()),
})
}
pub fn render_clear_rgba8(
&self,
width: u32,
height: u32,
color: [f64; 4],
) -> Result<Vec<u8>, String> {
rasterize_draw_commands_rgba8(
width,
height,
&[WgpuDrawCommand::Clear {
color: Rgba8 {
r: (color[0].clamp(0.0, 1.0) * 255.0) as u8,
g: (color[1].clamp(0.0, 1.0) * 255.0) as u8,
b: (color[2].clamp(0.0, 1.0) * 255.0) as u8,
a: (color[3].clamp(0.0, 1.0) * 255.0) as u8,
},
}],
)
}
pub fn render_clear_gpu(
&self,
width: u32,
height: u32,
color: [f64; 4],
) -> Result<Vec<u8>, String> {
let expected_len = rgba_buffer_len(width, height)?;
let mut colour_bytes = [0u8; 16];
for (channel, value) in color.iter().enumerate() {
let clamped = value.clamp(0.0, 1.0) as f32;
colour_bytes[channel * 4..channel * 4 + 4].copy_from_slice(&clamped.to_le_bytes());
}
let frame_bytes = readback_frame_bytes(width, height)?;
let mut cache = self.cache.borrow_mut();
cache.ensure_uniform(&self.device, &self.bind_group_layout, 1)?;
cache.ensure_target(&self.device, width, height);
cache.ensure_readback(&self.device, frame_bytes);
let uniform = cache.uniform.as_ref().expect("the arena was just ensured");
let target = cache.target.as_ref().expect("the target was just ensured");
let readback = cache.readback.as_ref().expect("the readback was just ensured");
self.queue.write_buffer(&uniform.buffer, 0, &colour_bytes);
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("gpu_clear_encoder"),
});
{
let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("gpu_clear_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &target.view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store },
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
rp.set_pipeline(&self.clear_pipeline);
rp.set_bind_group(0, &uniform.bind_group, &[0]);
rp.draw(0..3, 0..1); }
let padded_bytes_per_row = padded_row_bytes(width)?;
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &target.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback.buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded_bytes_per_row),
rows_per_image: Some(height),
},
},
wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
);
self.queue.submit(Some(encoder.finish()));
self.read_back(&readback.buffer, width, height, expected_len)
}
fn encode_fill_rects(
&self,
width: u32,
height: u32,
rects: &[(i32, i32, u32, u32, [f64; 4])],
) -> Result<(), String> {
if rects.len() <= 1 {
self.encode_fill_rects_per_rect(width, height, rects)
} else {
self.encode_fill_rects_instanced(width, height, rects)
}
}
fn encode_fill_rects_per_rect(
&self,
width: u32,
height: u32,
rects: &[(i32, i32, u32, u32, [f64; 4])],
) -> Result<(), String> {
let w = width as f32;
let h = height as f32;
let mut vertex_bytes: Vec<u8> = Vec::with_capacity(rects.len() * 48);
let mut colours: Vec<[f32; 4]> = Vec::with_capacity(rects.len());
for &(x, y, rw, rh, color) in rects {
let x0 = (x as f32 / w) * 2.0 - 1.0;
let y0 = 1.0 - (y as f32 / h) * 2.0;
let x1 = ((x.saturating_add(rw as i32)) as f32 / w) * 2.0 - 1.0;
let y1 = 1.0 - ((y.saturating_add(rh as i32)) as f32 / h) * 2.0;
for value in [x0, y0, x1, y0, x0, y1, x1, y0, x1, y1, x0, y1] {
vertex_bytes.extend_from_slice(&value.to_le_bytes());
}
colours.push([
color[0].clamp(0.0, 1.0) as f32,
color[1].clamp(0.0, 1.0) as f32,
color[2].clamp(0.0, 1.0) as f32,
color[3].clamp(0.0, 1.0) as f32,
]);
}
let mut cache = self.cache.borrow_mut();
cache.ensure_uniform(&self.device, &self.bind_group_layout, colours.len())?;
cache.ensure_target(&self.device, width, height);
cache.ensure_vertices(&self.device, vertex_bytes.len() as u64);
let uniform = cache.uniform.as_ref().expect("the arena was just ensured");
let target = cache.target.as_ref().expect("the target was just ensured");
let vertices = cache.vertices.as_ref().expect("the vertices were just ensured");
let stride = uniform.stride as usize;
let mut uniform_bytes = vec![0u8; stride * colours.len()];
for (slot, colour) in colours.iter().enumerate() {
let start = slot * stride;
for (channel, value) in colour.iter().enumerate() {
uniform_bytes[start + channel * 4..start + channel * 4 + 4]
.copy_from_slice(&value.to_le_bytes());
}
}
self.queue.write_buffer(&uniform.buffer, 0, &uniform_bytes);
self.queue.write_buffer(&vertices.buffer, 0, &vertex_bytes);
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("gpu_rect_encoder"),
});
{
let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("gpu_rect_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &target.view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
rp.set_pipeline(&self.rect_pipeline);
rp.set_vertex_buffer(0, vertices.buffer.slice(..vertex_bytes.len() as u64));
for index in 0..colours.len() {
let offset = (index * stride) as u32;
rp.set_bind_group(0, &uniform.bind_group, &[offset]);
let start = (index * 6) as u32;
let end = ((index + 1) * 6) as u32;
rp.draw(start..end, 0..1);
}
}
self.queue.submit(Some(encoder.finish()));
Ok(())
}
fn encode_fill_rects_instanced(
&self,
width: u32,
height: u32,
rects: &[(i32, i32, u32, u32, [f64; 4])],
) -> Result<(), String> {
let mut quads: Vec<([crate::core::Point; 4], [f64; 4])> = Vec::with_capacity(rects.len());
for &(x, y, rw, rh, colour) in rects {
let x1 = x.saturating_add(rw as i32);
let y1 = y.saturating_add(rh as i32);
quads.push((
[
crate::core::Point::new(x, y),
crate::core::Point::new(x1, y),
crate::core::Point::new(x1, y1),
crate::core::Point::new(x, y1),
],
colour,
));
}
let mvp_bytes = ortho_mvp_bytes(width, height);
self.encode_instances(width, height, &mvp_bytes, &quads)
}
fn encode_instances(
&self,
width: u32,
height: u32,
mvp_bytes: &[u8; 64],
quads: &[([crate::core::Point; 4], [f64; 4])],
) -> Result<(), String> {
let instance_bytes = instanced_quad_bytes(quads)?;
let mut cache = self.cache.borrow_mut();
cache.ensure_target(&self.device, width, height);
cache.ensure_vertices(&self.device, instance_bytes.len() as u64);
cache.ensure_transformed_uniform(&self.device, &self.transformed_bind_group_layout);
let target = cache.target.as_ref().expect("the target was just ensured");
let instances = cache.vertices.as_ref().expect("the instance buffer was just ensured");
let transformed = cache.transformed_uniform.as_ref().expect("the mvp was just ensured");
self.queue.write_buffer(&transformed.buffer, 0, mvp_bytes);
if !instance_bytes.is_empty() {
self.queue.write_buffer(&instances.buffer, 0, &instance_bytes);
}
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("gpu_quads_encoder"),
});
{
let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("gpu_quads_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &target.view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
rp.set_pipeline(&self.transformed_pipeline);
rp.set_bind_group(0, &transformed.bind_group, &[]);
for (chunk_index, chunk) in quads.chunks(MAX_INSTANCES_PER_DRAW).enumerate() {
let start = chunk_index * MAX_INSTANCES_PER_DRAW * INSTANCE_BYTES;
let end = start + chunk.len() * INSTANCE_BYTES;
rp.set_vertex_buffer(0, instances.buffer.slice(start as u64..end as u64));
rp.draw(0..6, 0..chunk.len() as u32);
}
}
self.queue.submit(Some(encoder.finish()));
Ok(())
}
#[cfg(feature = "3d")]
fn encode_quads_instanced(
&self,
width: u32,
height: u32,
affine: &crate::render::math3d::Mat3,
quads: &[([crate::core::Point; 4], [f64; 4])],
) -> Result<(), String> {
let w = width as f32;
let h = height as f32;
let ortho = crate::render::math3d::Mat3::scaling(2.0 / w, -2.0 / h)
.then(&crate::render::math3d::Mat3::translation(-1.0, 1.0));
let mvp = affine.then(&ortho);
let mvp_bytes = mvp_column_major_bytes(&crate::render::math3d::Mat4::from_affine(mvp));
self.encode_instances(width, height, &mvp_bytes, quads)
}
pub fn with_fill_rect_texture<R>(
&self,
width: u32,
height: u32,
rects: &[(i32, i32, u32, u32, [f64; 4])],
f: impl FnOnce(&wgpu::TextureView) -> R,
) -> Result<R, String> {
rgba_buffer_len(width, height)?;
if rects.is_empty() {
return Err("with_fill_rect_texture needs at least one rectangle".to_string());
}
self.encode_fill_rects(width, height, rects)?;
let cache = self.cache.borrow();
let target = cache.target.as_ref().ok_or_else(|| "render target missing".to_string())?;
Ok(f(&target.view))
}
pub fn render_fill_rect_gpu(
&self,
width: u32,
height: u32,
rects: &[(i32, i32, u32, u32, [f64; 4])],
) -> Result<Vec<u8>, String> {
let expected_len = rgba_buffer_len(width, height)?;
if rects.is_empty() {
return Ok(vec![0u8; expected_len]);
}
self.encode_fill_rects(width, height, rects)?;
let frame_bytes = readback_frame_bytes(width, height)?;
let padded_bytes_per_row = padded_row_bytes(width)?;
let mut cache = self.cache.borrow_mut();
cache.ensure_readback(&self.device, frame_bytes);
let target = cache.target.as_ref().expect("the target was just ensured");
let readback = cache.readback.as_ref().expect("the readback was just ensured");
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("gpu_rect_readback"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &target.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback.buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded_bytes_per_row),
rows_per_image: Some(height),
},
},
wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
);
self.queue.submit(Some(encoder.finish()));
self.read_back(&readback.buffer, width, height, expected_len)
}
#[cfg(feature = "3d")]
pub fn render_fill_quads_gpu(
&self,
width: u32,
height: u32,
affine: &crate::render::math3d::Mat3,
quads: &[([crate::core::Point; 4], [f64; 4])],
) -> Result<Vec<u8>, String> {
let expected_len = rgba_buffer_len(width, height)?;
if quads.is_empty() {
return Ok(vec![0u8; expected_len]);
}
self.encode_quads_instanced(width, height, affine, quads)?;
let frame_bytes = readback_frame_bytes(width, height)?;
let padded_bytes_per_row = padded_row_bytes(width)?;
let mut cache = self.cache.borrow_mut();
cache.ensure_readback(&self.device, frame_bytes);
let target = cache.target.as_ref().expect("the target was just ensured");
let readback = cache.readback.as_ref().expect("the readback was just ensured");
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("gpu_quads_readback"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &target.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback.buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded_bytes_per_row),
rows_per_image: Some(height),
},
},
wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
);
self.queue.submit(Some(encoder.finish()));
self.read_back(&readback.buffer, width, height, expected_len)
}
#[cfg(feature = "3d")]
pub fn render_mesh_3d_gpu(
&self,
width: u32,
height: u32,
mesh: &crate::render::mesh3d::Mesh3D,
model: &crate::render::math3d::Mat4,
camera: &crate::render::mesh3d::Camera,
viewport: crate::core::Rect,
) -> Result<Vec<u8>, String> {
let expected_len = rgba_buffer_len(width, height)?;
if mesh.triangle_count() == 0 || viewport.width == 0 || viewport.height == 0 {
return Ok(vec![0u8; expected_len]);
}
let aspect = viewport.width as f32 / viewport.height as f32;
let mvp = model.then(&camera.view_projection(aspect)).then(
&crate::render::mesh3d::viewport_matrix(
viewport,
crate::core::Size::new(width, height),
),
);
let mvp_bytes = mvp_column_major_bytes(&mvp);
let vertex_bytes = mesh_vertex_bytes(mesh);
let index_bytes = mesh_index_bytes(mesh);
let index_count = mesh.indices().len() as u32;
if index_count == 0 {
return Ok(vec![0u8; expected_len]);
}
let mut cache = self.cache.borrow_mut();
cache.ensure_target(&self.device, width, height);
cache.ensure_depth_target(&self.device, width, height);
cache.ensure_vertices(&self.device, vertex_bytes.len() as u64);
cache.ensure_mesh_indices(&self.device, index_bytes.len() as u64);
cache.ensure_mesh_uniform(&self.device, &self.transformed_bind_group_layout);
let target = cache.target.as_ref().ok_or_else(|| "render target missing".to_string())?;
let depth =
cache.depth_target.as_ref().ok_or_else(|| "depth target missing".to_string())?;
let vertices =
cache.vertices.as_ref().ok_or_else(|| "vertex buffer missing".to_string())?;
let indices =
cache.mesh_indices.as_ref().ok_or_else(|| "index buffer missing".to_string())?;
let uniform =
cache.mesh_uniform.as_ref().ok_or_else(|| "mvp uniform missing".to_string())?;
self.queue.write_buffer(&uniform.buffer, 0, &mvp_bytes);
self.queue.write_buffer(&vertices.buffer, 0, &vertex_bytes);
self.queue.write_buffer(&indices.buffer, 0, &index_bytes);
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("gpu_mesh_encoder"),
});
{
let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("gpu_mesh_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &target.view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &depth.view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
rp.set_pipeline(&self.mesh_pipeline);
rp.set_bind_group(0, &uniform.bind_group, &[]);
rp.set_vertex_buffer(0, vertices.buffer.slice(..vertex_bytes.len() as u64));
rp.set_index_buffer(
indices.buffer.slice(..index_bytes.len() as u64),
wgpu::IndexFormat::Uint32,
);
rp.draw_indexed(0..index_count, 0, 0..1);
}
drop(cache);
self.queue.submit(Some(encoder.finish()));
self.read_target(width, height, expected_len, "gpu_mesh_readback")
}
#[cfg(feature = "3d")]
fn read_target(
&self,
width: u32,
height: u32,
expected_len: usize,
label: &str,
) -> Result<Vec<u8>, String> {
let frame_bytes = readback_frame_bytes(width, height)?;
let padded_bytes_per_row = padded_row_bytes(width)?;
let mut cache = self.cache.borrow_mut();
cache.ensure_readback(&self.device, frame_bytes);
let target = cache.target.as_ref().ok_or_else(|| "render target missing".to_string())?;
let readback =
cache.readback.as_ref().ok_or_else(|| "readback buffer missing".to_string())?;
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some(label) });
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &target.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback.buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded_bytes_per_row),
rows_per_image: Some(height),
},
},
wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
);
self.queue.submit(Some(encoder.finish()));
self.read_back(&readback.buffer, width, height, expected_len)
}
fn read_back(
&self,
buffer: &wgpu::Buffer,
width: u32,
height: u32,
expected_len: usize,
) -> Result<Vec<u8>, String> {
let unpadded = width
.checked_mul(4)
.ok_or_else(|| format!("row byte width for {width} pixels overflows u32"))?
as usize;
let padded = padded_row_bytes(width)? as usize;
let buffer_slice = buffer.slice(..(padded * height as usize) as u64);
let (sender, receiver) = mpsc::channel();
buffer_slice.map_async(
wgpu::MapMode::Read,
move |result: Result<(), wgpu::BufferAsyncError>| {
let _ = sender.send(result);
},
);
if let Err(error) = self.device.poll(wgpu::PollType::Wait {
submission_index: None,
timeout: Some(GPU_READBACK_TIMEOUT),
}) {
buffer.unmap();
return Err(format!(
"GPU readback did not complete within {GPU_READBACK_TIMEOUT:?}: {error}"
));
}
let map_result = match receiver.recv_timeout(GPU_READBACK_TIMEOUT) {
Ok(result) => result,
Err(error) => {
buffer.unmap();
return Err(format!(
"GPU map callback did not arrive within {GPU_READBACK_TIMEOUT:?}: {error}"
));
}
};
if let Err(error) = map_result {
buffer.unmap();
return Err(format!("GPU buffer could not be mapped for CPU readback: {error:?}"));
}
let mapped = match buffer_slice.get_mapped_range() {
Ok(mapped) => mapped,
Err(error) => {
buffer.unmap();
return Err(format!("mapped GPU buffer range could not be read back: {error:?}"));
}
};
let mut pixels = vec![0u8; expected_len];
for row in 0..height as usize {
let src_start = row * padded;
let dst_start = row * unpadded;
pixels[dst_start..dst_start + unpadded]
.copy_from_slice(&mapped[src_start..src_start + unpadded]);
}
drop(mapped);
buffer.unmap();
Ok(pixels)
}
#[allow(clippy::type_complexity)]
pub fn render_stroke_rect_gpu(
&self,
width: u32,
height: u32,
rects: &[((i32, i32, u32, u32, u32), [f64; 4])],
) -> Result<Vec<u8>, String> {
let expected_len = rgba_buffer_len(width, height)?;
if rects.is_empty() {
return Ok(vec![0u8; expected_len]);
}
let mut fill_rects: Vec<((i32, i32, u32, u32), [f64; 4])> =
Vec::with_capacity(rects.len() * 4);
for &((x, y, rw, rh, sw), color) in rects {
let sw_i = sw as i32;
fill_rects.push(((x, y, rw, sw), color));
fill_rects.push(((x, y + rh as i32 - sw_i, rw, sw), color));
let inner_h = rh.saturating_sub(sw * 2);
if inner_h > 0 {
fill_rects.push(((x, y + sw_i, sw, inner_h), color));
}
if inner_h > 0 {
fill_rects.push(((x + rw as i32 - sw_i, y + sw_i, sw, inner_h), color));
}
}
let rect_input: Vec<(i32, i32, u32, u32, [f64; 4])> =
fill_rects.into_iter().map(|((x, y, w, h), color)| (x, y, w, h, color)).collect();
self.render_fill_rect_gpu(width, height, &rect_input)
}
#[allow(clippy::type_complexity)]
pub fn render_draw_line_gpu(
&self,
width: u32,
height: u32,
lines: &[((i32, i32, i32, i32, u32), [f64; 4])],
) -> Result<Vec<u8>, String> {
let expected_len = rgba_buffer_len(width, height)?;
if lines.is_empty() {
return Ok(vec![0u8; expected_len]);
}
let mut fill_rects: Vec<(i32, i32, u32, u32, [f64; 4])> = Vec::with_capacity(lines.len());
for &((x1, y1, x2, y2, lw), color) in lines {
let lw_i = lw as i32;
let dx = (x2 - x1).abs();
let dy = (y2 - y1).abs();
if dx >= dy {
let min_x = x1.min(x2);
let line_len = dx.max(1) as u32;
fill_rects.push((min_x, y1 - lw_i / 2, line_len, lw, color));
} else {
let min_y = y1.min(y2);
let line_len = dy.max(1) as u32;
fill_rects.push((x1 - lw_i / 2, min_y, lw, line_len, color));
}
}
self.render_fill_rect_gpu(width, height, &fill_rects)
}
#[allow(clippy::type_complexity)]
pub fn render_fill_circle_gpu(
&self,
width: u32,
height: u32,
circles: &[((i32, i32, u32), [f64; 4])], ) -> Result<Vec<u8>, String> {
let expected_len = rgba_buffer_len(width, height)?;
if circles.is_empty() {
return Ok(vec![0u8; expected_len]);
}
let mut fill_rects: Vec<(i32, i32, u32, u32, [f64; 4])> = Vec::new();
for &((cx, cy, radius), color) in circles {
let r = radius as i32;
for dx in -r..=r {
let half_height = ((r * r - dx * dx) as f64).sqrt() as i32;
let x = cx + dx;
let y1 = cy - half_height;
let strip_height = (half_height * 2) as u32;
if strip_height > 0 {
fill_rects.push((x, y1, 1, strip_height, color));
}
}
}
let rects: Vec<(i32, i32, u32, u32, [f64; 4])> = fill_rects;
self.render_fill_rect_gpu(width, height, &rects)
}
pub fn render_draw_commands_rgba8(
&self,
width: u32,
height: u32,
commands: &[WgpuDrawCommand],
) -> Result<Vec<u8>, String> {
let pixels = rasterize_draw_commands_rgba8(width, height, commands)?;
self.upload_and_readback_rgba8(width, height, &pixels)
}
pub fn render_blur_rgba8(
&self,
width: u32,
height: u32,
rgba8: &[u8],
radius: u32,
) -> Result<Vec<u8>, String> {
let expected_len = rgba_buffer_len(width, height)?;
if rgba8.len() != expected_len {
return Err(format!(
"rgba8 input is {} bytes but a {width}x{height} image needs {expected_len} bytes",
rgba8.len(),
));
}
let radius = radius.min(100);
if radius == 0 {
return Ok(rgba8.to_vec());
}
let padded_bytes_per_row = padded_row_bytes(width)?;
let output_buffer_size = readback_frame_bytes(width, height)?;
let mut cache = self.cache.borrow_mut();
cache.ensure_blur_targets(&self.device, &self.blur_bind_group_layout, width, height);
cache.ensure_readback(&self.device, output_buffer_size);
let targets = cache.blur.as_ref().expect("the blur targets were just ensured");
let readback = cache.readback.as_ref().expect("the readback was just ensured");
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &targets.source,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
rgba8,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(width * 4),
rows_per_image: Some(height),
},
wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
);
let write_params = |buffer: &wgpu::Buffer, direction: [f32; 2]| {
let mut bytes = [0u8; 16];
bytes[0..4].copy_from_slice(&direction[0].to_ne_bytes());
bytes[4..8].copy_from_slice(&direction[1].to_ne_bytes());
bytes[8..12].copy_from_slice(&(radius as f32).to_ne_bytes());
self.queue.write_buffer(buffer, 0, &bytes);
};
write_params(&targets.horizontal_params, [1.0, 0.0]);
write_params(&targets.vertical_params, [0.0, 1.0]);
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("wgpu_blur_encoder"),
});
let mut blur_pass = |view: &wgpu::TextureView, group: &wgpu::BindGroup, label: &str| {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(label),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&self.blur_pipeline);
pass.set_bind_group(0, group, &[]);
pass.draw(0..3, 0..1);
};
blur_pass(&targets.scratch_view, &targets.horizontal, "wgpu_blur_horizontal");
blur_pass(&targets.source_view, &targets.vertical, "wgpu_blur_vertical");
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &targets.source,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback.buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded_bytes_per_row),
rows_per_image: Some(height),
},
},
wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
);
self.queue.submit(Some(encoder.finish()));
self.read_back(&readback.buffer, width, height, expected_len)
}
pub fn render_backdrop_blur_rgba8(
&self,
width: u32,
height: u32,
backdrop: &[u8],
radius: u32,
tint: Option<(Rgba8, f32)>,
) -> Result<Vec<u8>, String> {
let mut frame = self.render_blur_rgba8(width, height, backdrop, radius)?;
if let Some((tint, weight)) = tint {
tint_blurred_frame(&mut frame, tint, weight);
}
Ok(frame)
}
pub fn upload_rgba8_and_readback(
&self,
width: u32,
height: u32,
rgba8: &[u8],
) -> Result<Vec<u8>, String> {
if width == 0 || height == 0 {
return Err(format!("width/height must be > 0, got {width}x{height}"));
}
let expected_len = rgba_buffer_len(width, height)?;
if rgba8.len() != expected_len {
return Err(format!(
"rgba8 input is {} bytes but a {width}x{height} image needs {expected_len} bytes",
rgba8.len(),
));
}
let padded_bytes_per_row = padded_row_bytes(width)?;
let output_buffer_size = readback_frame_bytes(width, height)?;
let mut cache = self.cache.borrow_mut();
cache.ensure_upload_target(&self.device, width, height);
cache.ensure_readback(&self.device, output_buffer_size);
let target = cache.upload_target.as_ref().expect("the upload target was just ensured");
let readback = cache.readback.as_ref().expect("the readback was just ensured");
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &target.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
rgba8,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(width * 4),
rows_per_image: Some(height),
},
wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
);
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("rw_wgpu_encoder"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &target.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback.buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded_bytes_per_row),
rows_per_image: Some(height),
},
},
wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
);
self.queue.submit(Some(encoder.finish()));
self.read_back(&readback.buffer, width, height, expected_len)
}
fn upload_and_readback_rgba8(
&self,
width: u32,
height: u32,
rgba8: &[u8],
) -> Result<Vec<u8>, String> {
self.upload_rgba8_and_readback(width, height, rgba8)
}
pub fn shader_modules(&self) -> Vec<ShaderModule> {
vec![
ShaderModule::FillRect,
ShaderModule::DrawImage,
ShaderModule::DrawText,
ShaderModule::FillRoundedRect,
ShaderModule::FillCircle,
]
}
pub fn get_shader_module(&self, name: &str) -> Option<&wgpu::ShaderModule> {
self.shader_cache.get(name)
}
}
impl GpuRenderer for WgpuRenderer {
fn initialize(&mut self) -> Result<(), String> {
Ok(())
}
fn capabilities(&self) -> &[GpuCapability] {
&[GpuCapability::Basic2D, GpuCapability::Texture]
}
fn has_capability(&self, capability: GpuCapability) -> bool {
matches!(capability, GpuCapability::Basic2D | GpuCapability::Texture)
}
fn render_frame(&mut self) -> Result<(), String> {
Err("WgpuRenderer has no present/swapchain path; render offscreen with the \
render_*_rgba8 methods instead"
.to_string())
}
fn memory_usage(&self) -> u64 {
0
}
fn vendor_info(&self) -> &str {
"wgpu (WGPU)"
}
}
#[cfg(test)]
mod tests {
use super::*;
fn renderer() -> Option<WgpuRenderer> {
match WgpuRenderer::new() {
Ok(renderer) => Some(renderer),
Err(error) => {
eprintln!("skipping GPU test: no wgpu adapter available ({error})");
None
}
}
}
fn pixel(pixels: &[u8], width: u32, x: u32, y: u32) -> [u8; 4] {
let start = ((y * width + x) * 4) as usize;
[pixels[start], pixels[start + 1], pixels[start + 2], pixels[start + 3]]
}
#[test]
fn shader_module_registry_matches_pipelines() {
let Some(renderer) = renderer() else { return };
let mut expected = vec![
"clear_vs",
"clear_fs",
"rect_vs",
"rect_fs",
"blur_vs",
"blur_fs",
"rect_vs_transformed",
"rect_fs_instanced",
];
#[cfg(feature = "3d")]
expected.extend(["mesh3d_vs", "mesh3d_fs"]);
let mut registered: Vec<&str> = renderer.shader_cache.keys().map(String::as_str).collect();
expected.sort_unstable();
registered.sort_unstable();
assert_eq!(
registered, expected,
"the registered shader cache drifted from the pipeline table; wire or remove the \
entry and update the table in this test"
);
for module in renderer.shader_modules() {
if let Some(key) = module.pipeline_cache_key() {
assert!(
renderer.get_shader_module(key).is_some(),
"{module:?} advertises pipeline key {key:?}, but no such module is \
registered"
);
}
}
}
#[test]
fn buffer_growth_leaves_headroom_and_respects_the_limit() {
assert_eq!(grown_buffer_capacity(0, 1, u64::MAX), 65_536);
let first = grown_buffer_capacity(0, 100 * 1024, u64::MAX);
assert!(first >= 100 * 1024);
assert_eq!(first % 65_536, 0, "capacities are whole 64 KiB blocks");
let grown = grown_buffer_capacity(first, first + 16, u64::MAX);
assert!(grown > first, "growth is geometric, not exact-size");
assert_eq!(grown, first + first / 2, "the next step is 1.5x before rounding");
assert_eq!(grown_buffer_capacity(0, 3, 4), 4);
}
#[test]
fn texture_path_renders_a_view_without_readback() {
let Some(renderer) = renderer() else { return };
let rects = [(0, 0, 4, 4, [1.0, 0.0, 0.0, 1.0])];
let seen = renderer
.with_fill_rect_texture(4, 4, &rects, |_view| true)
.expect("the texture path must render");
assert!(seen, "the closure receives the rendered view");
}
#[test]
fn clear_writes_the_requested_colour_across_the_frame() {
let Some(renderer) = renderer() else { return };
let pixels =
renderer.render_clear_gpu(4, 3, [1.0, 0.0, 0.0, 1.0]).expect("the clear must render");
assert_eq!(pixels.len(), 4 * 3 * 4);
for y in 0..3 {
for x in 0..4 {
assert_eq!(pixel(&pixels, 4, x, y), [255, 0, 0, 255]);
}
}
}
#[test]
fn fill_rect_reuses_its_buffers_without_changing_the_picture() {
let Some(renderer) = renderer() else { return };
let rects = [(0, 0, 4, 4, [1.0, 0.0, 0.0, 1.0]), (4, 4, 4, 4, [0.0, 0.0, 1.0, 1.0])];
let first = renderer.render_fill_rect_gpu(8, 8, &rects).expect("first fill");
let second = renderer.render_fill_rect_gpu(8, 8, &rects).expect("second fill");
assert_eq!(first, second, "cached resources must reproduce the same frame");
assert_eq!(pixel(&first, 8, 0, 0), [255, 0, 0, 255]);
assert_eq!(pixel(&first, 8, 3, 3), [255, 0, 0, 255]);
assert_eq!(pixel(&first, 8, 4, 4), [0, 0, 255, 255]);
assert_eq!(pixel(&first, 8, 7, 7), [0, 0, 255, 255]);
assert_eq!(pixel(&first, 8, 7, 0), [0, 0, 0, 0]);
}
#[test]
fn a_larger_run_renders_every_colour() {
let Some(renderer) = renderer() else { return };
let one = [(0, 0, 4, 2, [1.0, 0.0, 0.0, 1.0])];
renderer.render_fill_rect_gpu(4, 2, &one).expect("one rect");
let three = [
(0, 0, 1, 1, [1.0, 0.0, 0.0, 1.0]),
(1, 0, 1, 1, [0.0, 1.0, 0.0, 1.0]),
(2, 0, 1, 1, [0.0, 0.0, 1.0, 1.0]),
];
let pixels = renderer.render_fill_rect_gpu(4, 2, &three).expect("three rects");
assert_eq!(pixel(&pixels, 4, 0, 0), [255, 0, 0, 255]);
assert_eq!(pixel(&pixels, 4, 1, 0), [0, 255, 0, 255]);
assert_eq!(pixel(&pixels, 4, 2, 0), [0, 0, 255, 255]);
}
#[test]
fn upload_round_trips_the_given_pixels() {
let Some(renderer) = renderer() else { return };
let input: Vec<u8> = (0..(4 * 4 * 4) as u8).collect();
let output =
renderer.upload_rgba8_and_readback(4, 4, &input).expect("the upload must round-trip");
assert_eq!(output, input);
assert_eq!(renderer.upload_rgba8_and_readback(4, 4, &input).expect("second upload"), input);
}
#[cfg(feature = "3d")]
#[test]
fn fill_quads_empty_is_transparent() {
let Some(renderer) = renderer() else { return };
let pixels = renderer
.render_fill_quads_gpu(8, 8, &crate::render::math3d::Mat3::IDENTITY, &[])
.expect("an empty quad batch must produce a frame");
assert_eq!(pixels.len(), 8 * 8 * 4);
assert!(pixels.iter().all(|&byte| byte == 0));
}
#[cfg(feature = "3d")]
#[test]
fn a_45_degree_rotated_rect_agrees_with_the_software_path() {
use crate::core::{Color, Point};
use crate::render::math3d::Mat3;
use crate::render::SoftwareSurface;
let Some(renderer) = renderer() else { return };
let affine = Mat3::translation(-16.0, -16.0)
.then(&Mat3::rotation(core::f32::consts::FRAC_PI_4))
.then(&Mat3::translation(16.0, 16.0));
let local = [Point::new(8, 8), Point::new(24, 8), Point::new(24, 24), Point::new(8, 24)];
let gpu = renderer
.render_fill_quads_gpu(32, 32, &affine, &[(local, [1.0, 0.0, 0.0, 1.0])])
.expect("the matrix pipeline must render");
let mapped = local.map(|point| affine.map_point(point));
let mut surface = SoftwareSurface::new(crate::core::Size::new(32, 32), 1.0);
surface.begin_frame(Color::TRANSPARENT);
surface.draw_path(&mapped[..], true, Color::RED, true, 0);
surface.end_frame();
let software = surface.frame_rgba();
for (x, y, what) in [
(16, 16, "centre"),
(10, 16, "left interior"),
(22, 16, "right interior"),
(16, 10, "top interior"),
(16, 22, "bottom interior"),
] {
assert_eq!(
pixel(software, 32, x, y),
pixel(&gpu, 32, x, y),
"{what} must agree between the software and matrix paths"
);
}
for (x, y) in [(0, 0), (31, 0), (0, 31), (31, 31)] {
assert_eq!(
pixel(software, 32, x, y),
pixel(&gpu, 32, x, y),
"a corner outside the rotated square must remain transparent in both paths"
);
}
}
#[cfg(feature = "3d")]
fn quad_rect(x: i32, y: i32, x1: i32, y1: i32) -> [crate::core::Point; 4] {
[
crate::core::Point::new(x, y),
crate::core::Point::new(x1, y),
crate::core::Point::new(x1, y1),
crate::core::Point::new(x, y1),
]
}
#[cfg(feature = "3d")]
fn gpu_colour(color: crate::core::Color) -> [f64; 4] {
[
color.r as f64 / 255.0,
color.g as f64 / 255.0,
color.b as f64 / 255.0,
color.a as f64 / 255.0,
]
}
#[cfg(feature = "3d")]
#[test]
fn instanced_run_matches_the_software_reference() {
use crate::core::{Color, Point};
use crate::render::math3d::Mat3;
use crate::render::SoftwareSurface;
let Some(renderer) = renderer() else { return };
let run = [
(quad_rect(2, 2, 10, 10), Color::RED),
(quad_rect(8, 2, 16, 10), Color::GREEN),
(quad_rect(2, 8, 10, 16), Color::BLUE),
(quad_rect(8, 8, 16, 16), Color::YELLOW),
];
let quads: Vec<([Point; 4], [f64; 4])> =
run.iter().map(|(corners, colour)| (*corners, gpu_colour(*colour))).collect();
let gpu = renderer
.render_fill_quads_gpu(18, 18, &Mat3::IDENTITY, &quads)
.expect("the instanced run must render");
let mut surface = SoftwareSurface::new(crate::core::Size::new(18, 18), 1.0);
surface.begin_frame(Color::TRANSPARENT);
for (corners, colour) in run {
surface.draw_path(&corners, true, colour, true, 0);
}
surface.end_frame();
let software = surface.frame_rgba();
for (x, y, expected) in [
(4, 4, Color::RED),
(12, 4, Color::GREEN),
(4, 12, Color::BLUE),
(12, 12, Color::YELLOW),
(9, 4, Color::GREEN),
(4, 9, Color::BLUE),
(12, 9, Color::YELLOW),
(9, 12, Color::YELLOW),
(9, 9, Color::YELLOW),
] {
assert_eq!(
pixel(&gpu, 18, x, y),
pixel(&software, 18, x, y),
"pixel ({x}, {y}) must match the software reference"
);
assert_eq!(
pixel(&gpu, 18, x, y),
[expected.r, expected.g, expected.b, expected.a],
"pixel ({x}, {y}) must show the last quad in the run that covers it"
);
}
}
#[cfg(feature = "3d")]
#[test]
fn instanced_empty_run_is_transparent() {
let Some(renderer) = renderer() else { return };
let pixels = renderer
.render_fill_quads_gpu(8, 8, &crate::render::math3d::Mat3::IDENTITY, &[])
.expect("an empty instanced run must produce a frame");
assert_eq!(pixels.len(), 8 * 8 * 4);
assert!(pixels.iter().all(|&byte| byte == 0));
}
#[cfg(feature = "3d")]
#[test]
fn an_over_cap_instanced_run_streams_every_chunk_in_order() {
let Some(renderer) = renderer() else { return };
let per_row = 1024u32;
let total = MAX_INSTANCES_PER_DRAW + 1;
let width = per_row * 2;
let height = (total as u32).div_ceil(per_row);
let mut quads = Vec::with_capacity(total);
for index in 0..total {
let x = ((index as u32 % per_row) * 2) as i32;
let y = (index as u32 / per_row) as i32;
let colour =
if index + 1 == total { [0.0, 0.0, 1.0, 1.0] } else { [1.0, 0.0, 0.0, 1.0] };
quads.push((quad_rect(x, y, x + 2, y + 1), colour));
}
let pixels = renderer
.render_fill_quads_gpu(width, height, &crate::render::math3d::Mat3::IDENTITY, &quads)
.expect("an over-cap run must render");
let sample = |index: usize| -> [u8; 4] {
let x = (index as u32 % per_row) * 2;
let y = index as u32 / per_row;
pixel(&pixels, width, x, y)
};
let last = total - 1;
assert_eq!(sample(last), [0, 0, 255, 255], "the second chunk must draw its own quad");
assert_eq!(sample(last - 1), [255, 0, 0, 255], "the first chunk's last quad must draw");
assert_eq!(sample(0), [255, 0, 0, 255], "the first chunk's first quad must draw");
}
#[cfg(feature = "3d")]
#[test]
fn a_45_degree_instanced_run_agrees_with_the_software_path() {
use crate::core::{Color, Point};
use crate::render::math3d::Mat3;
use crate::render::SoftwareSurface;
let Some(renderer) = renderer() else { return };
let affine = Mat3::translation(-16.0, -16.0)
.then(&Mat3::rotation(core::f32::consts::FRAC_PI_4))
.then(&Mat3::translation(16.0, 16.0));
let run = [
(quad_rect(6, 6, 14, 14), Color::RED),
(quad_rect(18, 6, 26, 14), Color::GREEN),
(quad_rect(6, 18, 14, 26), Color::BLUE),
];
let quads: Vec<([Point; 4], [f64; 4])> =
run.iter().map(|(corners, colour)| (*corners, gpu_colour(*colour))).collect();
let gpu = renderer
.render_fill_quads_gpu(32, 32, &affine, &quads)
.expect("the instanced matrix pipeline must render");
let mut surface = SoftwareSurface::new(crate::core::Size::new(32, 32), 1.0);
surface.begin_frame(Color::TRANSPARENT);
for (corners, colour) in run {
let mapped = corners.map(|point| affine.map_point(point));
surface.draw_path(&mapped, true, colour, true, 0);
}
surface.end_frame();
let software = surface.frame_rgba();
for ((corners, _colour), what) in run.iter().zip(["top-left", "top-right", "bottom-left"]) {
let centre = affine.map_point(Point::new(
(corners[0].x + corners[2].x) / 2,
(corners[0].y + corners[2].y) / 2,
));
let (x, y) = (centre.x as u32, centre.y as u32);
assert_eq!(
pixel(software, 32, x, y),
pixel(&gpu, 32, x, y),
"the {what} square centre must agree between the software and instanced paths"
);
}
for (x, y) in [(0, 0), (31, 0), (0, 31), (31, 31)] {
assert_eq!(
pixel(software, 32, x, y),
pixel(&gpu, 32, x, y),
"a corner outside the rotated run must remain transparent in both paths"
);
}
}
#[test]
fn a_zero_radius_blur_returns_the_input() {
let Some(renderer) = renderer() else { return };
let input = vec![9u8; 4 * 4 * 4];
let output = renderer.render_blur_rgba8(4, 4, &input, 0).expect("identity blur");
assert_eq!(output, input);
}
#[test]
fn a_uniform_field_survives_the_gpu_blur() {
let Some(renderer) = renderer() else { return };
let input: Vec<u8> = (0..8 * 8).flat_map(|_| [100u8, 150, 200, 255]).collect();
let output = renderer.render_blur_rgba8(8, 8, &input, 3).expect("the blur pass runs");
for y in 0..8 {
for x in 0..8 {
let seen = pixel(&output, 8, x, y);
for channel in 0..4 {
assert!(
seen[channel].abs_diff(input[channel]) <= 1,
"a uniform field must stay uniform at ({x},{y}), got {seen:?}"
);
}
}
}
}
#[test]
fn the_gpu_blur_spreads_an_edge_symmetrically() {
let Some(renderer) = renderer() else { return };
let width = 16u32;
let height = 4u32;
let mut input = vec![0u8; (width * height * 4) as usize];
for y in 0..height {
for x in 0..width {
let offset = ((y * width + x) * 4) as usize;
if x < width / 2 {
input[offset..offset + 4].copy_from_slice(&[255, 0, 0, 255]);
} else {
input[offset..offset + 4].copy_from_slice(&[0, 0, 255, 255]);
}
}
}
let output = renderer.render_blur_rgba8(width, height, &input, 4).expect("the blur runs");
let left = pixel(&output, width, width / 2 - 1, 2);
let right = pixel(&output, width, width / 2, 2);
assert!(left[2] > 0, "blue must bleed left of the seam: {left:?}");
assert!(right[0] > 0, "red must bleed right of the seam: {right:?}");
assert!(
left[0].abs_diff(right[2]) <= 2 && left[2].abs_diff(right[0]) <= 2,
"the two sides of a symmetric edge must mirror: {left:?} vs {right:?}"
);
}
#[cfg(feature = "3d")]
#[test]
fn the_gpu_mesh_pipeline_draws_a_cube_through_a_depth_attachment() {
use crate::render::math3d::Mat4;
use crate::render::mesh3d::{Camera, Mesh3D, Vec3};
let Some(renderer) = renderer() else { return };
let mesh = Mesh3D::cube(Vec3::ZERO, 2.0, [crate::core::Color::RED; 6]);
let camera = Camera::new(Vec3::new(3.0, 2.0, 6.0), Vec3::ZERO, Vec3::new(0.0, 1.0, 0.0));
let pixels = renderer
.render_mesh_3d_gpu(
64,
64,
&mesh,
&Mat4::IDENTITY,
&camera,
crate::core::Rect::new(0, 0, 64, 64),
)
.expect("the mesh pipeline runs");
assert_eq!(pixels.len(), 64 * 64 * 4);
let painted = pixels.chunks_exact(4).filter(|pixel| pixel[3] > 0).count();
assert!(painted > 100, "the cube must cover part of the target, got {painted} pixels");
}
}