blob: 1bcb269ef0082d7290709dc2c6ffceb68a848ace [file]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! `wgpu` backend for Vello GPU.
#![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::draw::{EXTERNAL_TEXTURE_SLOT_COUNT, ExternalTextureBindings, ExternalTextureRun};
use crate::render::common::IMAGE_PADDING;
use crate::util::RangedSlice;
use crate::{
ClearSettings, GpuStrip, LayersConfig, RenderError, RenderSettings, RenderSize, Resources,
TargetInit,
blend::{BlendStrip, GpuBlendInstance},
copy::GpuCopyInstance,
filter::{FilterContext, FilterInstanceData, FilterPassPlan},
gradient_cache::GradientRampCache,
paint::{PaintResolver, TextureSourceId},
render::{
Config,
common::{
DeviceLimits, GpuBlurredRoundedRect, GpuClearInstance, GpuEncodedImage,
GpuEncodedPaint, GpuLinearGradient, GpuRadialGradient, GpuSweepGradient,
ScratchBuffers, ScratchTexture, 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::{
Backend, IntermediateTextureAllocations, Schedule, ScheduleStorage, round::BlendOp,
},
target::{
BlendPassBindings, DrawPassBindings, DrawPassTarget, FilterPassBindings, LayerTextureId,
RootTarget, TextureParity,
},
};
use alloc::vec::Vec;
use alloc::{sync::Arc, vec};
use core::{fmt::Debug, num::NonZeroU64, ops::Range};
#[cfg(feature = "text")]
use glifo::PendingClearRect;
use hashbrown::{HashMap, hash_map::Entry};
use vello_common::color::{AlphaColor, Srgb};
use vello_common::image_cache::{ImageCache, ImageResource};
use vello_common::multi_atlas::{AtlasConfig, AtlasId};
use vello_common::{
TextureId,
encode::{
EncodedBlurredRoundedRectangle, EncodedGradient, EncodedKind, EncodedPaint,
MAX_GRADIENT_LUT_SIZE, RadialKind,
},
geometry::{RectU16, SizeU16},
paint::ImageSource,
peniko,
pixmap::Pixmap,
tile::Tile,
};
use wgpu::{
BindGroup, BindGroupLayout, BlendState, Buffer, BufferDescriptor, COPY_BUFFER_ALIGNMENT,
ColorTargetState, ColorWrites, CommandEncoder, Device, Extent3d, PipelineCompilationOptions,
Queue, RenderPassColorAttachment, RenderPassDescriptor, RenderPipeline, Sampler, 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: u16,
/// Height of the rendering target
pub height: u16,
}
/// Runtime bindings for [externally owned textures](`TextureId`) sampled by image paints.
#[derive(Debug, Default, Clone)]
pub struct TextureBindings {
views: HashMap<TextureId, TextureView>,
}
impl TextureBindings {
/// Create an empty binding map.
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Insert or replace a texture binding.
///
/// The [`TextureView`] must fit the following binding type.
///
/// ```ignore
/// wgpu::BindGroupLayoutEntry {
/// binding: 1,
/// visibility: wgpu::ShaderStages::FRAGMENT,
/// ty: wgpu::BindingType::Texture {
/// sample_type: wgpu::TextureSampleType::Float { filterable: false },
/// view_dimension: wgpu::TextureViewDimension::D2,
/// multisampled: false,
/// },
/// count: None,
/// }
/// ```
///
/// This means the view must be a non-array 2D view of a float-sampleable texture (e.g. integer
/// formats are rejected by wgpu at bind time), the underlying texture must include
/// [`wgpu::TextureUsages::TEXTURE_BINDING`], and only mip level 0 is read.
#[inline]
pub fn insert(&mut self, texture_id: TextureId, view: TextureView) {
self.views.insert(texture_id, view);
}
/// Get a texture binding.
#[inline]
fn get(&self, texture_id: TextureId) -> Option<&TextureView> {
self.views.get(&texture_id)
}
/// Remove a texture binding.
///
/// This returns the removed [`TextureView`] binding if it existed.
#[inline]
pub fn remove(&mut self, texture_id: TextureId) -> Option<TextureView> {
self.views.remove(&texture_id)
}
}
/// Vello GPU's renderer.
#[derive(Debug)]
pub struct Renderer {
/// Programs for rendering.
programs: Programs,
/// 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,
dummy_image_cache: Option<ImageCache>,
schedule_storage: ScheduleStorage,
scratch_buffers: ScratchBuffers,
layers_config: LayersConfig,
#[cfg(feature = "text")]
atlas_clear_scratch: Vec<u8>,
}
impl Renderer {
/// Creates a new renderer and its persistent resources.
pub fn new(device: &Device, render_target_config: &RenderTargetConfig) -> (Self, Resources) {
Self::new_with(device, render_target_config, RenderSettings::default())
}
/// Creates a new renderer and its persistent resources with specific settings.
pub fn new_with(
device: &Device,
render_target_config: &RenderTargetConfig,
settings: RenderSettings,
) -> (Self, Resources) {
super::common::maybe_warn_about_webgl_feature_conflict();
let mut settings = settings;
let limits = device.limits();
let device_limits = DeviceLimits {
max_texture_dimension_2d: u16::try_from(limits.max_texture_dimension_2d)
.unwrap_or(u16::MAX),
};
settings.memory_settings.normalize(&device_limits);
let resources = Resources::new(settings.memory_settings.image_atlas_config);
let resource_texture_dimension_2d = device_limits.resource_texture_dimension_2d();
// Estimate the maximum number of gradient cache entries based on the resource texture
// dimension and the maximum gradient LUT size - worst case scenario.
let max_gradient_cache_size = resource_texture_dimension_2d * resource_texture_dimension_2d
/ MAX_GRADIENT_LUT_SIZE as u32;
let gradient_cache = GradientRampCache::new(max_gradient_cache_size, settings.level);
let layer_config = settings.memory_settings.layers_config;
let renderer = Self {
programs: Programs::new(
device,
&resources.image_cache,
render_target_config,
layer_config,
resource_texture_dimension_2d,
),
gradient_cache,
encoded_paints: Vec::new(),
paint_idxs: Vec::new(),
dummy_image_cache: Some(ImageCache::new_dummy()),
schedule_storage: ScheduleStorage::default(),
scratch_buffers: ScratchBuffers::default(),
layers_config: layer_config,
#[cfg(feature = "text")]
atlas_clear_scratch: Vec::new(),
};
(renderer, resources)
}
/// Creates a depth texture view compatible with [`render`](Self::render).
///
/// The returned view has the same dimensions as `render_size`, uses
/// [`TextureFormat::Depth24Plus`](wgpu::TextureFormat::Depth24Plus), and can be used as a
/// render attachment.
pub fn create_depth_texture_view(device: &Device, render_size: &RenderSize) -> TextureView {
device
.create_texture(&wgpu::TextureDescriptor {
label: Some("Vello GPU Depth Texture"),
size: Extent3d {
width: u32::from(render_size.width.max(1)),
height: u32::from(render_size.height.max(1)),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth24Plus,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
})
.create_view(&TextureViewDescriptor::default())
}
/// Render `scene`.
///
/// Every [`TextureId`] referenced by the scene must have a binding; this returns
/// [`RenderError::MissingTextureBinding`] otherwise. See [`TextureBindings::insert`] for the
/// requirements on the bound texture views.
///
/// To render without any texture bindings, you can pass an empty [`TextureBindings`].
///
/// Passing `Some(depth_view)` enables depth buffering to reduce overdraw. The depth view must
/// use [`TextureFormat::Depth24Plus`](wgpu::TextureFormat::Depth24Plus), have the same
/// dimensions as `render_size`, have a sample count of one, and support
/// [`TextureUsages::RENDER_ATTACHMENT`](wgpu::TextureUsages::RENDER_ATTACHMENT). Pass `None`
/// to render without a depth buffer.
///
/// The [WebGPU render pass validation rules] require every color and depth attachment view in
/// a render pass to have matching render extents.
///
/// [WebGPU render pass validation rules]: https://gpuweb.github.io/gpuweb/#abstract-opdef-gpurenderpassdescriptor-valid-usage
pub fn render(
&mut self,
scene: &Scene,
resources: &mut Resources,
device: &Device,
queue: &Queue,
encoder: &mut CommandEncoder,
render_size: &RenderSize,
view: &TextureView,
depth_view: Option<&TextureView>,
texture_bindings: &TextureBindings,
target_init: TargetInit<'_>,
) -> Result<(), RenderError> {
#[cfg(feature = "text")]
{
resources.before_render(
self,
|renderer, glyph_renderer, atlas_count, atlas_config, atlas_id| {
renderer.render_to_atlas(
glyph_renderer,
atlas_count,
atlas_config,
device,
queue,
atlas_id,
texture_bindings,
)
},
|renderer, image_cache, upload, dst_x, dst_y| {
renderer.write_to_atlas(
image_cache,
device,
queue,
encoder,
upload.image_id,
&upload.pixmap,
Some([dst_x, dst_y]),
);
Ok(())
},
)?;
}
let result = self.render_scene(
scene,
device,
queue,
encoder,
render_size,
view,
depth_view,
&resources.image_cache,
&scene.encoded_paints,
target_init,
RootTarget::UserSurface,
texture_bindings,
);
#[cfg(feature = "text")]
resources.after_render(self, |renderer, rect| {
clear_atlas_region(queue, renderer, rect);
Ok::<(), RenderError>(())
})?;
result
}
/// 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`).
///
/// `texture_bindings` provides [externally bound textures](`TextureBindings`)
/// referenced by the scene. Pass `&TextureBindings::new()` if the scene does
/// not use any.
#[doc(hidden)]
pub fn render_to_atlas(
&mut self,
scene: &Scene,
atlas_count: u32,
atlas_config: AtlasConfig,
device: &Device,
queue: &Queue,
atlas_id: AtlasId,
texture_bindings: &TextureBindings,
) -> Result<(), RenderError> {
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render to Atlas Encoder"),
});
Programs::maybe_create_atlas_textures(device, &mut self.programs.resources, atlas_count);
let (atlas_width, atlas_height) = atlas_config.atlas_size;
let atlas_render_size = RenderSize {
width: atlas_width,
height: atlas_height,
};
let layer_view =
self.programs.resources.atlas_texture_views[atlas_id.as_u32() as usize].clone();
let encoded_paints = &scene.encoded_paints;
let dummy_image_cache = self
.dummy_image_cache
.take()
.expect("dummy image cache must exist");
let result = self.render_scene(
scene,
device,
queue,
&mut encoder,
&atlas_render_size,
&layer_view,
None,
&dummy_image_cache,
encoded_paints,
TargetInit::SrcOver,
RootTarget::AtlasLayer,
texture_bindings,
);
self.dummy_image_cache = Some(dummy_image_cache);
// 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
}
fn current_allocations(&self) -> IntermediateTextureAllocations {
IntermediateTextureAllocations {
layer_pages: self
.programs
.resources
.layer_textures
.each_ref()
.map(Vec::len),
scratch: self.programs.resources.scratch_texture.is_some(),
}
}
/// Shared render pipeline: prepares GPU resources, runs the scheduler against
/// the provided `view` at `render_size`, and maintains caches.
fn render_scene(
&mut self,
scene: &Scene,
device: &Device,
queue: &Queue,
encoder: &mut CommandEncoder,
render_size: &RenderSize,
view: &TextureView,
depth_view: Option<&TextureView>,
image_cache: &ImageCache,
encoded_paints: &[EncodedPaint],
target_init: TargetInit<'_>,
root_output_target: RootTarget,
texture_bindings: &TextureBindings,
) -> Result<(), RenderError> {
self.programs.depth_cleared_this_frame = false;
self.prepare_gpu_encoded_paints(
encoded_paints,
image_cache,
texture_bindings,
view.texture(),
)?;
let required_texture_size = self
.layers_config
.required_intermediate_texture_size(&scene.recorder)?;
// We currently only grow and never shrink textures, so max it with whatever
// we had in the previous run.
let texture_size = self
.programs
.resources
.texture_size
.max(required_texture_size);
let current_allocations = self.current_allocations();
let paint_resolver =
PaintResolver::new(encoded_paints, &self.paint_idxs).with_image_cache(image_cache);
let schedule = Schedule::try_new(
&mut self.schedule_storage,
scene,
root_output_target,
depth_view.is_some(),
paint_resolver,
texture_size,
current_allocations,
self.layers_config.max_textures,
)?;
self.programs
.prepare_intermediate_textures(device, &schedule);
// 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,
&self.schedule_storage.filter_context,
);
let strip_count = self
.schedule_storage
.buffers
.draw_buffers
.opaque
.strips()
.len()
+ self.schedule_storage.buffers.draw_buffers.strips.len();
self.programs
.strips_arena
.begin_frame(device, (strip_count * size_of::<GpuStrip>()) as u64);
let mut ctx = RendererContext {
programs: &mut self.programs,
device,
queue,
encoder,
view,
depth_view,
texture_bindings,
external_texture_bind_groups: HashMap::new(),
scratch_buffers: &mut self.scratch_buffers,
root_load_op: wgpu::LoadOp::Load,
};
ctx.init_root_clear(target_init, root_output_target);
crate::schedule::execute(
&mut ctx,
&mut self.schedule_storage,
schedule,
root_output_target,
)
.unwrap_or_else(|error| match error {});
ctx.finish_root_clear();
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,
resources: &mut Resources,
device: &Device,
queue: &Queue,
encoder: &mut CommandEncoder,
writer: &T,
) -> vello_common::paint::ImageId {
self.upload_image_with(
&mut resources.image_cache,
device,
queue,
encoder,
writer,
IMAGE_PADDING,
)
}
pub(crate) fn upload_image_with<T: AtlasWriter>(
&mut self,
image_cache: &mut ImageCache,
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 = image_cache.allocate(width, height, padding).unwrap();
self.write_to_atlas(image_cache, 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.
pub(crate) fn write_to_atlas<T: AtlasWriter>(
&mut self,
image_cache: &ImageCache,
device: &Device,
queue: &Queue,
encoder: &mut CommandEncoder,
image_id: vello_common::paint::ImageId,
writer: &T,
offset_override: Option<[u16; 2]>,
) {
let image_resource = image_cache.get(image_id).expect("Image resource not found");
Programs::maybe_create_atlas_textures(
device,
&mut self.programs.resources,
image_cache.atlas_count() as u32,
);
let offset = offset_override.unwrap_or(image_resource.offset);
let atlas_texture =
&self.programs.resources.atlas_textures[image_resource.atlas_id.as_u32() as usize];
writer.write_to_atlas(
device,
queue,
encoder,
atlas_texture,
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,
resources: &mut Resources,
encoder: &mut CommandEncoder,
image_id: vello_common::paint::ImageId,
) {
if let Some(image_resource) = resources.image_cache.deallocate(image_id) {
let padding = image_resource.padding;
self.clear_atlas_region(
encoder,
image_resource.atlas_id,
[
image_resource.offset[0] - padding,
image_resource.offset[1] - padding,
],
image_resource.width + padding * 2,
image_resource.height + padding * 2,
);
}
}
/// Returns an individual image atlas texture.
pub fn atlas_texture(&self, atlas_id: AtlasId) -> &Texture {
self.programs
.resources
.atlas_textures
.get(atlas_id.as_u32() as usize)
.unwrap()
}
/// Clear a specific region of the atlas texture.
fn clear_atlas_region(
&mut self,
encoder: &mut CommandEncoder,
atlas_id: AtlasId,
offset: [u16; 2],
width: u16,
height: u16,
) {
let layer_view =
self.programs.resources.atlas_texture_views[atlas_id.as_u32() as usize].clone();
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(
u32::from(offset[0]),
u32::from(offset[1]),
u32::from(width),
u32::from(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],
image_cache: &ImageCache,
texture_bindings: &TextureBindings,
render_target_texture: &Texture,
) -> Result<(), RenderError> {
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;
let gpu_paint = match paint {
EncodedPaint::Image(img) => match &img.source {
ImageSource::OpaqueId { id, .. } => {
let image_resource = image_cache.get(*id).unwrap();
self.encode_image_paint(img, image_resource)
}
ImageSource::ExternalTexture {
id, source_region, ..
} => {
let texture_view = texture_bindings
.get(*id)
.ok_or(RenderError::MissingTextureBinding(*id))?;
if texture_view.texture() == render_target_texture {
return Err(RenderError::TextureFeedbackLoop(*id));
}
self.encode_external_texture_paint(img, *source_region)
}
ImageSource::Pixmap(_) => {
panic!("pixmap image sources are not supported by Vello GPU")
}
},
EncodedPaint::Gradient(gradient) => {
let (gradient_start, gradient_width) =
self.gradient_cache.get_or_create_ramp(gradient);
self.encode_gradient_paint(gradient, gradient_width, gradient_start)
}
EncodedPaint::BlurredRoundedRect(blurred_rect) => {
Self::encode_blurred_rounded_rect_paint(blurred_rect)
}
};
current_idx += gpu_paint.size_texels();
self.encoded_paints[encoded_paint_idx] = gpu_paint;
}
self.paint_idxs[encoded_paints.len()] = current_idx;
Ok(())
}
fn encode_image_paint(
&self,
image: &vello_common::encode::EncodedImage,
image_resource: &ImageResource,
) -> GpuEncodedPaint {
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,
);
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_external_texture_paint(
&self,
image: &vello_common::encode::EncodedImage,
region: RectU16,
) -> GpuEncodedPaint {
let transform = image.transform.as_coeffs().map(|x| x as f32);
let image_size = pack_image_size(region.width(), region.height());
let image_offset = pack_image_offset(region.x0, region.y0);
let image_params = pack_image_params(
image.sampler.quality as u32,
image.sampler.x_extend as u32,
image.sampler.y_extend 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: 0,
})
}
fn encode_gradient_paint(
&self,
gradient: &EncodedGradient,
gradient_width: u32,
gradient_start: u32,
) -> GpuEncodedPaint {
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],
}),
}
}
fn encode_blurred_rounded_rect_paint(rect: &EncodedBlurredRoundedRectangle) -> GpuEncodedPaint {
GpuEncodedPaint::BlurredRoundedRect(GpuBlurredRoundedRect {
transform: rect.transform.as_coeffs().map(|x| x as f32),
color: rect.color.as_premul_rgba8().to_u32(),
invert: u32::from(rect.invert),
params0: [
rect.exponent,
rect.recip_exponent,
rect.scale,
rect.std_dev_inv,
],
params1: [rect.min_edge, rect.w, rect.h, rect.r1],
size: [rect.width, rect.height],
_padding1: [0, 0],
})
}
}
#[cfg(feature = "text")]
fn clear_atlas_region(queue: &Queue, renderer: &mut Renderer, rect: &PendingClearRect) {
// TODO: Can we optimize this more?
let byte_count = rect.width as usize * rect.height as usize * 4;
renderer.atlas_clear_scratch.resize(byte_count, 0);
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: renderer.atlas_texture(AtlasId::new(rect.page_index)),
mip_level: 0,
origin: wgpu::Origin3d {
x: rect.x as u32,
y: rect.y as u32,
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
&renderer.atlas_clear_scratch[..byte_count],
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(rect.width as u32 * 4),
rows_per_image: None,
},
Extent3d {
width: rect.width as u32,
height: rect.height as u32,
depth_or_array_layers: 1,
},
);
}
/// Defines the GPU resources and pipelines for rendering.
#[derive(Debug)]
struct Programs {
/// Intermediate strip pipeline.
intermediate_strip_pipeline: RenderPipeline,
/// Root alpha-strip pipeline.
alpha_strip_pipeline: RenderPipeline,
/// Root alpha-strip pipeline with depth testing.
depth_alpha_strip_pipeline: RenderPipeline,
/// Root opaque-strip pipeline.
opaque_strip_pipeline: RenderPipeline,
/// Whether the depth buffer has been cleared this frame.
depth_cleared_this_frame: bool,
/// 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 external textures.
external_texture_bind_group_layout: BindGroupLayout,
/// Bind group layout for filter data texture.
filter_bind_group_layout: BindGroupLayout,
/// Filter input layouts.
filter_input_bind_group_layouts: [BindGroupLayout; 2],
/// Filter sampler.
filter_sampler: Sampler,
/// Blend bind group layout.
blend_layer_bind_group_layout: BindGroupLayout,
/// Copy bind group layout.
copy_bind_group_layout: BindGroupLayout,
/// Strip bind group cache.
strip_layer_bind_groups: HashMap<(StripTargetKind, Option<LayerTextureId>), BindGroup>,
/// Blend bind group cache.
blend_layer_bind_groups: HashMap<BlendPassBindings, BindGroup>,
/// Filter bind group cache.
filter_pair_bind_groups: HashMap<FilterPassBindings, FilterPairBindGroups>,
/// Pipeline for applying filter effects.
filter_pipeline: RenderPipeline,
/// Layer-clear pipeline.
clear_pipeline: RenderPipeline,
/// User-target rectangle-clear pipeline.
root_clear_pipeline: RenderPipeline,
/// Pipeline for clearing atlas regions.
atlas_clear_pipeline: RenderPipeline,
/// Blend pipeline.
blend_pipeline: RenderPipeline,
/// Copy pipeline.
copy_pipeline: RenderPipeline,
/// GPU resources for rendering (created during prepare)
resources: GpuResources,
/// Arena holding all [`GpuStrip`] data.
strips_arena: StripBufferArena,
/// Dimensions of the rendering target
render_size: RenderSize,
/// Scratch buffer for staging encoded paints texture data.
encoded_paints_data: Vec<u8>,
/// Scratch buffer for staging filter data texture data.
filter_data: Vec<u8>,
}
#[derive(Debug)]
struct StripBufferArena {
buffer: Buffer,
capacity: u64,
cursor: u64,
}
fn create_arena_buffer(device: &Device, size: u64) -> Buffer {
device.create_buffer(&BufferDescriptor {
label: Some("Strips Arena"),
size,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
}
impl StripBufferArena {
fn new(device: &Device) -> Self {
Self {
buffer: create_arena_buffer(device, 0),
capacity: 0,
cursor: 0,
}
}
fn begin_frame(&mut self, device: &Device, frame_size: u64) {
self.cursor = 0;
if frame_size > self.capacity {
let new_capacity = frame_size
.max(self.capacity * 2)
.min(device.limits().max_buffer_size);
self.buffer = create_arena_buffer(device, new_capacity);
self.capacity = new_capacity;
}
}
fn alloc(&mut self, len: u64) -> u64 {
let offset = self.cursor.next_multiple_of(COPY_BUFFER_ALIGNMENT);
self.cursor = offset + len;
offset
}
}
/// Contains all GPU resources needed for rendering
#[derive(Debug)]
struct GpuResources {
/// Alpha texture.
alphas_texture: Texture,
/// Maximum width or height of packed resource textures.
resource_texture_dimension_2d: u32,
/// One 2D texture per image atlas.
atlas_textures: Vec<Texture>,
/// Default view corresponding to each image atlas texture.
atlas_texture_views: Vec<TextureView>,
/// Configured atlas dimensions.
atlas_size: (u16, u16),
/// Transparent 1x1 placeholder used for unoccupied external texture slots.
placeholder_external_texture_view: TextureView,
/// Bind group used when a draw does not sample an external texture.
empty_external_texture_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,
/// Texture holding serialized `GpuFilterData` for all filter layers.
filter_data_texture: Texture,
/// Bind group for the filter data texture.
filter_base_bind_group: BindGroup,
/// Intermediate texture size.
texture_size: SizeU16,
/// Root config buffer.
view_config_buffer: Buffer,
/// Layer config buffer.
layer_config_buffer: Buffer,
/// Layer textures by parity.
layer_textures: [Vec<TextureView>; 2],
/// Scratch texture.
scratch_texture: Option<ScratchTexture<TextureView>>,
/// Filter bind group.
filter_original_bind_group: BindGroup,
/// Copy bind group.
scratch_copy_bind_group: BindGroup,
}
#[derive(Debug)]
struct FilterPairBindGroups {
inputs: [BindGroup; 2],
copy_source: BindGroup,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum StripTargetKind {
Root,
Layer,
}
impl GpuResources {
fn layer_views(&self, layer_ids: [LayerTextureId; 2]) -> [&TextureView; 2] {
layer_ids.map(|id| self.layer_view(id))
}
fn layer_view(&self, id: LayerTextureId) -> &TextureView {
self.layer_textures[id.texture_parity.get_parity()]
.get(usize::from(id.page_index))
.unwrap()
}
fn scratch_view(&self) -> &TextureView {
self.scratch_texture.as_ref().unwrap().get()
}
}
const SIZE_OF_CONFIG: NonZeroU64 = NonZeroU64::new(size_of::<Config>() as u64).unwrap();
impl GpuStrip {
/// Vertex attributes for the strip
pub fn vertex_attributes() -> [wgpu::VertexAttribute; 6] {
wgpu::vertex_attr_array![
0 => Uint32,
1 => Uint32,
2 => Uint32,
3 => Uint32,
4 => Uint32,
5 => Uint32,
]
}
}
impl Programs {
fn new(
device: &Device,
image_cache: &ImageCache,
render_target_config: &RenderTargetConfig,
layer_config: LayersConfig,
resource_texture_dimension_2d: u32,
) -> 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::Uint,
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 external_texture_layout_entry = |binding| wgpu::BindGroupLayoutEntry {
binding,
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 external_texture_layout_entries = [external_texture_layout_entry(0)];
let external_texture_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("External Texture Bind Group Layout"),
entries: &external_texture_layout_entries,
});
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,
}],
});
let strip_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Strip Shader"),
source: wgpu::ShaderSource::Wgsl(vello_gpu_shaders::wgsl::RENDER.into()),
});
let clear_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Clear Shader"),
source: wgpu::ShaderSource::Wgsl(vello_gpu_shaders::wgsl::CLEAR.into()),
});
let strip_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Strip Pipeline Layout"),
bind_group_layouts: &[
Some(&strip_bind_group_layout),
Some(&external_texture_bind_group_layout),
Some(&encoded_paints_bind_group_layout),
Some(&gradient_bind_group_layout),
],
immediate_size: 0,
});
let clear_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Clear Pipeline Layout"),
bind_group_layouts: &[],
immediate_size: 0,
});
let depth_format = wgpu::TextureFormat::Depth24Plus;
let strip_vertex_state = wgpu::VertexBufferLayout {
array_stride: size_of::<GpuStrip>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &GpuStrip::vertex_attributes(),
};
let create_strip_pipeline =
|label, format, blend, depth_stencil: Option<wgpu::DepthStencilState>| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(&strip_pipeline_layout),
vertex: wgpu::VertexState {
module: &strip_shader,
entry_point: Some("vs_main"),
buffers: &[Some(strip_vertex_state.clone())],
compilation_options: PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &strip_shader,
entry_point: Some("fs_main"),
targets: &[Some(ColorTargetState {
format,
blend,
write_mask: ColorWrites::ALL,
})],
compilation_options: PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleStrip,
..Default::default()
},
depth_stencil,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
})
};
let depth_stencil = |depth_write_enabled| wgpu::DepthStencilState {
format: depth_format,
depth_write_enabled: Some(depth_write_enabled),
depth_compare: Some(wgpu::CompareFunction::LessEqual),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
};
let intermediate_strip_pipeline = create_strip_pipeline(
"Strip Intermediate Pipeline",
wgpu::TextureFormat::Rgba8Unorm,
Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
None,
);
let alpha_strip_pipeline = create_strip_pipeline(
"Strip Alpha Pipeline",
render_target_config.format,
Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
None,
);
let depth_alpha_strip_pipeline = create_strip_pipeline(
"Strip Depth Alpha Pipeline",
render_target_config.format,
Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
Some(depth_stencil(false)),
);
let opaque_strip_pipeline = create_strip_pipeline(
"Strip Opaque Pipeline",
render_target_config.format,
None,
Some(depth_stencil(true)),
);
let clear_vertex_state = wgpu::VertexBufferLayout {
array_stride: size_of::<GpuClearInstance>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &wgpu::vertex_attr_array![
0 => Uint32x2,
1 => Uint32x2,
2 => Uint32x2,
3 => Uint32,
],
};
let create_clear_pipeline = |label, format| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(&clear_pipeline_layout),
vertex: wgpu::VertexState {
module: &clear_shader,
entry_point: Some("vs_main"),
buffers: &[Some(clear_vertex_state.clone())],
compilation_options: PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &clear_shader,
entry_point: Some("fs_main"),
targets: &[Some(ColorTargetState {
format,
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 clear_pipeline =
create_clear_pipeline("Clear Pipeline", wgpu::TextureFormat::Rgba8Unorm);
let root_clear_pipeline =
create_clear_pipeline("Root Clear Pipeline", render_target_config.format);
// 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,
});
// TODO: Change the atlas clear pipeline to use the existing layer clear mechanism.
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_transparent"),
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 filter_texture_entry = 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,
};
let filter_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Filter Bind Group Layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Uint,
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
}],
});
let filter_input_bind_group_layouts = [
// Input texture and linear sampler.
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Filter Input Bind Group Layout"),
entries: &[
filter_texture_entry,
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
}),
// The original layer texture.
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Filter Original Texture Bind Group Layout"),
entries: &[filter_texture_entry],
}),
];
let filter_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Filter Shader"),
source: wgpu::ShaderSource::Wgsl(vello_gpu_shaders::wgsl::FILTER.into()),
});
let filter_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Filter Pipeline Layout"),
bind_group_layouts: &[
Some(&filter_bind_group_layout),
Some(&filter_input_bind_group_layouts[0]),
Some(&filter_input_bind_group_layouts[1]),
],
immediate_size: 0,
});
let filter_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Filter Pipeline"),
layout: Some(&filter_pipeline_layout),
vertex: wgpu::VertexState {
module: &filter_shader,
entry_point: Some("vs_main"),
buffers: &[Some(wgpu::VertexBufferLayout {
array_stride: size_of::<FilterInstanceData>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &wgpu::vertex_attr_array![
0 => Uint32,
1 => Uint32,
2 => Uint32,
3 => Uint32,
4 => Uint32,
5 => Uint32,
6 => Uint32,
7 => Uint32,
8 => Uint32,
],
})],
compilation_options: PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &filter_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(),
cache: None,
multiview_mask: None,
});
let blend_layer_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Blend Layer Textures 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::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Uint,
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
],
});
let copy_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Copy 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,
}],
});
let blend_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Blend Shader"),
source: wgpu::ShaderSource::Wgsl(vello_gpu_shaders::wgsl::BLEND.into()),
});
let copy_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Copy Shader"),
source: wgpu::ShaderSource::Wgsl(vello_gpu_shaders::wgsl::COPY.into()),
});
let blend_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Blend Pipeline Layout"),
bind_group_layouts: &[Some(&blend_layer_bind_group_layout)],
immediate_size: 0,
});
let copy_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Copy Pipeline Layout"),
bind_group_layouts: &[Some(&copy_bind_group_layout)],
immediate_size: 0,
});
let blend_vertex_state = wgpu::VertexBufferLayout {
array_stride: size_of::<GpuBlendInstance>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &wgpu::vertex_attr_array![
0 => Uint32,
1 => Uint32,
2 => Uint32,
3 => Uint32,
4 => Uint32,
5 => Uint32,
6 => Uint32,
7 => Uint32,
],
};
let copy_vertex_state = wgpu::VertexBufferLayout {
array_stride: size_of::<GpuCopyInstance>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &wgpu::vertex_attr_array![
0 => Uint32,
1 => Uint32,
2 => Uint32,
3 => Uint32,
],
};
let create_texture_op_pipeline =
|label,
shader_module: &wgpu::ShaderModule,
layout,
vertex_state: &wgpu::VertexBufferLayout<'_>| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(layout),
vertex: wgpu::VertexState {
module: shader_module,
entry_point: Some("vs_main"),
buffers: &[Some(vertex_state.clone())],
compilation_options: PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: shader_module,
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 blend_pipeline = create_texture_op_pipeline(
"Blend Pipeline",
&blend_shader,
&blend_pipeline_layout,
&blend_vertex_state,
);
let copy_pipeline = create_texture_op_pipeline(
"Copy Pipeline",
&copy_shader,
&copy_pipeline_layout,
&copy_vertex_state,
);
let texture_size = layer_config.min_texture_size;
let filter_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Filter Linear Sampler"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let layer_textures: [Vec<TextureView>; 2] = core::array::from_fn(|_| Vec::new());
let scratch_texture = None;
let placeholder_external_texture_view = Self::create_placeholder_external_texture(device);
let filter_original_bind_group = create_filter_original_texture_bind_group(
device,
&filter_input_bind_group_layouts[1],
&placeholder_external_texture_view,
);
let layer_config_buffer = Self::create_config_buffer(
device,
u32::from(texture_size.width()),
u32::from(texture_size.height()),
resource_texture_dimension_2d,
);
const INITIAL_ALPHA_TEXTURE_HEIGHT: u32 = 1;
let alphas_texture = Self::create_alphas_texture(
device,
resource_texture_dimension_2d,
INITIAL_ALPHA_TEXTURE_HEIGHT,
);
let view_config_buffer = Self::create_config_buffer(
device,
u32::from(render_target_config.width),
u32::from(render_target_config.height),
resource_texture_dimension_2d,
);
let AtlasConfig {
atlas_size: (atlas_width, atlas_height),
initial_atlas_count,
..
} = image_cache.atlas_manager().config();
let atlas_size = (*atlas_width, *atlas_height);
let atlas_textures: Vec<_> = (0..*initial_atlas_count)
.map(|_| Self::create_atlas_texture(device, *atlas_width, *atlas_height))
.collect();
let atlas_texture_views = atlas_textures
.iter()
.map(|texture| texture.create_view(&TextureViewDescriptor::default()))
.collect();
let empty_external_texture_bind_group = Self::create_external_texture_bind_group(
device,
&external_texture_bind_group_layout,
[&placeholder_external_texture_view; EXTERNAL_TEXTURE_SLOT_COUNT],
);
const INITIAL_ENCODED_PAINTS_TEXTURE_HEIGHT: u32 = 1;
let encoded_paints_data = vec![
0;
((resource_texture_dimension_2d * INITIAL_ENCODED_PAINTS_TEXTURE_HEIGHT) << 4)
as usize
];
let encoded_paints_texture = Self::create_encoded_paints_texture(
device,
resource_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,
resource_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()),
);
// TODO: We really should deduplicate handling of this this with encoded paints texture.
const INITIAL_FILTER_TEXTURE_HEIGHT: u32 = 1;
let filter_data = vec![
0_u8;
((resource_texture_dimension_2d * INITIAL_FILTER_TEXTURE_HEIGHT) << 4)
as usize
];
let filter_data_texture = Self::create_filter_data_texture(
device,
resource_texture_dimension_2d,
INITIAL_FILTER_TEXTURE_HEIGHT,
);
let filter_base_bind_group = Self::create_filter_base_bind_group(
device,
&filter_bind_group_layout,
&filter_data_texture.create_view(&TextureViewDescriptor::default()),
);
let scratch_copy_bind_group = Self::create_copy_bind_group(
device,
&copy_bind_group_layout,
&placeholder_external_texture_view,
);
let resources = GpuResources {
layer_textures,
scratch_texture,
filter_original_bind_group,
scratch_copy_bind_group,
layer_config_buffer,
alphas_texture,
resource_texture_dimension_2d,
atlas_textures,
atlas_texture_views,
atlas_size,
placeholder_external_texture_view,
empty_external_texture_bind_group,
encoded_paints_texture,
encoded_paints_bind_group,
gradient_texture,
gradient_bind_group,
filter_data_texture,
filter_base_bind_group,
texture_size,
view_config_buffer,
};
Self {
intermediate_strip_pipeline,
alpha_strip_pipeline,
depth_alpha_strip_pipeline,
opaque_strip_pipeline,
depth_cleared_this_frame: false,
strip_bind_group_layout,
encoded_paints_bind_group_layout,
gradient_bind_group_layout,
external_texture_bind_group_layout,
filter_bind_group_layout,
filter_pipeline,
blend_pipeline,
copy_pipeline,
resources,
strips_arena: StripBufferArena::new(device),
encoded_paints_data,
filter_data,
render_size: RenderSize {
width: render_target_config.width,
height: render_target_config.height,
},
clear_pipeline,
root_clear_pipeline,
atlas_clear_pipeline,
filter_input_bind_group_layouts,
filter_sampler,
blend_layer_bind_group_layout,
copy_bind_group_layout,
strip_layer_bind_groups: HashMap::new(),
blend_layer_bind_groups: HashMap::new(),
filter_pair_bind_groups: HashMap::new(),
}
}
fn create_intermediate_texture(
device: &Device,
size: SizeU16,
label: &'static str,
) -> TextureView {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: Extent3d {
width: u32::from(size.width()),
height: u32::from(size.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,
view_formats: &[],
});
texture.create_view(&TextureViewDescriptor::default())
}
fn prepare_intermediate_textures(&mut self, device: &Device, schedule: &Schedule) {
let requirements = schedule.intermediate_texture_requirements();
let texture_size = requirements.size;
let current_size = self.resources.texture_size;
let allocations = requirements.allocations;
let layer_pages = allocations.layer_pages;
let scratch_required = allocations.scratch;
let size_changed = current_size != texture_size;
if size_changed {
self.resources.layer_config_buffer = Self::create_config_buffer(
device,
u32::from(texture_size.width()),
u32::from(texture_size.height()),
self.resources.resource_texture_dimension_2d,
);
}
for (index, textures) in self.resources.layer_textures.iter_mut().enumerate() {
let required_page_count = layer_pages[index];
// Note: Currently, `texture_size` only grows across frames, so if this condition
// is true it means that the new required size is larger than what we currently have.
if size_changed {
for texture in textures.iter_mut() {
*texture = Self::create_intermediate_texture(
device,
texture_size,
"Layer Intermediate Texture",
);
}
}
textures.extend((textures.len()..required_page_count).map(|_| {
Self::create_intermediate_texture(
device,
texture_size,
"Layer Intermediate Texture",
)
}));
}
let scratch_changed = (self.resources.scratch_texture.is_some() && size_changed)
|| (self.resources.scratch_texture.is_none() && scratch_required);
if scratch_changed {
self.resources.scratch_texture = Some(ScratchTexture::new(
Self::create_intermediate_texture(device, texture_size, "Scratch Texture"),
));
}
self.resources.texture_size = texture_size;
if size_changed {
self.clear_layer_bind_group_caches();
}
if scratch_changed {
self.update_scratch_bind_groups(device);
}
}
fn clear_layer_bind_group_caches(&mut self) {
self.strip_layer_bind_groups.clear();
self.blend_layer_bind_groups.clear();
self.filter_pair_bind_groups.clear();
}
fn update_scratch_bind_groups(&mut self, device: &Device) {
let scratch_view = self.resources.scratch_view();
let filter_original_bind_group = create_filter_original_texture_bind_group(
device,
&self.filter_input_bind_group_layouts[1],
scratch_view,
);
let scratch_copy_bind_group =
Self::create_copy_bind_group(device, &self.copy_bind_group_layout, scratch_view);
self.resources.filter_original_bind_group = filter_original_bind_group;
self.resources.scratch_copy_bind_group = scratch_copy_bind_group;
}
fn create_blend_layer_bind_group(
device: &Device,
layout: &BindGroupLayout,
layer_texture_views: [&TextureView; 2],
alphas_texture_view: &TextureView,
) -> BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Blend Layer Textures Bind Group"),
layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(layer_texture_views[0]),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(layer_texture_views[1]),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(alphas_texture_view),
},
],
})
}
fn create_copy_bind_group(
device: &Device,
layout: &BindGroupLayout,
source_texture_view: &TextureView,
) -> BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Copy Source Bind Group"),
layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(source_texture_view),
}],
})
}
fn create_config_buffer(
device: &Device,
width: u32,
height: u32,
alpha_texture_width: u32,
) -> Buffer {
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Config Buffer"),
contents: bytemuck::bytes_of(&Config {
width,
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(),
strip_offset_x: 0,
strip_offset_y: 0,
negate_ndc: 0,
}),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
})
}
fn create_alphas_texture(device: &Device, width: u32, height: u32) -> Texture {
device.create_texture(&wgpu::TextureDescriptor {
label: Some("Alpha 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_atlas_texture(device: &Device, width: u16, height: u16) -> Texture {
device.create_texture(&wgpu::TextureDescriptor {
label: Some("Atlas Texture"),
size: Extent3d {
width: u32::from(width),
height: u32::from(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
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
})
}
fn create_filter_data_texture(device: &Device, width: u32, height: u32) -> Texture {
device.create_texture(&wgpu::TextureDescriptor {
label: Some("Filter Data 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_filter_base_bind_group(
device: &Device,
filter_bind_group_layout: &BindGroupLayout,
filter_texture_view: &TextureView,
) -> BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Filter Base Bind Group"),
layout: filter_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(filter_texture_view),
}],
})
}
fn create_placeholder_external_texture(device: &Device) -> TextureView {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Placeholder External Texture"),
size: Extent3d {
width: 1,
height: 1,
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,
view_formats: &[],
});
texture.create_view(&TextureViewDescriptor::default())
}
fn create_external_texture_bind_group(
device: &Device,
external_texture_bind_group_layout: &BindGroupLayout,
texture_views: [&TextureView; EXTERNAL_TEXTURE_SLOT_COUNT],
) -> BindGroup {
let entries = [wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(texture_views[0]),
}];
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("External Texture Bind Group"),
layout: external_texture_bind_group_layout,
entries: &entries,
})
}
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_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],
filter_context: &FilterContext,
) {
let resource_texture_dimension_2d = self.resources.resource_texture_dimension_2d;
self.maybe_resize_alphas_tex(device, resource_texture_dimension_2d, alphas.len());
self.maybe_resize_encoded_paints_tex(device, resource_texture_dimension_2d, paint_idxs);
self.maybe_resize_filter_tex(device, resource_texture_dimension_2d, filter_context);
self.maybe_update_config_buffer(queue, resource_texture_dimension_2d, new_render_size);
self.upload_alpha_texture(queue, alphas);
self.upload_encoded_paints_texture(queue, encoded_paints, paint_idxs);
self.upload_filter_texture(queue, filter_context);
if gradient_cache.has_changed() {
self.maybe_resize_gradient_tex(device, resource_texture_dimension_2d, gradient_cache);
self.upload_gradient_texture(queue, gradient_cache);
gradient_cache.mark_synced();
}
}
fn maybe_resize_filter_tex(
&mut self,
device: &Device,
resource_texture_dimension_2d: u32,
filter_context: &FilterContext,
) {
let Some(required_filter_height) =
filter_context.required_filter_data_height(resource_texture_dimension_2d)
else {
return;
};
debug_assert!(
self.resources.filter_data_texture.width() == resource_texture_dimension_2d,
"Filter texture width must match resource texture dimensions"
);
let current_filter_height = self.resources.filter_data_texture.height();
if required_filter_height > current_filter_height {
let required_filter_size =
(resource_texture_dimension_2d * required_filter_height) << 4;
self.filter_data.resize(required_filter_size as usize, 0);
let filter_texture = Self::create_filter_data_texture(
device,
resource_texture_dimension_2d,
required_filter_height,
);
self.resources.filter_data_texture = filter_texture;
self.resources.filter_base_bind_group = Self::create_filter_base_bind_group(
device,
&self.filter_bind_group_layout,
&self
.resources
.filter_data_texture
.create_view(&TextureViewDescriptor::default()),
);
}
}
/// Update the alpha texture size if needed.
fn maybe_resize_alphas_tex(
&mut self,
device: &Device,
resource_texture_dimension_2d: u32,
alphas_len: usize,
) {
let required_alpha_height = u32::try_from(alphas_len)
.unwrap()
// There are 16 1-byte alpha values per texel.
.div_ceil(resource_texture_dimension_2d << 4);
debug_assert!(
self.resources.alphas_texture.width() == resource_texture_dimension_2d,
"Alpha texture width must match resource texture dimensions"
);
let current_alpha_height = self.resources.alphas_texture.height();
if required_alpha_height > current_alpha_height {
// We need to resize the alpha texture to fit the new alpha data.
assert!(
required_alpha_height <= resource_texture_dimension_2d,
"Alpha texture height exceeds resource texture dimensions"
);
// The alpha texture encodes 16 1-byte alpha values per texel, with 4 alpha values packed in each channel
let alphas_texture = Self::create_alphas_texture(
device,
resource_texture_dimension_2d,
required_alpha_height,
);
self.resources.alphas_texture = alphas_texture;
self.strip_layer_bind_groups.clear();
self.blend_layer_bind_groups.clear();
}
}
/// Update the encoded paints texture size if needed.
fn maybe_resize_encoded_paints_tex(
&mut self,
device: &Device,
resource_texture_dimension_2d: u32,
paint_idxs: &[u32],
) {
let required_texels = paint_idxs.last().unwrap();
let required_encoded_paints_height =
required_texels.div_ceil(resource_texture_dimension_2d);
debug_assert!(
self.resources.encoded_paints_texture.width() == resource_texture_dimension_2d,
"Encoded paints texture width must match resource 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 <= resource_texture_dimension_2d,
"Encoded paints texture height exceeds resource texture dimensions"
);
let required_encoded_paints_size =
(resource_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,
resource_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,
resource_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(resource_texture_dimension_2d);
debug_assert!(
self.resources.gradient_texture.width() == resource_texture_dimension_2d,
"Gradient texture width must match resource texture dimensions"
);
let current_gradient_height = self.resources.gradient_texture.height();
if required_gradient_height > current_gradient_height {
assert!(
required_gradient_height <= resource_texture_dimension_2d,
"Gradient texture height exceeds resource texture dimensions"
);
let gradient_texture = Self::create_gradient_texture(
device,
resource_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,
resource_texture_dimension_2d: u32,
new_render_size: &RenderSize,
) {
if self.render_size != *new_render_size {
let config = Config {
width: u32::from(new_render_size.width),
height: u32::from(new_render_size.height),
strip_height: Tile::HEIGHT.into(),
alphas_tex_width_bits: resource_texture_dimension_2d.trailing_zeros(),
encoded_paints_tex_width_bits: resource_texture_dimension_2d.trailing_zeros(),
strip_offset_x: 0,
strip_offset_y: 0,
negate_ndc: 0,
};
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();
}
}
/// Create any newly allocated atlas textures.
fn maybe_create_atlas_textures(
device: &Device,
resources: &mut GpuResources,
required_atlas_count: u32,
) {
let (width, height) = resources.atlas_size;
while resources.atlas_textures.len() < required_atlas_count as usize {
let texture = Self::create_atlas_texture(device, width, height);
let texture_view = texture.create_view(&TextureViewDescriptor::default());
resources.atlas_textures.push(texture);
resources.atlas_texture_views.push(texture_view);
}
}
fn create_run_external_texture_bind_group(
&self,
device: &Device,
texture_views: [&TextureView; EXTERNAL_TEXTURE_SLOT_COUNT],
) -> BindGroup {
Self::create_external_texture_bind_group(
device,
&self.external_texture_bind_group_layout,
texture_views,
)
}
/// Upload alpha data to the texture.
fn upload_alpha_texture(&mut self, queue: &Queue, alphas: &mut Vec<u8>) {
if alphas.is_empty() {
return;
}
let texture_width = self.resources.alphas_texture.width();
let texture_height = self.resources.alphas_texture.height();
let row_bytes = (texture_width << 4) as usize;
let rows = alphas
.len()
.div_ceil(row_bytes)
.min(texture_height as usize);
let original_len = alphas.len();
// Temporarily pad the last row with zeros before uploading.
alphas.resize(rows * row_bytes, 0);
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &self.resources.alphas_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
alphas,
wgpu::TexelCopyBufferLayout {
offset: 0,
// 16 bytes per RGBA32Uint texel (4 u32s × 4 bytes each), which is equivalent to
// a bit shift of 4.
bytes_per_row: Some(texture_width << 4),
rows_per_image: Some(rows as u32),
},
Extent3d {
width: texture_width,
height: rows as u32,
depth_or_array_layers: 1,
},
);
// Truncate back to the original size.
alphas.truncate(original_len);
}
/// Upload encoded paints to the texture.
fn upload_encoded_paints_texture(
&mut self,
queue: &Queue,
encoded_paints: &[GpuEncodedPaint],
paint_idxs: &[u32],
) {
if encoded_paints.is_empty() {
return;
}
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();
let used_texels = *paint_idxs.last().unwrap() as usize;
let rows = used_texels
.div_ceil(encoded_paints_texture_width as usize)
.min(encoded_paints_texture_height as usize);
GpuEncodedPaint::serialize_to_buffer(
encoded_paints,
&mut self.encoded_paints_data[..rows * encoded_paints_texture_width as usize * 16],
);
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: encoded_paints_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&self.encoded_paints_data[..rows * encoded_paints_texture_width as usize * 16],
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(rows as u32),
},
Extent3d {
width: encoded_paints_texture_width,
height: rows as u32,
depth_or_array_layers: 1,
},
);
}
fn upload_filter_texture(&mut self, queue: &Queue, filter_context: &FilterContext) {
if filter_context.is_empty() {
return;
}
let filter_texture = &self.resources.filter_data_texture;
let width = filter_texture.width();
let height = filter_texture.height();
let used_texels = filter_context.total_texels() as usize;
let rows = used_texels.div_ceil(width as usize).min(height as usize);
filter_context.serialize_to_buffer(&mut self.filter_data[..rows * width as usize * 16]);
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: filter_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&self.filter_data[..rows * width as usize * 16],
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(width << 4),
rows_per_image: Some(rows as u32),
},
Extent3d {
width,
height: rows as u32,
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);
}
}
/// Uploads two strip slices (opaque then alpha) into the frame arena and
/// returns the byte range of the uploaded data.
fn upload_strip_pair(
&mut self,
queue: &Queue,
opaque_strips: &[GpuStrip],
alpha_strips: RangedSlice<'_, GpuStrip>,
) -> Range<u64> {
let opaque_len = size_of_val(opaque_strips) as u64;
let alpha_len = (alpha_strips.len() * size_of::<GpuStrip>()) as u64;
let total_len = opaque_len + alpha_len;
if total_len == 0 {
return 0..0;
}
let arena = &mut self.strips_arena;
let offset = arena.alloc(total_len);
let size = NonZeroU64::new(total_len).expect("total length is non-zero");
// TODO: Consider using a staging belt to avoid an extra staging buffer allocation.
let mut view = queue
.write_buffer_with(&arena.buffer, offset, size)
.expect("Capacity handled in creation");
if opaque_len > 0 {
view.slice(..opaque_len as usize)
.copy_from_slice(bytemuck::cast_slice(opaque_strips));
}
let mut pos = opaque_len as usize;
for strips in alpha_strips.slices() {
let bytes = bytemuck::cast_slice(strips);
view.slice(pos..pos + bytes.len()).copy_from_slice(bytes);
pos += bytes.len();
}
offset..offset + total_len
}
}
/// 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,
depth_view: Option<&'a TextureView>,
texture_bindings: &'a TextureBindings,
external_texture_bind_groups: HashMap<ExternalTextureBindings, BindGroup>,
scratch_buffers: &'a mut ScratchBuffers,
root_load_op: wgpu::LoadOp<wgpu::Color>,
}
impl RendererContext<'_> {
fn init_root_clear(&mut self, target_init: TargetInit<'_>, root_target: RootTarget) {
self.root_load_op = match target_init {
TargetInit::SrcOver => wgpu::LoadOp::Load,
TargetInit::Clear(ClearSettings::Viewport { color }) => {
wgpu::LoadOp::Clear(clear_color(color))
}
TargetInit::Clear(clear @ ClearSettings::Rects { .. }) => {
self.clear_pass_inner(DrawPassTarget::Root(root_target), clear);
wgpu::LoadOp::Load
}
};
}
fn finish_root_clear(&mut self) {
// If the load mode was set to clear but no actual drawing instructions were
// emitted, we still need to start a pass to clear the output.
if let wgpu::LoadOp::Clear(color) = self.take_root_load_op() {
Self::clear_full_target(self.encoder, self.view, color);
}
}
fn take_root_load_op(&mut self) -> wgpu::LoadOp<wgpu::Color> {
core::mem::replace(&mut self.root_load_op, wgpu::LoadOp::Load)
}
fn clear_full_target(encoder: &mut CommandEncoder, view: &TextureView, color: wgpu::Color) {
encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("Clear Target"),
color_attachments: &[Some(RenderPassColorAttachment {
view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(color),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
multiview_mask: None,
});
}
fn external_texture_bind_group_for_textures(
&mut self,
bindings: ExternalTextureBindings,
) -> &BindGroup {
let atlas_texture_views = &self.programs.resources.atlas_texture_views;
let placeholder = &self.programs.resources.placeholder_external_texture_view;
let texture_bindings = self.texture_bindings;
match self.external_texture_bind_groups.entry(bindings) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
let texture_sources = bindings.as_array();
let texture_views = core::array::from_fn(|slot| match texture_sources[slot] {
Some(TextureSourceId::Atlas(atlas_id)) => {
atlas_texture_views[atlas_id.as_u32() as usize].clone()
}
Some(TextureSourceId::External(texture_id)) => texture_bindings
.get(texture_id)
.expect("external texture binding was validated during paint preparation")
.clone(),
None => placeholder.clone(),
});
let bind_group = self
.programs
.create_run_external_texture_bind_group(self.device, texture_views.each_ref());
entry.insert(bind_group)
}
}
}
/// Render the strips to the specified render target.
fn strip_pass_inner(
&mut self,
opaque_strips: &[GpuStrip],
alpha_strips: RangedSlice<'_, GpuStrip>,
external_texture_runs: &[ExternalTextureRun],
target: DrawPassTarget,
child_layer_texture: Option<LayerTextureId>,
) {
let opaque_count = opaque_strips.len();
let alpha_count = alpha_strips.len();
if opaque_count == 0 && alpha_count == 0 {
return;
}
// Create bind groups for all external textures used by this pass.
for run in external_texture_runs {
self.external_texture_bind_group_for_textures(run.bindings);
}
let strips_range = self
.programs
.upload_strip_pair(self.queue, opaque_strips, alpha_strips);
let opaque_count = opaque_count as u32;
let alpha_count = alpha_count as u32;
let color_load_op = if matches!(target, DrawPassTarget::Root(_)) {
self.take_root_load_op()
} else {
// For layer targets we always want to load.
wgpu::LoadOp::Load
};
let (view, config_buffer, bind_group_target) = match target {
DrawPassTarget::Root(_) => (
self.view,
&self.programs.resources.view_config_buffer,
StripTargetKind::Root,
),
DrawPassTarget::Layer(id) => (
self.programs.resources.layer_view(id),
&self.programs.resources.layer_config_buffer,
StripTargetKind::Layer,
),
};
let bind_group_key = (bind_group_target, child_layer_texture);
if !self
.programs
.strip_layer_bind_groups
.contains_key(&bind_group_key)
{
let child_layer_view = child_layer_texture.map_or(
&self.programs.resources.placeholder_external_texture_view,
|id| self.programs.resources.layer_view(id),
);
let alphas_texture_view = self
.programs
.resources
.alphas_texture
.create_view(&TextureViewDescriptor::default());
let bind_group = Programs::create_strip_bind_group(
self.device,
&self.programs.strip_bind_group_layout,
&alphas_texture_view,
config_buffer,
child_layer_view,
);
self.programs
.strip_layer_bind_groups
.insert(bind_group_key, bind_group);
}
let bind_group = &self.programs.strip_layer_bind_groups[&bind_group_key];
let enable_opaque = self.depth_view.is_some() && target.enable_opaque();
let depth_stencil_attachment = if enable_opaque {
let depth_load = if self.programs.depth_cleared_this_frame {
wgpu::LoadOp::Load
} else {
self.programs.depth_cleared_this_frame = true;
wgpu::LoadOp::Clear(1.0)
};
Some(wgpu::RenderPassDepthStencilAttachment {
view: self
.depth_view
.expect("enabled depth buffering requires a depth texture view"),
depth_ops: Some(wgpu::Operations {
load: depth_load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
})
} else {
None
};
let mut render_pass = self.encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("Render to Texture Pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: color_load_op,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment,
occlusion_query_set: None,
timestamp_writes: None,
multiview_mask: None,
});
render_pass.set_bind_group(0, 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
.strips_arena
.buffer
.slice(strips_range.clone()),
);
let draw_strip_runs = |render_pass: &mut wgpu::RenderPass<'_>, first_instance, count| {
if external_texture_runs.is_empty() {
render_pass.set_bind_group(
1,
&self.programs.resources.empty_external_texture_bind_group,
&[],
);
render_pass.draw(0..4, first_instance..first_instance + count);
return;
}
// Each run is drawn with different external texture bindings. Runs go from
// `run.strips_start` to the next run's `strips_start`; the last run goes to the end of
// the strips buffer.
for (i, run) in external_texture_runs.iter().enumerate() {
let external_texture_bind_group = self
.external_texture_bind_groups
.get(&run.bindings)
.unwrap();
render_pass.set_bind_group(1, external_texture_bind_group, &[]);
let start = u32::try_from(run.strips_start).unwrap();
let end = external_texture_runs
.get(i + 1)
.map_or(count, |next| u32::try_from(next.strips_start).unwrap());
render_pass.draw(0..4, first_instance + start..first_instance + end);
}
};
if opaque_count > 0 {
// Opaque pass
debug_assert!(
enable_opaque,
"opaque strips require the final view depth attachment"
);
render_pass.set_pipeline(&self.programs.opaque_strip_pipeline);
draw_strip_runs(&mut render_pass, 0, opaque_count);
}
if alpha_count > 0 {
// Alpha pass
if matches!(target, DrawPassTarget::Root(RootTarget::UserSurface)) {
let pipeline = if enable_opaque {
&self.programs.depth_alpha_strip_pipeline
} else {
&self.programs.alpha_strip_pipeline
};
render_pass.set_pipeline(pipeline);
} else {
render_pass.set_pipeline(&self.programs.intermediate_strip_pipeline);
}
draw_strip_runs(&mut render_pass, opaque_count, alpha_count);
}
}
fn texture_size(&self) -> SizeU16 {
self.programs.resources.texture_size
}
fn blend_pass_inner(
&mut self,
blends: RangedSlice<'_, BlendOp>,
blend_strips: &[BlendStrip],
bindings: BlendPassBindings,
) {
let texture_size = self.texture_size();
let resources = &self.programs.resources;
let blend_layer_bind_group = match self.programs.blend_layer_bind_groups.entry(bindings) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
let alphas_texture_view = resources
.alphas_texture
.create_view(&TextureViewDescriptor::default());
entry.insert(Programs::create_blend_layer_bind_group(
self.device,
&self.programs.blend_layer_bind_group_layout,
resources.layer_views([
bindings.layer_id(TextureParity::Even),
bindings.layer_id(TextureParity::Odd),
]),
&alphas_texture_view,
))
}
};
self.scratch_buffers.blend_instances.clear();
for blend in blends.iter() {
if let Some(range) = &blend.clip_strips {
self.scratch_buffers.blend_instances.extend(
blend_strips[usize::try_from(range.start).unwrap()
..usize::try_from(range.end).unwrap()]
.iter()
.copied()
.map(|strip| GpuBlendInstance::new(blend, Some(strip), texture_size)),
);
} else {
self.scratch_buffers
.blend_instances
.push(GpuBlendInstance::new(blend, None, texture_size));
}
}
let instance_count = u32::try_from(self.scratch_buffers.blend_instances.len()).unwrap();
let instance_buffer = self
.device
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Blend Instances Buffer"),
contents: bytemuck::cast_slice(&self.scratch_buffers.blend_instances),
usage: wgpu::BufferUsages::VERTEX,
});
{
let mut render_pass = self.encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("Blend To Scratch"),
color_attachments: &[Some(RenderPassColorAttachment {
view: self.programs.resources.scratch_view(),
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
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.blend_pipeline);
render_pass.set_bind_group(0, &*blend_layer_bind_group, &[]);
render_pass.set_vertex_buffer(0, instance_buffer.slice(..));
render_pass.draw(0..4, 0..instance_count);
}
self.scratch_buffers.copy_instances.clear();
self.scratch_buffers.copy_instances.extend(
self.scratch_buffers
.blend_instances
.iter()
.copied()
.map(GpuBlendInstance::copy_from_scratch),
);
encode_copy_pass(
self.device,
self.encoder,
&self.programs.copy_pipeline,
&self.scratch_buffers.copy_instances,
&self.programs.resources.scratch_copy_bind_group,
self.programs.resources.layer_view(bindings.target()),
"Copy Blend Scratch To Layer",
);
}
fn filter_pass_inner(&mut self, plan: &FilterPassPlan, bindings: FilterPassBindings) {
let resources = &self.programs.resources;
if !self
.programs
.filter_pair_bind_groups
.contains_key(&bindings)
{
let target = bindings.target();
let temporary = bindings.temporary();
let layer_ids = match target.texture_parity {
TextureParity::Even => [target, temporary],
TextureParity::Odd => [temporary, target],
};
let layer_views = resources.layer_views(layer_ids);
let inputs = layer_views.map(|view| {
create_filter_input_bind_group(
self.device,
&self.programs.filter_input_bind_group_layouts[0],
&self.programs.filter_sampler,
view,
)
});
let copy_source = Programs::create_copy_bind_group(
self.device,
&self.programs.copy_bind_group_layout,
resources.layer_view(target),
);
self.programs.filter_pair_bind_groups.insert(
bindings,
FilterPairBindGroups {
inputs,
copy_source,
},
);
}
let filter_pair_bind_groups = &self.programs.filter_pair_bind_groups[&bindings];
if let Some(copy_pass) = plan.copy_pass() {
encode_copy_pass(
self.device,
self.encoder,
&self.programs.copy_pipeline,
copy_pass,
&filter_pair_bind_groups.copy_source,
resources.scratch_view(),
"Filter Copy Pass",
);
}
for (step_index, instances) in plan.steps().enumerate() {
let input = bindings.input(step_index);
let output = bindings.output(step_index);
encode_filter_pass(
self.device,
self.encoder,
resources,
&self.programs.filter_pipeline,
instances,
&filter_pair_bind_groups.inputs[input.texture_parity.get_parity()],
&resources.filter_original_bind_group,
output,
);
}
}
fn clear_pass_inner(&mut self, target: DrawPassTarget, settings: ClearSettings<'_>) {
let color = settings.clear_color();
let view = match target {
DrawPassTarget::Root(_) => self.view,
DrawPassTarget::Layer(target) => self.programs.resources.layer_view(target),
};
let rects = match settings {
ClearSettings::Viewport { .. } => {
Self::clear_full_target(self.encoder, view, clear_color(color));
return;
}
ClearSettings::Rects { rects, .. } => rects,
};
let (target_size, pipeline) = match target {
DrawPassTarget::Root(_) => (
[
self.programs.render_size.width,
self.programs.render_size.height,
],
&self.programs.root_clear_pipeline,
),
DrawPassTarget::Layer(_) => {
let texture_size = self.texture_size();
(
[texture_size.width(), texture_size.height()],
&self.programs.clear_pipeline,
)
}
};
let bounds = RectU16::new(0, 0, target_size[0], target_size[1]);
let target_size = target_size.map(u32::from);
let color = color.premultiply().to_rgba8().to_u32();
self.scratch_buffers.clear_instances.clear();
self.scratch_buffers.clear_instances.extend(
rects
.iter()
.map(|rect| rect.intersect(bounds))
.filter(|rect| !rect.is_empty())
.map(|rect| GpuClearInstance {
origin: [u32::from(rect.x0), u32::from(rect.y0)],
size: [u32::from(rect.width()), u32::from(rect.height())],
target_size,
color,
}),
);
if self.scratch_buffers.clear_instances.is_empty() {
return;
}
let clear_buffer = self
.device
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Clear Buffer"),
contents: bytemuck::cast_slice(&self.scratch_buffers.clear_instances),
usage: wgpu::BufferUsages::VERTEX,
});
let mut render_pass = self.encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("Clear Rects"),
color_attachments: &[Some(RenderPassColorAttachment {
view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
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(pipeline);
render_pass.set_vertex_buffer(0, clear_buffer.slice(..));
render_pass.draw(
0..4,
0..u32::try_from(self.scratch_buffers.clear_instances.len()).unwrap(),
);
}
}
impl Backend for RendererContext<'_> {
type Error = core::convert::Infallible;
fn opaque_draw_pass(
&mut self,
strips: &[GpuStrip],
external_texture_runs: &[ExternalTextureRun],
) -> Result<(), Self::Error> {
self.strip_pass_inner(
strips,
RangedSlice::empty(),
external_texture_runs,
DrawPassTarget::Root(RootTarget::UserSurface),
None,
);
Ok(())
}
fn draw_pass(
&mut self,
strips: RangedSlice<'_, GpuStrip>,
external_texture_runs: &[ExternalTextureRun],
bindings: DrawPassBindings,
) -> Result<(), Self::Error> {
self.strip_pass_inner(
&[],
strips,
external_texture_runs,
bindings.target,
bindings.child,
);
Ok(())
}
fn blend_pass(
&mut self,
blends: RangedSlice<'_, BlendOp>,
blend_strips: &[BlendStrip],
bindings: BlendPassBindings,
) -> Result<(), Self::Error> {
self.blend_pass_inner(blends, blend_strips, bindings);
Ok(())
}
fn filter_pass(
&mut self,
plan: &FilterPassPlan,
bindings: FilterPassBindings,
) -> Result<(), Self::Error> {
self.filter_pass_inner(plan, bindings);
Ok(())
}
fn clear_pass(&mut self, target: LayerTextureId, rects: &[RectU16]) -> Result<(), Self::Error> {
self.clear_pass_inner(
DrawPassTarget::Layer(target),
ClearSettings::Rects {
color: AlphaColor::TRANSPARENT,
rects,
},
);
Ok(())
}
}
fn encode_copy_pass(
device: &Device,
encoder: &mut CommandEncoder,
copy_pipeline: &RenderPipeline,
instances: &[GpuCopyInstance],
source_bind_group: &BindGroup,
output_view: &TextureView,
label: &'static str,
) {
let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(label),
contents: bytemuck::cast_slice(instances),
usage: wgpu::BufferUsages::VERTEX,
});
let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
label: Some(label),
color_attachments: &[Some(RenderPassColorAttachment {
view: output_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
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(copy_pipeline);
render_pass.set_bind_group(0, source_bind_group, &[]);
render_pass.set_vertex_buffer(0, instance_buffer.slice(..));
render_pass.draw(0..4, 0..u32::try_from(instances.len()).unwrap());
}
fn encode_filter_pass(
device: &Device,
encoder: &mut CommandEncoder,
resources: &GpuResources,
filter_pipeline: &RenderPipeline,
instances: &[FilterInstanceData],
input_bind_group: &BindGroup,
original_texture_bind_group: &BindGroup,
output: LayerTextureId,
) {
let output_view = resources.layer_view(output);
let instance_count = u32::try_from(instances.len()).unwrap();
let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Filter Instances Buffer"),
contents: bytemuck::cast_slice(instances),
usage: wgpu::BufferUsages::VERTEX,
});
let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("Apply Filter Pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view: output_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
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(filter_pipeline);
render_pass.set_bind_group(0, &resources.filter_base_bind_group, &[]);
render_pass.set_bind_group(1, input_bind_group, &[]);
render_pass.set_bind_group(2, original_texture_bind_group, &[]);
render_pass.set_vertex_buffer(0, instance_buffer.slice(..));
render_pass.draw(0..4, 0..instance_count);
}
fn create_filter_input_bind_group(
device: &Device,
layout: &BindGroupLayout,
sampler: &Sampler,
texture_view: &TextureView,
) -> BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Filter Input Bind Group"),
layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
],
})
}
fn create_filter_original_texture_bind_group(
device: &Device,
layout: &BindGroupLayout,
layer_texture_view: &TextureView,
) -> BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Filter Original Texture Bind Group"),
layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(layer_texture_view),
}],
})
}
/// 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) -> u16;
/// Get the height of the image.
fn height(&self) -> u16;
/// Write image data to an atlas texture at the specified offset.
fn write_to_atlas(
&self,
device: &Device,
queue: &Queue,
encoder: &mut CommandEncoder,
atlas_texture: &Texture,
offset: [u16; 2],
width: u16,
height: u16,
);
}
/// Implementation for `wgpu::Texture` - uses texture-to-texture copy
impl AtlasWriter for Texture {
fn width(&self) -> u16 {
u16::try_from(self.width()).expect("texture width exceeds the u16 atlas domain")
}
fn height(&self) -> u16 {
u16::try_from(self.height()).expect("texture height exceeds the u16 atlas domain")
}
fn write_to_atlas(
&self,
_device: &Device,
_queue: &Queue,
encoder: &mut CommandEncoder,
atlas_texture: &Texture,
offset: [u16; 2],
width: u16,
height: u16,
) {
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: u32::from(offset[0]),
y: u32::from(offset[1]),
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
Extent3d {
width: u32::from(width),
height: u32::from(height),
depth_or_array_layers: 1,
},
);
}
}
/// Implementation for `Pixmap` - direct upload to atlas
impl AtlasWriter for Pixmap {
fn width(&self) -> u16 {
self.width()
}
fn height(&self) -> u16 {
self.height()
}
fn write_to_atlas(
&self,
_device: &Device,
queue: &Queue,
_encoder: &mut CommandEncoder,
atlas_texture: &Texture,
offset: [u16; 2],
width: u16,
height: u16,
) {
let width = u32::from(width);
let height = u32::from(height);
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: atlas_texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: u32::from(offset[0]),
y: u32::from(offset[1]),
z: 0,
},
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) -> u16 {
self.as_ref().width()
}
fn height(&self) -> u16 {
self.as_ref().height()
}
fn write_to_atlas(
&self,
device: &Device,
queue: &Queue,
encoder: &mut CommandEncoder,
atlas_texture: &Texture,
offset: [u16; 2],
width: u16,
height: u16,
) {
self.as_ref()
.write_to_atlas(device, queue, encoder, atlas_texture, offset, width, height);
}
}
fn clear_color(color: AlphaColor<Srgb>) -> wgpu::Color {
let [r, g, b, a] = color.premultiply().components;
wgpu::Color {
r: f64::from(r),
g: f64::from(g),
b: f64::from(b),
a: f64::from(a),
}
}