blob: f1305c8bda16f95b2f3e194c9e2832f80eb38f8e [file]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! GPU rendering module for the sparse strips CPU/GPU rendering engine.
//!
//! This module provides the GPU-side implementation of the hybrid rendering system.
//! It handles:
//! - GPU resource management (buffers, textures, pipelines)
//! - Surface/window management and presentation
//! - Shader execution and rendering
//!
//! The hybrid approach combines CPU-side path processing with efficient GPU rendering
//! to balance flexibility and performance.
#![expect(
clippy::cast_possible_truncation,
reason = "We temporarily ignore those because the casts\
only break in edge cases, and some of them are also only related to conversions from f64 to f32."
)]
use crate::{
GpuStrip, RenderError, RenderSettings, RenderSize,
gradient_cache::GradientRampCache,
render::{
Config,
common::{
GPU_ENCODED_IMAGE_SIZE_TEXELS, GPU_LINEAR_GRADIENT_SIZE_TEXELS,
GPU_RADIAL_GRADIENT_SIZE_TEXELS, GPU_SWEEP_GRADIENT_SIZE_TEXELS, GpuEncodedImage,
GpuEncodedPaint, GpuLinearGradient, GpuRadialGradient, GpuSweepGradient,
pack_image_offset, pack_image_params, pack_image_size, pack_radial_kind_and_swapped,
pack_texture_width_and_extend_mode, pack_tint,
},
},
scene::Scene,
schedule::{LoadOp, RendererBackend, Scheduler, SchedulerState},
};
use alloc::vec::Vec;
use alloc::{sync::Arc, vec};
use bytemuck::{Pod, Zeroable};
use core::{fmt::Debug, num::NonZeroU64};
use vello_common::image_cache::{ImageCache, ImageResource};
use vello_common::multi_atlas::{AtlasConfig, AtlasId};
use vello_common::{
coarse::WideTile,
encode::{EncodedGradient, EncodedKind, EncodedPaint, MAX_GRADIENT_LUT_SIZE, RadialKind},
kurbo::Affine,
paint::ImageSource,
peniko,
pixmap::Pixmap,
tile::Tile,
};
use wgpu::{
BindGroup, BindGroupLayout, BlendState, Buffer, ColorTargetState, ColorWrites, CommandEncoder,
Device, Extent3d, PipelineCompilationOptions, Queue, RenderPassColorAttachment,
RenderPassDescriptor, RenderPipeline, Texture, TextureView, TextureViewDescriptor,
util::DeviceExt,
};
/// Placeholder value for uninitialized GPU encoded paints.
const GPU_PAINT_PLACEHOLDER: GpuEncodedPaint = GpuEncodedPaint::LinearGradient(GpuLinearGradient {
texture_width_and_extend_mode: 0,
gradient_start: 0,
transform: [0.0; 6],
});
/// Options for the renderer
#[derive(Debug)]
pub struct RenderTargetConfig {
/// Format of the rendering target
pub format: wgpu::TextureFormat,
/// Width of the rendering target
pub width: u32,
/// Height of the rendering target
pub height: u32,
}
/// Vello Hybrid's Renderer.
#[derive(Debug)]
pub struct Renderer {
/// Programs for rendering.
programs: Programs,
/// Scheduler for scheduling draws.
scheduler: Scheduler,
/// The state used by the scheduler.
scheduler_state: SchedulerState,
/// Image cache for storing images atlas allocations.
pub image_cache: ImageCache,
/// Encoded paints for storing encoded paints.
encoded_paints: Vec<GpuEncodedPaint>,
/// Stores the index (offset) of the encoded paints in the encoded paints texture.
paint_idxs: Vec<u32>,
/// Gradient cache for storing gradient ramps.
gradient_cache: GradientRampCache,
}
impl Renderer {
/// Creates a new renderer.
pub fn new(device: &Device, render_target_config: &RenderTargetConfig) -> Self {
Self::new_with(device, render_target_config, RenderSettings::default())
}
/// Creates a new renderer with specific settings.
pub fn new_with(
device: &Device,
render_target_config: &RenderTargetConfig,
settings: RenderSettings,
) -> Self {
super::common::maybe_warn_about_webgl_feature_conflict();
let max_texture_dimension_2d = device.limits().max_texture_dimension_2d;
let total_slots = (max_texture_dimension_2d / u32::from(Tile::HEIGHT)) as usize;
let image_cache = ImageCache::new_with_config(settings.atlas_config);
// Estimate the maximum number of gradient cache entries based on the max texture dimension
// and the maximum gradient LUT size - worst case scenario.
let max_gradient_cache_size =
max_texture_dimension_2d * max_texture_dimension_2d / MAX_GRADIENT_LUT_SIZE as u32;
let gradient_cache = GradientRampCache::new(max_gradient_cache_size, settings.level);
Self {
programs: Programs::new(device, &image_cache, render_target_config, total_slots),
scheduler: Scheduler::new(total_slots),
scheduler_state: SchedulerState::default(),
image_cache,
gradient_cache,
encoded_paints: Vec::new(),
paint_idxs: Vec::new(),
}
}
/// Render `scene` into the provided command encoder.
///
/// This method creates GPU resources as needed and schedules potentially multiple
/// render passes.
pub fn render(
&mut self,
scene: &Scene,
device: &Device,
queue: &Queue,
encoder: &mut CommandEncoder,
render_size: &RenderSize,
view: &TextureView,
) -> Result<(), RenderError> {
self.render_scene(scene, device, queue, encoder, render_size, view, true)
}
/// Render a `scene` directly into an atlas layer.
///
/// This renders the scene's content into the specified atlas layer, which can then
/// be sampled as an image in subsequent render passes. This is useful for rendering
/// vector content (e.g., glyphs) into the atlas for later use as cached images.
///
/// The scene should be sized to the atlas layer dimensions
/// ([`AtlasConfig::atlas_size`]), with content positioned at the allocated offset
/// coordinates from `ImageCache::allocate`.
///
/// This method creates its own command encoder and submits immediately,
/// ensuring atlas content is committed before any subsequent
/// [`render`](Self::render) call (the two methods share GPU resources that
/// are staged by `queue.write_*` and only applied on the next `queue.submit`).
#[doc(hidden)]
pub fn render_to_atlas(
&mut self,
scene: &Scene,
device: &Device,
queue: &Queue,
atlas_id: AtlasId,
) -> Result<(), RenderError> {
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render to Atlas Encoder"),
});
Programs::maybe_resize_atlas_texture_array(
device,
&mut encoder,
&mut self.programs.resources,
&self.programs.atlas_bind_group_layout,
self.image_cache.atlas_count() as u32,
);
let AtlasConfig {
atlas_size: (atlas_width, atlas_height),
..
} = self.image_cache.atlas_manager().config();
let atlas_render_size = RenderSize {
width: *atlas_width,
height: *atlas_height,
};
let layer_view =
self.programs
.resources
.atlas_texture_array
.create_view(&TextureViewDescriptor {
label: Some("Atlas Layer Render View"),
format: Some(wgpu::TextureFormat::Rgba8Unorm),
dimension: Some(wgpu::TextureViewDimension::D2),
aspect: wgpu::TextureAspect::All,
base_mip_level: 0,
mip_level_count: Some(1),
base_array_layer: atlas_id.as_u32(),
array_layer_count: Some(1),
usage: None,
});
// Swap in the stub atlas bind group to avoid the read-write conflict:
// the real atlas texture is used as the render target (COLOR_TARGET), so it
// cannot also be bound as a shader resource (TEXTURE_BINDING) in the same pass.
core::mem::swap(
&mut self.programs.resources.atlas_bind_group,
&mut self.programs.resources.stub_atlas_bind_group,
);
// TODO: The atlas is always RGBA8; when the surface uses a different format (e.g. BGRA on
// macOS), we may need a dedicated RGBA8 render pipeline for atlas rendering. Adopt the
// fix from the filters/native-format pipeline work when available.
let result = self.render_scene(
scene,
device,
queue,
&mut encoder,
&atlas_render_size,
&layer_view,
false,
);
// Restore the real atlas bind group.
core::mem::swap(
&mut self.programs.resources.atlas_bind_group,
&mut self.programs.resources.stub_atlas_bind_group,
);
// Submit immediately so the atlas content is committed before subsequent
// render() calls overwrite the shared alpha/config/paint resources.
queue.submit(Some(encoder.finish()));
result
}
/// Shared render pipeline: prepares GPU resources, runs the scheduler against
/// the provided `view` at `render_size`, and maintains caches.
///
/// When `clear` is true the render target is cleared to transparent black
/// before drawing (normal frame rendering).
fn render_scene(
&mut self,
scene: &Scene,
device: &Device,
queue: &Queue,
encoder: &mut CommandEncoder,
render_size: &RenderSize,
view: &TextureView,
// See https://github.com/linebender/vello/pull/1458/changes#r2851077556
// TODO: Fix this ASAP!
_clear: bool,
) -> Result<(), RenderError> {
self.prepare_gpu_encoded_paints(&scene.encoded_paints);
// TODO: For the time being, we upload the entire alpha buffer as one big chunk. As a future
// refinement, we could have a bounded alpha buffer, and break draws when the alpha
// buffer fills.
self.programs.prepare(
device,
queue,
&mut self.gradient_cache,
&self.encoded_paints,
&mut scene.strip_storage.borrow_mut().alphas,
render_size,
&self.paint_idxs,
);
// Upload tile-line instances and run the winding render pass before strip rendering.
self.programs
.upload_tile_lines(device, queue, &scene.tile_lines);
let mut ctx = RendererContext {
programs: &mut self.programs,
device,
queue,
encoder,
view,
};
ctx.do_winding_render_pass(&scene.tile_lines);
self.scheduler
.do_scene(&mut self.scheduler_state, &mut ctx, scene, &self.paint_idxs)?;
self.gradient_cache.maintain();
Ok(())
}
/// Upload image to cache and atlas in one step. Returns the `ImageId`.
///
/// It's used when an image is not already in the cache.
///
/// This is a convenience method that:
/// 1. Reserves space in the image cache
/// 2. Writes the image data directly to the atlas
/// 3. Returns the `ImageId` for use in rendering
pub fn upload_image<T: AtlasWriter>(
&mut self,
device: &Device,
queue: &Queue,
encoder: &mut CommandEncoder,
writer: &T,
) -> vello_common::paint::ImageId {
// TODO: If we want to use native bilinear sampling for uploaded images,
// we can pass 1 instead of 0 here.
self.upload_image_with(device, queue, encoder, writer, 0)
}
pub(crate) fn upload_image_with<T: AtlasWriter>(
&mut self,
device: &Device,
queue: &Queue,
encoder: &mut CommandEncoder,
writer: &T,
padding: u16,
) -> vello_common::paint::ImageId {
let width = writer.width();
let height = writer.height();
let image_id = self.image_cache.allocate(width, height, padding).unwrap();
self.write_to_atlas(device, queue, encoder, image_id, writer, None);
image_id
}
/// Write pixel data to an existing atlas allocation.
///
/// Unlike [`upload_image`](Self::upload_image), this does not allocate space in the image
/// cache. The `image_id` must have been previously allocated (e.g. via
/// `ImageCache::allocate`). This is useful for uploading CPU-side pixel data (such as
/// bitmap font glyphs) to a pre-allocated atlas region.
///
/// If `offset_override` is `Some`, the provided offset is used instead of the
/// allocator-assigned position. Pass `None` to use the default atlas offset.
#[doc(hidden)]
pub fn write_to_atlas<T: AtlasWriter>(
&mut self,
device: &Device,
queue: &Queue,
encoder: &mut CommandEncoder,
image_id: vello_common::paint::ImageId,
writer: &T,
offset_override: Option<[u32; 2]>,
) {
let image_resource = self
.image_cache
.get(image_id)
.expect("Image resource not found");
Programs::maybe_resize_atlas_texture_array(
device,
encoder,
&mut self.programs.resources,
&self.programs.atlas_bind_group_layout,
self.image_cache.atlas_count() as u32,
);
let offset = offset_override.unwrap_or([
image_resource.offset[0] as u32,
image_resource.offset[1] as u32,
]);
writer.write_to_atlas_layer(
device,
queue,
encoder,
&self.programs.resources.atlas_texture_array,
image_resource.atlas_id.as_u32(),
offset,
writer.width(),
writer.height(),
);
}
/// Destroy an image from the cache and clear the allocated slot in the atlas.
pub fn destroy_image(
&mut self,
device: &Device,
queue: &Queue,
encoder: &mut CommandEncoder,
image_id: vello_common::paint::ImageId,
) {
if let Some(image_resource) = self.image_cache.deallocate(image_id) {
let padding = image_resource.padding as u32;
self.clear_atlas_region(
device,
queue,
encoder,
image_resource.atlas_id,
[
image_resource.offset[0] as u32 - padding,
image_resource.offset[1] as u32 - padding,
],
image_resource.width as u32 + padding * 2,
image_resource.height as u32 + padding * 2,
);
}
}
/// Returns a reference to the underlying atlas texture array.
///
/// This is a 2D array texture (`TextureViewDimension::D2Array`) containing all
/// atlas layers used by the image cache. Each layer holds cached image data
/// (e.g., rasterised glyphs) that the renderer samples during draw calls.
pub fn atlas_texture(&self) -> &Texture {
&self.programs.resources.atlas_texture_array
}
/// Clear a specific region of the atlas texture.
fn clear_atlas_region(
&mut self,
_device: &Device,
_queue: &Queue,
encoder: &mut CommandEncoder,
atlas_id: AtlasId,
offset: [u32; 2],
width: u32,
height: u32,
) {
// Create a texture view for the specific atlas layer
let layer_view =
self.programs
.resources
.atlas_texture_array
.create_view(&TextureViewDescriptor {
label: Some("Atlas Layer Clear View"),
format: Some(wgpu::TextureFormat::Rgba8Unorm),
dimension: Some(wgpu::TextureViewDimension::D2),
aspect: wgpu::TextureAspect::All,
base_mip_level: 0,
mip_level_count: Some(1),
base_array_layer: atlas_id.as_u32(),
array_layer_count: Some(1),
// Inherit usage from the texture
usage: None,
});
let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("Clear Atlas Region"),
color_attachments: &[Some(RenderPassColorAttachment {
view: &layer_view,
resolve_target: None,
ops: wgpu::Operations {
// Don't clear entire texture, just the scissor region
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
multiview_mask: None,
});
// Set scissor rectangle to limit clearing to specific region
render_pass.set_scissor_rect(offset[0], offset[1], width, height);
// Use atlas clear pipeline to render transparent pixels
render_pass.set_pipeline(&self.programs.atlas_clear_pipeline);
// Draw fullscreen quad
render_pass.draw(0..4, 0..1);
}
fn prepare_gpu_encoded_paints(&mut self, encoded_paints: &[EncodedPaint]) {
self.encoded_paints
.resize_with(encoded_paints.len(), || GPU_PAINT_PLACEHOLDER);
self.paint_idxs.resize(encoded_paints.len() + 1, 0);
let mut current_idx = 0;
for (encoded_paint_idx, paint) in encoded_paints.iter().enumerate() {
self.paint_idxs[encoded_paint_idx] = current_idx;
match paint {
EncodedPaint::Image(img) => {
if let ImageSource::OpaqueId { id: image_id, .. } = img.source {
let image_resource: Option<&ImageResource> = self.image_cache.get(image_id);
if let Some(image_resource) = image_resource {
let image_paint = self.encode_image_paint(img, image_resource);
self.encoded_paints[encoded_paint_idx] = image_paint;
current_idx += GPU_ENCODED_IMAGE_SIZE_TEXELS;
}
}
}
EncodedPaint::Gradient(gradient) => {
let (gradient_start, gradient_width) =
self.gradient_cache.get_or_create_ramp(gradient);
let gradient_paint: GpuEncodedPaint =
self.encode_gradient_paint(gradient, gradient_width, gradient_start);
let gradient_size_texels = match &gradient_paint {
GpuEncodedPaint::LinearGradient(_) => GPU_LINEAR_GRADIENT_SIZE_TEXELS,
GpuEncodedPaint::RadialGradient(_) => GPU_RADIAL_GRADIENT_SIZE_TEXELS,
GpuEncodedPaint::SweepGradient(_) => GPU_SWEEP_GRADIENT_SIZE_TEXELS,
_ => unreachable!("encode_gradient_for_gpu only returns gradient types"),
};
self.encoded_paints[encoded_paint_idx] = gradient_paint;
current_idx += gradient_size_texels;
}
EncodedPaint::BlurredRoundedRect(_blurred_rect) => {
// TODO: Blurred rounded rectangles are not yet supported
log::warn!(
"Blurred rounded rectangles are not yet supported in sparse strips hybrid renderer"
);
}
}
}
self.paint_idxs[encoded_paints.len()] = current_idx;
}
fn encode_image_paint(
&self,
image: &vello_common::encode::EncodedImage,
image_resource: &ImageResource,
) -> GpuEncodedPaint {
let image_transform = image.transform * Affine::translate((-0.5, -0.5));
let transform = image_transform.as_coeffs().map(|x| x as f32);
let image_size = pack_image_size(image_resource.width, image_resource.height);
let image_offset = pack_image_offset(image_resource.offset[0], image_resource.offset[1]);
let image_params = pack_image_params(
image.sampler.quality as u32,
image.sampler.x_extend as u32,
image.sampler.y_extend as u32,
image_resource.atlas_id.as_u32(),
);
let (tint, tint_mode) = pack_tint(image.tint);
GpuEncodedPaint::Image(GpuEncodedImage {
image_params,
image_size,
image_offset,
transform,
tint,
tint_mode,
image_padding: image_resource.padding as u32,
})
}
fn encode_gradient_paint(
&self,
gradient: &EncodedGradient,
gradient_width: u32,
gradient_start: u32,
) -> GpuEncodedPaint {
let gradient_transform = gradient.transform * Affine::translate((-0.5, -0.5));
let transform = gradient_transform.as_coeffs().map(|x| x as f32);
let extend_mode = match gradient.extend {
peniko::Extend::Pad => 0,
peniko::Extend::Repeat => 1,
peniko::Extend::Reflect => 2,
};
let texture_width_and_extend_mode =
pack_texture_width_and_extend_mode(gradient_width, extend_mode);
match &gradient.kind {
EncodedKind::Linear(_) => GpuEncodedPaint::LinearGradient(GpuLinearGradient {
texture_width_and_extend_mode,
gradient_start,
transform,
}),
EncodedKind::Radial(radial) => {
let (kind, bias, scale, fp0, fp1, fr1, f_focal_x, f_is_swapped, scaled_r0_squared) =
match radial {
RadialKind::Radial { bias, scale } => {
(0, *bias, *scale, 0.0, 0.0, 0.0, 0.0, 0, 0.0)
}
RadialKind::Strip { scaled_r0_squared } => {
(1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, *scaled_r0_squared)
}
RadialKind::Focal {
focal_data,
fp0,
fp1,
} => (
2,
*fp0,
*fp1,
*fp0,
*fp1,
focal_data.fr1,
focal_data.f_focal_x,
focal_data.f_is_swapped as u32,
0.0,
),
};
GpuEncodedPaint::RadialGradient(GpuRadialGradient {
texture_width_and_extend_mode,
gradient_start,
transform,
kind_and_f_is_swapped: pack_radial_kind_and_swapped(kind, f_is_swapped),
bias,
scale,
fp0,
fp1,
fr1,
f_focal_x,
scaled_r0_squared,
})
}
EncodedKind::Sweep(sweep) => GpuEncodedPaint::SweepGradient(GpuSweepGradient {
texture_width_and_extend_mode,
gradient_start,
transform,
start_angle: sweep.start_angle,
inv_angle_delta: sweep.inv_angle_delta,
_padding: [0, 0],
}),
}
}
}
/// Defines the GPU resources and pipelines for rendering.
#[derive(Debug)]
struct Programs {
/// Pipeline for rendering wide tile commands.
strip_pipeline: RenderPipeline,
/// Bind group layout for strip draws
strip_bind_group_layout: BindGroupLayout,
/// Bind group layout for encoded paints
encoded_paints_bind_group_layout: BindGroupLayout,
/// Bind group layout for gradient texture
gradient_bind_group_layout: BindGroupLayout,
/// Bind group layout for atlas textures
atlas_bind_group_layout: BindGroupLayout,
/// Pipeline for clearing slots in slot textures.
clear_pipeline: RenderPipeline,
/// Pipeline for clearing atlas regions.
atlas_clear_pipeline: RenderPipeline,
/// Pipeline for rendering tile-line instances into the winding texture.
winding_pipeline: RenderPipeline,
/// GPU resources for rendering (created during prepare)
resources: GpuResources,
/// Dimensions of the rendering target
render_size: RenderSize,
/// Scratch buffer for staging encoded paints texture data.
encoded_paints_data: Vec<u8>,
}
/// Contains all GPU resources needed for rendering
#[derive(Debug)]
struct GpuResources {
/// Buffer for [`GpuStrip`] data
strips_buffer: Buffer,
/// Winding texture (R16Float) replacing the alpha texture.
winding_texture: Texture,
/// View for the winding texture.
winding_texture_view: TextureView,
/// Buffer for [`GpuTileLine`] instances.
tile_lines_buffer: Buffer,
/// Bind group for the winding pass.
winding_bind_group: BindGroup,
/// Uniform buffer for winding shader config.
winding_config_buffer: Buffer,
/// Textures for atlas data (multiple atlases supported)
atlas_texture_array: Texture,
/// View for atlas texture array
atlas_texture_array_view: TextureView,
/// Bind group for atlas textures (as texture array)
atlas_bind_group: BindGroup,
/// Texture for encoded paints
encoded_paints_texture: Texture,
/// Bind group for encoded paints
encoded_paints_bind_group: BindGroup,
/// Texture for gradient lookup table
gradient_texture: Texture,
/// Bind group for gradient texture
gradient_bind_group: BindGroup,
/// Config buffer for rendering wide tile commands into the view texture.
view_config_buffer: Buffer,
/// Config buffer for rendering wide tile commands into a slot texture.
slot_config_buffer: Buffer,
/// Buffer for slot indices used in `clear_slots`
clear_slot_indices_buffer: Buffer,
// Bind groups for rendering with clip buffers
slot_bind_groups: [BindGroup; 3],
/// Slot texture views
slot_texture_views: [TextureView; 2],
/// Bind group for clear slots operation
clear_bind_group: BindGroup,
/// Placeholder atlas bind group with a 1x1 dummy texture, used during
/// `render_to_atlas` to avoid a read-write conflict on the real atlas texture.
stub_atlas_bind_group: BindGroup,
}
const SIZE_OF_CONFIG: NonZeroU64 = NonZeroU64::new(size_of::<Config>() as u64).unwrap();
/// Config for the clear slots pipeline
#[repr(C)]
#[derive(Debug, Copy, Clone, Pod, Zeroable)]
struct ClearSlotsConfig {
/// Width of a slot
pub slot_width: u32,
/// Height of a slot
pub slot_height: u32,
/// Total height of the texture
pub texture_height: u32,
/// Padding for 16-byte alignment
pub _padding: u32,
}
impl GpuStrip {
/// Vertex attributes for the strip
pub fn vertex_attributes() -> [wgpu::VertexAttribute; 5] {
wgpu::vertex_attr_array![
0 => Uint32,
1 => Uint32,
2 => Uint32,
3 => Uint32,
4 => Uint32,
]
}
}
impl Programs {
fn new(
device: &Device,
image_cache: &ImageCache,
render_target_config: &RenderTargetConfig,
slot_count: usize,
) -> Self {
let strip_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Strip 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::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
],
});
let atlas_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Atlas Texture Bind Group Layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2Array,
multisampled: false,
},
count: None,
}],
});
let encoded_paints_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Encoded Paints Bind Group Layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Uint,
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
}],
});
let gradient_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Gradient Bind Group Layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
}],
});
// Create bind group layout for clearing slots
let clear_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Clear Slots 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: None,
},
count: None,
}],
});
let strip_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Strip Shader"),
source: wgpu::ShaderSource::Wgsl(vello_sparse_shaders::wgsl::RENDER_STRIPS.into()),
});
let clear_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Clear Slots Shader"),
source: wgpu::ShaderSource::Wgsl(vello_sparse_shaders::wgsl::CLEAR_SLOTS.into()),
});
let strip_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Strip Pipeline Layout"),
bind_group_layouts: &[
&strip_bind_group_layout,
&atlas_bind_group_layout,
&encoded_paints_bind_group_layout,
&gradient_bind_group_layout,
],
immediate_size: 0,
});
let clear_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Clear Slots Pipeline Layout"),
bind_group_layouts: &[&clear_bind_group_layout],
immediate_size: 0,
});
let strip_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Strip Pipeline"),
layout: Some(&strip_pipeline_layout),
vertex: wgpu::VertexState {
module: &strip_shader,
entry_point: Some("vs_main"),
buffers: &[wgpu::VertexBufferLayout {
array_stride: size_of::<GpuStrip>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &GpuStrip::vertex_attributes(),
}],
compilation_options: PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &strip_shader,
entry_point: Some("fs_main"),
targets: &[Some(ColorTargetState {
format: render_target_config.format,
blend: Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
compilation_options: PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleStrip,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let clear_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Clear Slots Pipeline"),
layout: Some(&clear_pipeline_layout),
vertex: wgpu::VertexState {
module: &clear_shader,
entry_point: Some("vs_main"),
buffers: &[wgpu::VertexBufferLayout {
array_stride: size_of::<u32>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &[wgpu::VertexAttribute {
format: wgpu::VertexFormat::Uint32,
offset: 0,
shader_location: 0,
}],
}],
compilation_options: PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &clear_shader,
entry_point: Some("fs_main"),
targets: &[Some(ColorTargetState {
format: render_target_config.format,
// No blending needed for clearing
blend: None,
write_mask: ColorWrites::ALL,
})],
compilation_options: PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleStrip,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
// Create atlas clear pipeline
let atlas_clear_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Atlas Clear Pipeline Layout"),
bind_group_layouts: &[],
immediate_size: 0,
});
let atlas_clear_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Atlas Clear Pipeline"),
layout: Some(&atlas_clear_pipeline_layout),
vertex: wgpu::VertexState {
module: &clear_shader,
// Use a different vertex shader entry point
entry_point: Some("vs_main_fullscreen"),
buffers: &[],
compilation_options: PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &clear_shader,
entry_point: Some("fs_main"),
targets: &[Some(ColorTargetState {
format: wgpu::TextureFormat::Rgba8Unorm,
blend: None,
write_mask: ColorWrites::ALL,
})],
compilation_options: PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleStrip,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let slot_texture_views: [TextureView; 2] = core::array::from_fn(|_| {
device
.create_texture(&wgpu::TextureDescriptor {
label: Some("Slot Texture"),
size: Extent3d {
width: u32::from(WideTile::WIDTH),
height: u32::from(Tile::HEIGHT) * slot_count as u32,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: render_target_config.format,
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
})
.create_view(&TextureViewDescriptor::default())
});
let clear_config_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Clear Slots Config"),
contents: bytemuck::bytes_of(&ClearSlotsConfig {
slot_width: u32::from(WideTile::WIDTH),
slot_height: u32::from(Tile::HEIGHT),
texture_height: u32::from(Tile::HEIGHT) * slot_count as u32,
_padding: 0,
}),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let clear_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Clear Slots Bind Group"),
layout: &clear_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: clear_config_buffer.as_entire_binding(),
}],
});
let clear_slot_indices_buffer = Self::create_clear_slot_indices_buffer(
device,
slot_count as u64 * size_of::<u32>() as u64,
);
let slot_config_buffer = Self::create_config_buffer(
device,
&RenderSize {
width: u32::from(WideTile::WIDTH),
height: u32::from(Tile::HEIGHT) * slot_count as u32,
},
device.limits().max_texture_dimension_2d,
);
let max_texture_dimension_2d = device.limits().max_texture_dimension_2d;
// --- Winding texture (replaces alpha texture) ---
const INITIAL_WINDING_TEXTURE_HEIGHT: u32 = 4; // At least one band of TILE_HEIGHT
let winding_texture = Self::create_winding_texture(
device,
max_texture_dimension_2d,
INITIAL_WINDING_TEXTURE_HEIGHT,
);
let winding_texture_view = winding_texture.create_view(&TextureViewDescriptor::default());
// Winding shader + pipeline
let winding_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Winding Shader"),
source: wgpu::ShaderSource::Wgsl(vello_sparse_shaders::wgsl::WINDING.into()),
});
let winding_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Winding 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: None,
},
count: None,
}],
});
let winding_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Winding Pipeline Layout"),
bind_group_layouts: &[&winding_bind_group_layout],
immediate_size: 0,
});
let winding_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Winding Pipeline"),
layout: Some(&winding_pipeline_layout),
vertex: wgpu::VertexState {
module: &winding_shader,
entry_point: Some("vs_main"),
buffers: &[wgpu::VertexBufferLayout {
array_stride: size_of::<crate::gpu_winding::GpuTileLine>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &wgpu::vertex_attr_array![
0 => Uint32, // winding_col
1 => Uint32, // tile_xy_kind
2 => Float32x2, // p0
3 => Float32x2, // p1
],
}],
compilation_options: PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &winding_shader,
entry_point: Some("fs_main"),
targets: &[Some(ColorTargetState {
format: wgpu::TextureFormat::R16Float,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent::OVER,
}),
write_mask: ColorWrites::ALL,
})],
compilation_options: PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleStrip,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct WindingConfig {
winding_tex_width: u32,
winding_tex_height: u32,
}
let winding_config_buffer =
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Winding Config Buffer"),
contents: bytemuck::bytes_of(&WindingConfig {
winding_tex_width: max_texture_dimension_2d,
winding_tex_height: INITIAL_WINDING_TEXTURE_HEIGHT,
}),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let winding_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Winding Bind Group"),
layout: &winding_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: winding_config_buffer.as_entire_binding(),
}],
});
let tile_lines_buffer = Self::create_tile_lines_buffer(device, 0);
let view_config_buffer = Self::create_config_buffer(
device,
&RenderSize {
width: render_target_config.width,
height: render_target_config.height,
},
max_texture_dimension_2d,
);
let AtlasConfig {
atlas_size: (atlas_width, atlas_height),
initial_atlas_count,
..
} = image_cache.atlas_manager().config();
let (atlas_texture_array, atlas_texture_array_view) = Self::create_atlas_texture_array(
device,
*atlas_width,
*atlas_height,
*initial_atlas_count as u32,
);
let atlas_bind_group = Self::create_atlas_bind_group(
device,
&atlas_bind_group_layout,
&atlas_texture_array_view,
);
// Create a 1x1 stub atlas texture array for use during render_to_atlas.
// This avoids the read-write conflict that occurs when the real atlas is both
// a shader input (bind group) and render target in the same pass.
let (_stub_atlas_texture, stub_atlas_view) =
Self::create_atlas_texture_array(device, 1, 1, 1);
let stub_atlas_bind_group =
Self::create_atlas_bind_group(device, &atlas_bind_group_layout, &stub_atlas_view);
const INITIAL_ENCODED_PAINTS_TEXTURE_HEIGHT: u32 = 1;
let encoded_paints_data = vec![
0;
((max_texture_dimension_2d * INITIAL_ENCODED_PAINTS_TEXTURE_HEIGHT) << 4)
as usize
];
let encoded_paints_texture = Self::create_encoded_paints_texture(
device,
max_texture_dimension_2d,
INITIAL_ENCODED_PAINTS_TEXTURE_HEIGHT,
);
let encoded_paints_bind_group = Self::create_encoded_paints_bind_group(
device,
&encoded_paints_bind_group_layout,
&encoded_paints_texture.create_view(&TextureViewDescriptor::default()),
);
const INITIAL_GRADIENT_TEXTURE_HEIGHT: u32 = 1;
let gradient_texture = Self::create_gradient_texture(
device,
max_texture_dimension_2d,
INITIAL_GRADIENT_TEXTURE_HEIGHT,
);
let gradient_bind_group = Self::create_gradient_bind_group(
device,
&gradient_bind_group_layout,
&gradient_texture.create_view(&TextureViewDescriptor::default()),
);
let slot_bind_groups = Self::create_strip_bind_groups(
device,
&strip_bind_group_layout,
&winding_texture_view,
&slot_config_buffer,
&view_config_buffer,
&slot_texture_views,
);
let resources = GpuResources {
strips_buffer: Self::create_strips_buffer(device, 0),
clear_slot_indices_buffer,
slot_texture_views,
slot_config_buffer,
slot_bind_groups,
clear_bind_group,
winding_texture,
winding_texture_view,
tile_lines_buffer,
winding_bind_group,
winding_config_buffer,
atlas_texture_array,
atlas_texture_array_view,
atlas_bind_group,
stub_atlas_bind_group,
encoded_paints_texture,
encoded_paints_bind_group,
gradient_texture,
gradient_bind_group,
view_config_buffer,
};
Self {
strip_pipeline,
strip_bind_group_layout,
encoded_paints_bind_group_layout,
gradient_bind_group_layout,
atlas_bind_group_layout,
winding_pipeline,
resources,
encoded_paints_data,
render_size: RenderSize {
width: render_target_config.width,
height: render_target_config.height,
},
clear_pipeline,
atlas_clear_pipeline,
}
}
fn create_strips_buffer(device: &Device, required_strips_size: u64) -> Buffer {
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Strips Buffer"),
size: required_strips_size,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
}
fn create_clear_slot_indices_buffer(device: &Device, required_size: u64) -> Buffer {
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Slot Indices Buffer"),
size: required_size,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
}
fn create_config_buffer(
device: &Device,
render_size: &RenderSize,
alpha_texture_width: u32,
) -> Buffer {
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Config Buffer"),
contents: bytemuck::bytes_of(&Config {
width: render_size.width,
height: render_size.height,
strip_height: Tile::HEIGHT.into(),
alphas_tex_width_bits: alpha_texture_width.trailing_zeros(),
encoded_paints_tex_width_bits: alpha_texture_width.trailing_zeros(),
_padding: [0; 3],
}),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
})
}
fn create_winding_texture(device: &Device, width: u32, height: u32) -> Texture {
device.create_texture(&wgpu::TextureDescriptor {
label: Some("Winding Texture"),
size: Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R16Float,
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
})
}
fn create_tile_lines_buffer(device: &Device, required_size: u64) -> Buffer {
let size = required_size.max(size_of::<crate::gpu_winding::GpuTileLine>() as u64);
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Tile Lines Buffer"),
size,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
}
fn create_atlas_texture_array(
device: &Device,
width: u32,
height: u32,
atlas_count: u32,
) -> (Texture, TextureView) {
// Create a single texture array with multiple layers
let atlas_texture_array = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Atlas Texture Array"),
size: Extent3d {
width,
height,
depth_or_array_layers: atlas_count,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let atlas_texture_array_view = atlas_texture_array.create_view(&TextureViewDescriptor {
label: Some("Atlas Texture Array View"),
format: None,
dimension: Some(wgpu::TextureViewDimension::D2Array),
aspect: wgpu::TextureAspect::All,
base_mip_level: 0,
mip_level_count: None,
base_array_layer: 0,
array_layer_count: Some(atlas_count),
usage: None,
});
(atlas_texture_array, atlas_texture_array_view)
}
fn create_atlas_bind_group(
device: &Device,
atlas_bind_group_layout: &BindGroupLayout,
atlas_texture_array_view: &TextureView,
) -> BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Atlas Bind Group"),
layout: atlas_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(atlas_texture_array_view),
}],
})
}
fn create_encoded_paints_texture(device: &Device, width: u32, height: u32) -> Texture {
device.create_texture(&wgpu::TextureDescriptor {
label: Some("Encoded Paints Texture"),
size: Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba32Uint,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
})
}
fn create_encoded_paints_bind_group(
device: &Device,
encoded_paints_bind_group_layout: &BindGroupLayout,
encoded_paints_texture_view: &TextureView,
) -> BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Encoded Paints Bind Group"),
layout: encoded_paints_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(encoded_paints_texture_view),
}],
})
}
fn create_gradient_texture(device: &Device, width: u32, height: u32) -> Texture {
device.create_texture(&wgpu::TextureDescriptor {
label: Some("Gradient Texture"),
size: 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::COPY_DST,
view_formats: &[],
})
}
fn create_gradient_bind_group(
device: &Device,
gradient_bind_group_layout: &BindGroupLayout,
gradient_texture_view: &TextureView,
) -> BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Gradient Bind Group"),
layout: gradient_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(gradient_texture_view),
}],
})
}
fn create_strip_bind_groups(
device: &Device,
strip_bind_group_layout: &BindGroupLayout,
alphas_texture_view: &TextureView,
strip_config_buffer: &Buffer,
config_buffer: &Buffer,
strip_texture_views: &[TextureView],
) -> [BindGroup; 3] {
[
Self::create_strip_bind_group(
device,
strip_bind_group_layout,
alphas_texture_view,
strip_config_buffer,
&strip_texture_views[1],
),
Self::create_strip_bind_group(
device,
strip_bind_group_layout,
alphas_texture_view,
strip_config_buffer,
&strip_texture_views[0],
),
Self::create_strip_bind_group(
device,
strip_bind_group_layout,
alphas_texture_view,
config_buffer,
&strip_texture_views[1],
),
]
}
fn create_strip_bind_group(
device: &Device,
strip_bind_group_layout: &BindGroupLayout,
alphas_texture_view: &TextureView,
config_buffer: &Buffer,
strip_texture_view: &TextureView,
) -> BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Strip Bind Group"),
layout: strip_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(alphas_texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: config_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(strip_texture_view),
},
],
})
}
/// Prepare GPU buffers for rendering, given alphas.
///
/// Specifically, updates the alpha texture with `alphas` and the config buffer when
/// the rendering size changes.
fn prepare(
&mut self,
device: &Device,
queue: &Queue,
gradient_cache: &mut GradientRampCache,
encoded_paints: &[GpuEncodedPaint],
alphas: &mut Vec<u8>,
new_render_size: &RenderSize,
paint_idxs: &[u32],
) {
let max_texture_dimension_2d = device.limits().max_texture_dimension_2d;
// Still need to size winding texture based on alpha count (same number of values).
self.maybe_resize_winding_tex(device, queue, max_texture_dimension_2d, alphas.len());
self.maybe_resize_encoded_paints_tex(device, max_texture_dimension_2d, paint_idxs);
self.maybe_update_config_buffer(queue, max_texture_dimension_2d, new_render_size);
// Alpha upload is skipped — the GPU winding pass fills the winding texture.
self.upload_encoded_paints_texture(queue, encoded_paints);
if gradient_cache.has_changed() {
self.maybe_resize_gradient_tex(device, max_texture_dimension_2d, gradient_cache);
self.upload_gradient_texture(queue, gradient_cache);
gradient_cache.mark_synced();
}
}
/// Update the winding texture size if needed.
fn maybe_resize_winding_tex(
&mut self,
device: &Device,
queue: &Queue,
max_texture_dimension_2d: u32,
winding_value_count: usize,
) {
// R16Float: 1 pixel per winding value.
let required_height = u32::try_from(winding_value_count)
.unwrap()
.div_ceil(max_texture_dimension_2d)
.max(u32::from(Tile::HEIGHT)); // at least one band
let current_height = self.resources.winding_texture.height();
if required_height > current_height {
assert!(
required_height <= max_texture_dimension_2d,
"Winding texture height exceeds max texture dimensions"
);
let winding_texture = Self::create_winding_texture(
device,
max_texture_dimension_2d,
required_height,
);
self.resources.winding_texture_view =
winding_texture.create_view(&TextureViewDescriptor::default());
self.resources.winding_texture = winding_texture;
// Update strip bind groups to use the new winding texture.
self.resources.slot_bind_groups = Self::create_strip_bind_groups(
device,
&self.strip_bind_group_layout,
&self.resources.winding_texture_view,
&self.resources.slot_config_buffer,
&self.resources.view_config_buffer,
&self.resources.slot_texture_views,
);
}
// Update winding config uniform with current dimensions.
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct WindingConfig {
winding_tex_width: u32,
winding_tex_height: u32,
}
queue.write_buffer(
&self.resources.winding_config_buffer,
0,
bytemuck::bytes_of(&WindingConfig {
winding_tex_width: self.resources.winding_texture.width(),
winding_tex_height: self.resources.winding_texture.height(),
}),
);
}
/// Update the encoded paints texture size if needed.
fn maybe_resize_encoded_paints_tex(
&mut self,
device: &Device,
max_texture_dimension_2d: u32,
paint_idxs: &[u32],
) {
let required_texels = paint_idxs.last().unwrap();
let required_encoded_paints_height = required_texels.div_ceil(max_texture_dimension_2d);
debug_assert!(
self.resources.encoded_paints_texture.width() == max_texture_dimension_2d,
"Encoded paints texture width must match max texture dimensions"
);
let current_encoded_paints_height = self.resources.encoded_paints_texture.height();
if required_encoded_paints_height > current_encoded_paints_height {
assert!(
required_encoded_paints_height <= max_texture_dimension_2d,
"Encoded paints texture height exceeds max texture dimensions"
);
let required_encoded_paints_size =
(max_texture_dimension_2d * required_encoded_paints_height) << 4;
self.encoded_paints_data
.resize(required_encoded_paints_size as usize, 0);
let encoded_paints_texture = Self::create_encoded_paints_texture(
device,
max_texture_dimension_2d,
required_encoded_paints_height,
);
self.resources.encoded_paints_texture = encoded_paints_texture;
// Since the encoded paints texture has changed, we need to update the strip bind groups.
self.resources.encoded_paints_bind_group = Self::create_encoded_paints_bind_group(
device,
&self.encoded_paints_bind_group_layout,
&self
.resources
.encoded_paints_texture
.create_view(&TextureViewDescriptor::default()),
);
}
}
/// Update the gradient texture size if needed.
fn maybe_resize_gradient_tex(
&mut self,
device: &Device,
max_texture_dimension_2d: u32,
gradient_cache: &GradientRampCache,
) {
let gradient_pixels = (gradient_cache.luts_size() / 4) as u32; // 4 bytes per RGBA8 pixel
let required_gradient_height = gradient_pixels.div_ceil(max_texture_dimension_2d);
debug_assert!(
self.resources.gradient_texture.width() == max_texture_dimension_2d,
"Gradient texture width must match max texture dimensions"
);
let current_gradient_height = self.resources.gradient_texture.height();
if required_gradient_height > current_gradient_height {
assert!(
required_gradient_height <= max_texture_dimension_2d,
"Gradient texture height exceeds max texture dimensions"
);
let gradient_texture = Self::create_gradient_texture(
device,
max_texture_dimension_2d,
required_gradient_height,
);
self.resources.gradient_texture = gradient_texture;
// Since the gradient texture has changed, we need to update the gradient bind group.
self.resources.gradient_bind_group = Self::create_gradient_bind_group(
device,
&self.gradient_bind_group_layout,
&self
.resources
.gradient_texture
.create_view(&TextureViewDescriptor::default()),
);
}
}
/// Update config buffer if dimensions changed.
fn maybe_update_config_buffer(
&mut self,
queue: &Queue,
max_texture_dimension_2d: u32,
new_render_size: &RenderSize,
) {
if self.render_size != *new_render_size {
let config = Config {
width: new_render_size.width,
height: new_render_size.height,
strip_height: Tile::HEIGHT.into(),
alphas_tex_width_bits: max_texture_dimension_2d.trailing_zeros(),
encoded_paints_tex_width_bits: max_texture_dimension_2d.trailing_zeros(),
_padding: [0; 3],
};
let mut buffer = queue
.write_buffer_with(&self.resources.view_config_buffer, 0, SIZE_OF_CONFIG)
.expect("Buffer only ever holds `Config`");
buffer.copy_from_slice(bytemuck::bytes_of(&config));
self.render_size = new_render_size.clone();
}
}
/// Resize the texture array to accommodate more atlases.
fn maybe_resize_atlas_texture_array(
device: &Device,
encoder: &mut CommandEncoder,
resources: &mut GpuResources,
atlas_bind_group_layout: &BindGroupLayout,
required_atlas_count: u32,
) {
let Extent3d {
width,
height,
depth_or_array_layers: current_atlas_count,
} = resources.atlas_texture_array.size();
if required_atlas_count > current_atlas_count {
// Create new texture array with more layers
let (new_atlas_texture_array, new_atlas_texture_array_view) =
Self::create_atlas_texture_array(device, width, height, required_atlas_count);
// Copy existing atlas data from old texture array to new one
Self::copy_atlas_texture_data(
encoder,
&resources.atlas_texture_array,
&new_atlas_texture_array,
current_atlas_count,
width,
height,
);
// Update the bind group with the new texture array view
let new_atlas_bind_group = Self::create_atlas_bind_group(
device,
atlas_bind_group_layout,
&new_atlas_texture_array_view,
);
// Replace the old resources
resources.atlas_texture_array = new_atlas_texture_array;
resources.atlas_texture_array_view = new_atlas_texture_array_view;
resources.atlas_bind_group = new_atlas_bind_group;
}
}
/// Copy texture data from the old atlas texture array to a new one.
/// This is necessary when resizing the texture array to preserve existing atlas data.
fn copy_atlas_texture_data(
encoder: &mut CommandEncoder,
old_atlas_texture_array: &Texture,
new_atlas_texture_array: &Texture,
layer_count_to_copy: u32,
width: u32,
height: u32,
) {
// Copy all layers from old texture array to new texture array
encoder.copy_texture_to_texture(
wgpu::TexelCopyTextureInfo {
texture: old_atlas_texture_array,
mip_level: 0,
origin: wgpu::Origin3d { x: 0, y: 0, z: 0 },
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyTextureInfo {
texture: new_atlas_texture_array,
mip_level: 0,
origin: wgpu::Origin3d { x: 0, y: 0, z: 0 },
aspect: wgpu::TextureAspect::All,
},
Extent3d {
width,
height,
depth_or_array_layers: layer_count_to_copy,
},
);
}
/// Upload tile-line instances to the GPU buffer.
fn upload_tile_lines(
&mut self,
device: &Device,
queue: &Queue,
tile_lines: &[crate::gpu_winding::GpuTileLine],
) {
if tile_lines.is_empty() {
return;
}
let required_size = size_of_val(tile_lines) as u64;
if self.resources.tile_lines_buffer.size() < required_size {
self.resources.tile_lines_buffer =
Self::create_tile_lines_buffer(device, required_size);
}
queue.write_buffer(
&self.resources.tile_lines_buffer,
0,
bytemuck::cast_slice(tile_lines),
);
}
/// Upload encoded paints to the texture.
fn upload_encoded_paints_texture(&mut self, queue: &Queue, encoded_paints: &[GpuEncodedPaint]) {
let encoded_paints_texture = &self.resources.encoded_paints_texture;
let encoded_paints_texture_width = encoded_paints_texture.width();
let encoded_paints_texture_height = encoded_paints_texture.height();
GpuEncodedPaint::serialize_to_buffer(encoded_paints, &mut self.encoded_paints_data);
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: encoded_paints_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&self.encoded_paints_data,
wgpu::TexelCopyBufferLayout {
offset: 0,
// 16 bytes per RGBA32Uint texel (4 u32s × 4 bytes each), equivalent to bit shift of 4
bytes_per_row: Some(encoded_paints_texture_width << 4),
rows_per_image: Some(encoded_paints_texture_height),
},
Extent3d {
width: encoded_paints_texture_width,
height: encoded_paints_texture_height,
depth_or_array_layers: 1,
},
);
}
/// Upload gradient data to the texture.
fn upload_gradient_texture(&mut self, queue: &Queue, gradient_cache: &mut GradientRampCache) {
let gradient_texture = &self.resources.gradient_texture;
let gradient_texture_width = gradient_texture.width();
let gradient_texture_height = gradient_texture.height();
// Upload the gradient LUT data
if !gradient_cache.is_empty() {
let total_capacity = (gradient_texture_width * gradient_texture_height * 4) as usize;
// Take ownership of the luts to avoid copying, then resize for texture padding
let mut luts = gradient_cache.take_luts();
let old_luts_len = luts.len();
luts.resize(total_capacity, 0);
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: gradient_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&luts,
wgpu::TexelCopyBufferLayout {
offset: 0,
// 4 bytes per RGBA8 pixel
bytes_per_row: Some(gradient_texture_width << 2),
rows_per_image: Some(gradient_texture_height),
},
Extent3d {
width: gradient_texture_width,
height: gradient_texture_height,
depth_or_array_layers: 1,
},
);
// Restore the luts back to the cache
luts.truncate(old_luts_len);
gradient_cache.restore_luts(luts);
}
}
/// Upload the strip data by creating and assigning a new `self.resources.strips_buffer`.
fn upload_strips(&mut self, device: &Device, queue: &Queue, strips: &[GpuStrip]) {
let required_strips_size = size_of_val(strips) as u64;
self.resources.strips_buffer = Self::create_strips_buffer(device, required_strips_size);
// TODO: Consider using a staging belt to avoid an extra staging buffer allocation.
let mut buffer = queue
.write_buffer_with(
&self.resources.strips_buffer,
0,
required_strips_size.try_into().unwrap(),
)
.expect("Capacity handled in creation");
buffer.copy_from_slice(bytemuck::cast_slice(strips));
}
}
/// A struct containing references to the many objects needed to get work
/// scheduled onto the GPU.
struct RendererContext<'a> {
programs: &'a mut Programs,
device: &'a Device,
queue: &'a Queue,
encoder: &'a mut CommandEncoder,
view: &'a TextureView,
}
impl RendererContext<'_> {
/// Render tile-line instances into the winding texture.
fn do_winding_render_pass(
&mut self,
tile_lines: &[crate::gpu_winding::GpuTileLine],
) {
if tile_lines.is_empty() {
return;
}
self.programs
.upload_tile_lines(self.device, self.queue, tile_lines);
let mut render_pass = self.encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("Winding Render Pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view: &self.programs.resources.winding_texture_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,
occlusion_query_set: None,
timestamp_writes: None,
multiview_mask: None,
});
render_pass.set_pipeline(&self.programs.winding_pipeline);
render_pass.set_bind_group(0, &self.programs.resources.winding_bind_group, &[]);
render_pass
.set_vertex_buffer(0, self.programs.resources.tile_lines_buffer.slice(..));
render_pass.draw(0..4, 0..u32::try_from(tile_lines.len()).unwrap());
}
/// Render the strips to either the view or a slot texture (depending on `ix`).
fn do_strip_render_pass(
&mut self,
strips: &[GpuStrip],
ix: usize,
load: wgpu::LoadOp<wgpu::Color>,
) {
debug_assert!(ix < 3, "Invalid texture index");
if strips.is_empty() {
return;
}
// TODO: We currently allocate a new strips buffer for each render pass. A more efficient
// approach would be to re-use buffers or slices of a larger buffer.
self.programs.upload_strips(self.device, self.queue, strips);
let mut render_pass = self.encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("Render to Texture Pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view: if ix == 2 {
self.view
} else {
&self.programs.resources.slot_texture_views[ix]
},
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
multiview_mask: None,
});
render_pass.set_pipeline(&self.programs.strip_pipeline);
render_pass.set_bind_group(0, &self.programs.resources.slot_bind_groups[ix], &[]);
render_pass.set_bind_group(1, &self.programs.resources.atlas_bind_group, &[]);
render_pass.set_bind_group(2, &self.programs.resources.encoded_paints_bind_group, &[]);
render_pass.set_bind_group(3, &self.programs.resources.gradient_bind_group, &[]);
render_pass.set_vertex_buffer(0, self.programs.resources.strips_buffer.slice(..));
render_pass.draw(0..4, 0..u32::try_from(strips.len()).unwrap());
}
/// Clear specific slots from a slot texture.
fn do_clear_slots_render_pass(&mut self, ix: usize, slot_indices: &[u32]) {
if slot_indices.is_empty() {
return;
}
let resources = &mut self.programs.resources;
let size = size_of_val(slot_indices) as u64;
// TODO: We currently allocate a new strips buffer for each render pass. A more efficient
// approach would be to re-use buffers or slices of a larger buffer.
resources.clear_slot_indices_buffer =
Programs::create_clear_slot_indices_buffer(self.device, size);
// TODO: Consider using a staging belt to avoid an extra staging buffer allocation.
let mut buffer = self
.queue
.write_buffer_with(
&resources.clear_slot_indices_buffer,
0,
size.try_into().unwrap(),
)
.expect("Capacity handled in creation");
buffer.copy_from_slice(bytemuck::cast_slice(slot_indices));
{
let mut render_pass = self.encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("Clear Slots Render Pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view: &resources.slot_texture_views[ix],
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
// Don't clear the entire texture, just specific slots
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
multiview_mask: None,
});
render_pass.set_pipeline(&self.programs.clear_pipeline);
render_pass.set_bind_group(0, &resources.clear_bind_group, &[]);
render_pass.set_vertex_buffer(0, resources.clear_slot_indices_buffer.slice(..));
render_pass.draw(0..4, 0..u32::try_from(slot_indices.len()).unwrap());
}
}
}
impl RendererBackend for RendererContext<'_> {
/// Execute the render pass for clearing slots.
fn clear_slots(&mut self, texture_index: usize, slots: &[u32]) {
self.do_clear_slots_render_pass(texture_index, slots);
}
/// Execute the render pass for rendering strips.
fn render_strips(&mut self, strips: &[GpuStrip], target_index: usize, load_op: LoadOp) {
let wgpu_load_op = match load_op {
LoadOp::Load => wgpu::LoadOp::Load,
LoadOp::Clear => wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
};
self.do_strip_render_pass(strips, target_index, wgpu_load_op);
}
}
/// Trait for types that can write image data directly to the atlas texture.
///
/// This allows efficient uploading from different sources:
/// - `Pixmap`: Direct upload without intermediate texture
/// - `Texture`: Texture-to-texture copy
/// - Custom implementations for other image sources
pub trait AtlasWriter {
/// Get the width of the image.
fn width(&self) -> u32;
/// Get the height of the image.
fn height(&self) -> u32;
/// Write image data to a specific layer of an atlas texture array at the specified offset.
fn write_to_atlas_layer(
&self,
device: &Device,
queue: &Queue,
encoder: &mut CommandEncoder,
atlas_texture: &Texture,
layer: u32,
offset: [u32; 2],
width: u32,
height: u32,
);
}
/// Implementation for `wgpu::Texture` - uses texture-to-texture copy
impl AtlasWriter for Texture {
fn width(&self) -> u32 {
self.width()
}
fn height(&self) -> u32 {
self.height()
}
fn write_to_atlas_layer(
&self,
_device: &Device,
_queue: &Queue,
encoder: &mut CommandEncoder,
atlas_texture: &Texture,
layer: u32,
offset: [u32; 2],
width: u32,
height: u32,
) {
encoder.copy_texture_to_texture(
wgpu::TexelCopyTextureInfo {
texture: self,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyTextureInfo {
texture: atlas_texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: offset[0],
y: offset[1],
z: layer,
},
aspect: wgpu::TextureAspect::All,
},
Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
}
}
/// Implementation for `Pixmap` - direct upload to atlas
impl AtlasWriter for Pixmap {
fn width(&self) -> u32 {
self.width() as u32
}
fn height(&self) -> u32 {
self.height() as u32
}
fn write_to_atlas_layer(
&self,
_device: &Device,
queue: &Queue,
_encoder: &mut CommandEncoder,
atlas_texture: &Texture,
layer: u32,
offset: [u32; 2],
width: u32,
height: u32,
) {
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: atlas_texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: offset[0],
y: offset[1],
z: layer,
},
aspect: wgpu::TextureAspect::All,
},
self.data_as_u8_slice(),
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * width),
rows_per_image: Some(height),
},
Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
}
}
/// Implementation for `Arc<Pixmap>`
impl AtlasWriter for Arc<Pixmap> {
fn width(&self) -> u32 {
self.as_ref().width() as u32
}
fn height(&self) -> u32 {
self.as_ref().height() as u32
}
fn write_to_atlas_layer(
&self,
device: &Device,
queue: &Queue,
encoder: &mut CommandEncoder,
atlas_texture: &Texture,
layer: u32,
offset: [u32; 2],
width: u32,
height: u32,
) {
self.as_ref().write_to_atlas_layer(
device,
queue,
encoder,
atlas_texture,
layer,
offset,
width,
height,
);
}
}