| // Copyright 2025 the Vello Authors |
| // SPDX-License-Identifier: Apache-2.0 OR MIT |
| |
| //! GPU rendering module for the sparse strips CPU/GPU rendering engine. |
| //! |
| //! This module provides the GPU-side implementation of the hybrid rendering system. |
| //! It handles: |
| //! - GPU resource management (buffers, textures, pipelines) |
| //! - Surface/window management and presentation |
| //! - Shader execution and rendering |
| //! |
| //! The hybrid approach combines CPU-side path processing with efficient GPU rendering |
| //! to balance flexibility and performance. |
| |
| #![expect( |
| clippy::cast_possible_truncation, |
| reason = "We temporarily ignore those because the casts\ |
| only break in edge cases, and some of them are also only related to conversions from f64 to f32." |
| )] |
| |
| use crate::render::common::IMAGE_PADDING; |
| use crate::{ |
| GpuStrip, RenderError, RenderSettings, RenderSize, Resources, |
| filter::{FilterContext, FilterInstanceData, FilterPassState, FilterPassTarget}, |
| gradient_cache::GradientRampCache, |
| render::{ |
| Config, |
| common::{ |
| GPU_ENCODED_IMAGE_SIZE_TEXELS, GPU_LINEAR_GRADIENT_SIZE_TEXELS, |
| GPU_RADIAL_GRADIENT_SIZE_TEXELS, GPU_SWEEP_GRADIENT_SIZE_TEXELS, GpuEncodedImage, |
| GpuEncodedPaint, GpuLinearGradient, GpuRadialGradient, GpuSweepGradient, |
| normalize_atlas_config, pack_image_offset, pack_image_params, pack_image_size, |
| pack_radial_kind_and_swapped, pack_texture_width_and_extend_mode, pack_tint, |
| }, |
| }, |
| scene::Scene, |
| schedule::{ |
| LoadOp, RendererBackend, RootRenderTarget, Scheduler, SchedulerState, StripPassRenderTarget, |
| }, |
| }; |
| use alloc::vec::Vec; |
| use alloc::{sync::Arc, vec}; |
| use bytemuck::{Pod, Zeroable}; |
| use core::{fmt::Debug, num::NonZeroU64}; |
| #[cfg(feature = "text")] |
| use glifo::PendingClearRect; |
| use vello_common::image_cache::{ImageCache, ImageResource}; |
| use vello_common::multi_atlas::{AtlasConfig, AtlasError, AtlasId}; |
| use vello_common::render_graph::LayerId; |
| use vello_common::{ |
| coarse::WideTile, |
| encode::{EncodedGradient, EncodedKind, EncodedPaint, MAX_GRADIENT_LUT_SIZE, RadialKind}, |
| paint::ImageSource, |
| peniko, |
| pixmap::Pixmap, |
| tile::Tile, |
| }; |
| use wgpu::{ |
| BindGroup, BindGroupLayout, BlendState, Buffer, 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: u32, |
| /// Height of the rendering target |
| pub height: u32, |
| } |
| |
| /// Vello Hybrid's Renderer. |
| #[derive(Debug)] |
| pub struct Renderer { |
| /// Programs for rendering. |
| programs: Programs, |
| /// Scheduler for scheduling draws. |
| scheduler: Scheduler, |
| /// The state used by the scheduler. |
| scheduler_state: SchedulerState, |
| /// 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, |
| /// Context for GPU filter effects. |
| filter_context: FilterContext, |
| /// State used for constructing filter passes. |
| filter_pass_state: FilterPassState, |
| dummy_image_cache: Option<ImageCache>, |
| #[cfg(feature = "text")] |
| atlas_clear_scratch: Vec<u8>, |
| } |
| |
| impl Renderer { |
| /// Creates a new renderer. |
| pub fn new(device: &Device, render_target_config: &RenderTargetConfig) -> Self { |
| Self::new_with(device, render_target_config, RenderSettings::default()) |
| } |
| |
| /// Creates a new renderer with specific settings. |
| pub fn new_with( |
| device: &Device, |
| render_target_config: &RenderTargetConfig, |
| settings: RenderSettings, |
| ) -> Self { |
| super::common::maybe_warn_about_webgl_feature_conflict(); |
| |
| let mut settings = settings; |
| let max_texture_dimension_2d = device.limits().max_texture_dimension_2d; |
| // When targeting wasm32 with a WebGL/GLES backend, we need to set |
| // `initial_atlas_count` to 2. In WGPU's GLES backend, heuristics are used to decide |
| // whether a texture should be treated as D2 or D2Array. However, this can cause a |
| // mismatch: when depth_or_array_layers == 1, the backend assumes the texture is D2, |
| // even if it was actually created as a D2Array. This issue only occurs with the GLES |
| // backend. |
| // |
| // @see https://github.com/gfx-rs/wgpu/blob/61e5124eb9530d3b3865556a7da4fd320d03ddc5/wgpu-hal/src/gles/mod.rs#L470-L517 |
| // TODO: Can we somehow dynamically detect whether the WebGL backend was chosen, so that the |
| // wgpu backend isn't affected by this? |
| #[cfg(target_arch = "wasm32")] |
| let min_initial_atlas_count = 2; |
| #[cfg(not(target_arch = "wasm32"))] |
| let min_initial_atlas_count = 1; |
| normalize_atlas_config( |
| &mut settings.atlas_config, |
| max_texture_dimension_2d, |
| device.limits().max_texture_array_layers, |
| min_initial_atlas_count, |
| ); |
| let total_slots = (max_texture_dimension_2d / u32::from(Tile::HEIGHT)) as usize; |
| let image_cache = ImageCache::new_with_config(settings.atlas_config); |
| // Estimate the maximum number of gradient cache entries based on the max texture dimension |
| // and the maximum gradient LUT size - worst case scenario. |
| let max_gradient_cache_size = |
| max_texture_dimension_2d * max_texture_dimension_2d / MAX_GRADIENT_LUT_SIZE as u32; |
| let gradient_cache = GradientRampCache::new(max_gradient_cache_size, settings.level); |
| |
| let filter_context = FilterContext::new(settings.atlas_config); |
| Self { |
| programs: Programs::new( |
| device, |
| &image_cache, |
| &filter_context.image_cache, |
| render_target_config, |
| total_slots, |
| ), |
| scheduler: Scheduler::new(total_slots), |
| scheduler_state: SchedulerState::default(), |
| gradient_cache, |
| encoded_paints: Vec::new(), |
| paint_idxs: Vec::new(), |
| filter_context, |
| filter_pass_state: FilterPassState::default(), |
| dummy_image_cache: Some(ImageCache::new_dummy()), |
| #[cfg(feature = "text")] |
| atlas_clear_scratch: Vec::new(), |
| } |
| } |
| |
| fn prepare_filter_textures( |
| &mut self, |
| scene: &Scene, |
| device: &Device, |
| encoder: &mut CommandEncoder, |
| image_cache: &mut ImageCache, |
| encoded_paints: &mut Vec<EncodedPaint>, |
| ) -> Result<(), AtlasError> { |
| // TODO: Maybe we can do the clear implicitly when using the textures for the first time. |
| if !self.filter_context.filter_textures.is_empty() { |
| for view in &self.programs.resources.filter_atlas.views { |
| let _pass = encoder.begin_render_pass(&RenderPassDescriptor { |
| label: Some("Clear Filter Atlas Texture"), |
| color_attachments: &[Some(RenderPassColorAttachment { |
| view, |
| resolve_target: None, |
| ops: wgpu::Operations { |
| load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), |
| store: wgpu::StoreOp::Store, |
| }, |
| depth_slice: None, |
| })], |
| depth_stencil_attachment: None, |
| occlusion_query_set: None, |
| timestamp_writes: None, |
| multiview_mask: None, |
| }); |
| } |
| } |
| |
| self.filter_context |
| .deallocate_all_and_clear_context(image_cache); |
| |
| self.filter_context |
| .prepare(&scene.render_graph, image_cache, encoded_paints)?; |
| |
| Programs::maybe_resize_atlas_texture_array( |
| device, |
| encoder, |
| &mut self.programs.resources, |
| &self.programs.atlas_bind_group_layout, |
| image_cache.atlas_count() as u32, |
| ); |
| self.programs.resources.filter_atlas.ensure_count( |
| device, |
| self.filter_context.image_cache.atlas_count() as u32, |
| &self.programs.filter_input_bind_group_layouts[0], |
| &self.programs.filter_input_bind_group_layouts[1], |
| ); |
| |
| Ok(()) |
| } |
| |
| /// Render `scene` into the provided command encoder. |
| /// |
| /// This method creates GPU resources as needed and schedules potentially multiple |
| /// render passes. |
| pub fn render( |
| &mut self, |
| scene: &Scene, |
| resources: &mut Resources, |
| device: &Device, |
| queue: &Queue, |
| encoder: &mut CommandEncoder, |
| render_size: &RenderSize, |
| view: &TextureView, |
| ) -> 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, |
| ) |
| .expect("Failed to render glyphs to atlas"); |
| }, |
| |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]), |
| ); |
| }, |
| ); |
| } |
| |
| let mut encoded_paints = scene.encoded_paints.borrow_mut(); |
| let scene_paint_count = encoded_paints.len(); |
| |
| self.prepare_filter_textures( |
| scene, |
| device, |
| encoder, |
| &mut resources.image_cache, |
| &mut encoded_paints, |
| )?; |
| |
| // TODO: Passing `false` here because wgpu swapchain textures likely have |
| // undefined initial content, making an explicit clear redundant in the common |
| // case. Verify whether there are scenarios where wgpu would need a clear. |
| let result = self.render_scene( |
| scene, |
| device, |
| queue, |
| encoder, |
| render_size, |
| view, |
| &resources.image_cache, |
| &encoded_paints, |
| false, |
| RootRenderTarget::UserSurface, |
| ); |
| |
| encoded_paints.truncate(scene_paint_count); |
| #[cfg(feature = "text")] |
| resources.after_render(self, |renderer, rect| { |
| clear_atlas_region(queue, renderer, rect); |
| }); |
| 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`). |
| #[doc(hidden)] |
| pub fn render_to_atlas( |
| &mut self, |
| scene: &Scene, |
| atlas_count: u32, |
| atlas_config: AtlasConfig, |
| device: &Device, |
| queue: &Queue, |
| atlas_id: AtlasId, |
| ) -> Result<(), RenderError> { |
| let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { |
| label: Some("Render to Atlas Encoder"), |
| }); |
| |
| Programs::maybe_resize_atlas_texture_array( |
| device, |
| &mut encoder, |
| &mut self.programs.resources, |
| &self.programs.atlas_bind_group_layout, |
| 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_array |
| .create_view(&TextureViewDescriptor { |
| label: Some("Atlas Layer Render View"), |
| format: Some(wgpu::TextureFormat::Rgba8Unorm), |
| dimension: Some(wgpu::TextureViewDimension::D2), |
| aspect: wgpu::TextureAspect::All, |
| base_mip_level: 0, |
| mip_level_count: Some(1), |
| base_array_layer: atlas_id.as_u32(), |
| array_layer_count: Some(1), |
| usage: None, |
| }); |
| |
| // Swap in the stub atlas bind group to avoid the read-write conflict: |
| // the real atlas texture is used as the render target (COLOR_TARGET), so it |
| // cannot also be bound as a shader resource (TEXTURE_BINDING) in the same pass. |
| core::mem::swap( |
| &mut self.programs.resources.atlas_bind_group, |
| &mut self.programs.resources.stub_atlas_bind_group, |
| ); |
| |
| let encoded_paints = scene.encoded_paints.borrow(); |
| 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, |
| &dummy_image_cache, |
| &encoded_paints, |
| false, |
| RootRenderTarget::AtlasLayer, |
| ); |
| self.dummy_image_cache = Some(dummy_image_cache); |
| |
| // Restore the real atlas bind group. |
| core::mem::swap( |
| &mut self.programs.resources.atlas_bind_group, |
| &mut self.programs.resources.stub_atlas_bind_group, |
| ); |
| |
| // Submit immediately so the atlas content is committed before subsequent |
| // render() calls overwrite the shared alpha/config/paint resources. |
| queue.submit(Some(encoder.finish())); |
| |
| result |
| } |
| |
| /// Shared render pipeline: prepares GPU resources, runs the scheduler against |
| /// the provided `view` at `render_size`, and maintains caches. |
| /// |
| /// When `clear` is true the render target is cleared to transparent black |
| /// before drawing (normal frame rendering). |
| fn render_scene( |
| &mut self, |
| scene: &Scene, |
| device: &Device, |
| queue: &Queue, |
| encoder: &mut CommandEncoder, |
| render_size: &RenderSize, |
| view: &TextureView, |
| image_cache: &ImageCache, |
| encoded_paints: &[EncodedPaint], |
| clear: bool, |
| root_output_target: RootRenderTarget, |
| ) -> Result<(), RenderError> { |
| self.prepare_gpu_encoded_paints(encoded_paints, image_cache); |
| // 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.filter_context, |
| ); |
| |
| if clear { |
| Self::clear_view(encoder, view); |
| } |
| let mut ctx = RendererContext { |
| programs: &mut self.programs, |
| device, |
| queue, |
| encoder, |
| view, |
| image_cache, |
| filter_context: &self.filter_context, |
| filter_pass_state: &mut self.filter_pass_state, |
| }; |
| self.scheduler.do_scene( |
| &mut self.scheduler_state, |
| &mut ctx, |
| scene, |
| root_output_target, |
| &self.paint_idxs, |
| &self.filter_context, |
| encoded_paints, |
| )?; |
| self.gradient_cache.maintain(); |
| |
| Ok(()) |
| } |
| |
| /// Clear the view to transparent black. |
| // TODO: Investigate adding tests for the clear_view behavior. |
| fn clear_view(encoder: &mut CommandEncoder, view: &TextureView) { |
| encoder.begin_render_pass(&RenderPassDescriptor { |
| label: Some("Clear View"), |
| color_attachments: &[Some(RenderPassColorAttachment { |
| view, |
| resolve_target: None, |
| ops: wgpu::Operations { |
| load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), |
| store: wgpu::StoreOp::Store, |
| }, |
| depth_slice: None, |
| })], |
| depth_stencil_attachment: None, |
| occlusion_query_set: None, |
| timestamp_writes: None, |
| multiview_mask: None, |
| }); |
| } |
| |
| /// 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<[u32; 2]>, |
| ) { |
| let image_resource = image_cache.get(image_id).expect("Image resource not found"); |
| |
| Programs::maybe_resize_atlas_texture_array( |
| device, |
| encoder, |
| &mut self.programs.resources, |
| &self.programs.atlas_bind_group_layout, |
| image_cache.atlas_count() as u32, |
| ); |
| let offset = offset_override.unwrap_or([ |
| image_resource.offset[0] as u32, |
| image_resource.offset[1] as u32, |
| ]); |
| writer.write_to_atlas_layer( |
| device, |
| queue, |
| encoder, |
| &self.programs.resources.atlas_texture_array, |
| image_resource.atlas_id.as_u32(), |
| offset, |
| writer.width(), |
| writer.height(), |
| ); |
| } |
| |
| /// Destroy an image from the cache and clear the allocated slot in the atlas. |
| pub fn destroy_image( |
| &mut self, |
| resources: &mut Resources, |
| device: &Device, |
| queue: &Queue, |
| 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 as u32; |
| |
| self.clear_atlas_region( |
| device, |
| queue, |
| encoder, |
| image_resource.atlas_id, |
| [ |
| image_resource.offset[0] as u32 - padding, |
| image_resource.offset[1] as u32 - padding, |
| ], |
| image_resource.width as u32 + padding * 2, |
| image_resource.height as u32 + padding * 2, |
| ); |
| } |
| } |
| |
| /// Returns a reference to the underlying atlas texture array. |
| /// |
| /// This is a 2D array texture (`TextureViewDimension::D2Array`) containing all |
| /// atlas layers used by the image cache. Each layer holds cached image data |
| /// (e.g., rasterised glyphs) that the renderer samples during draw calls. |
| pub fn atlas_texture(&self) -> &Texture { |
| &self.programs.resources.atlas_texture_array |
| } |
| |
| /// Clear a specific region of the atlas texture. |
| fn clear_atlas_region( |
| &mut self, |
| _device: &Device, |
| _queue: &Queue, |
| encoder: &mut CommandEncoder, |
| atlas_id: AtlasId, |
| offset: [u32; 2], |
| width: u32, |
| height: u32, |
| ) { |
| // Create a texture view for the specific atlas layer |
| let layer_view = |
| self.programs |
| .resources |
| .atlas_texture_array |
| .create_view(&TextureViewDescriptor { |
| label: Some("Atlas Layer Clear View"), |
| format: Some(wgpu::TextureFormat::Rgba8Unorm), |
| dimension: Some(wgpu::TextureViewDimension::D2), |
| aspect: wgpu::TextureAspect::All, |
| base_mip_level: 0, |
| mip_level_count: Some(1), |
| base_array_layer: atlas_id.as_u32(), |
| array_layer_count: Some(1), |
| // Inherit usage from the texture |
| usage: None, |
| }); |
| |
| let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor { |
| label: Some("Clear Atlas Region"), |
| color_attachments: &[Some(RenderPassColorAttachment { |
| view: &layer_view, |
| resolve_target: None, |
| ops: wgpu::Operations { |
| // Don't clear entire texture, just the scissor region |
| load: wgpu::LoadOp::Load, |
| store: wgpu::StoreOp::Store, |
| }, |
| depth_slice: None, |
| })], |
| depth_stencil_attachment: None, |
| occlusion_query_set: None, |
| timestamp_writes: None, |
| multiview_mask: None, |
| }); |
| |
| // Set scissor rectangle to limit clearing to specific region |
| render_pass.set_scissor_rect(offset[0], offset[1], width, height); |
| // Use atlas clear pipeline to render transparent pixels |
| render_pass.set_pipeline(&self.programs.atlas_clear_pipeline); |
| // Draw fullscreen quad |
| render_pass.draw(0..4, 0..1); |
| } |
| |
| fn prepare_gpu_encoded_paints( |
| &mut self, |
| encoded_paints: &[EncodedPaint], |
| image_cache: &ImageCache, |
| ) { |
| self.encoded_paints |
| .resize_with(encoded_paints.len(), || GPU_PAINT_PLACEHOLDER); |
| self.paint_idxs.resize(encoded_paints.len() + 1, 0); |
| |
| let mut current_idx = 0; |
| for (encoded_paint_idx, paint) in encoded_paints.iter().enumerate() { |
| self.paint_idxs[encoded_paint_idx] = current_idx; |
| match paint { |
| EncodedPaint::Image(img) => { |
| if let ImageSource::OpaqueId { id: image_id, .. } = img.source { |
| let image_resource: Option<&ImageResource> = image_cache.get(image_id); |
| if let Some(image_resource) = image_resource { |
| let image_paint = self.encode_image_paint(img, image_resource); |
| self.encoded_paints[encoded_paint_idx] = image_paint; |
| current_idx += GPU_ENCODED_IMAGE_SIZE_TEXELS; |
| } |
| } |
| } |
| EncodedPaint::Gradient(gradient) => { |
| let (gradient_start, gradient_width) = |
| self.gradient_cache.get_or_create_ramp(gradient); |
| let gradient_paint: GpuEncodedPaint = |
| self.encode_gradient_paint(gradient, gradient_width, gradient_start); |
| let gradient_size_texels = match &gradient_paint { |
| GpuEncodedPaint::LinearGradient(_) => GPU_LINEAR_GRADIENT_SIZE_TEXELS, |
| GpuEncodedPaint::RadialGradient(_) => GPU_RADIAL_GRADIENT_SIZE_TEXELS, |
| GpuEncodedPaint::SweepGradient(_) => GPU_SWEEP_GRADIENT_SIZE_TEXELS, |
| _ => unreachable!("encode_gradient_for_gpu only returns gradient types"), |
| }; |
| self.encoded_paints[encoded_paint_idx] = gradient_paint; |
| current_idx += gradient_size_texels; |
| } |
| EncodedPaint::BlurredRoundedRect(_blurred_rect) => { |
| // TODO: Blurred rounded rectangles are not yet supported |
| log::warn!( |
| "Blurred rounded rectangles are not yet supported in sparse strips hybrid renderer" |
| ); |
| } |
| } |
| } |
| self.paint_idxs[encoded_paints.len()] = current_idx; |
| } |
| |
| fn encode_image_paint( |
| &self, |
| image: &vello_common::encode::EncodedImage, |
| image_resource: &ImageResource, |
| ) -> GpuEncodedPaint { |
| let transform = image.transform.as_coeffs().map(|x| x as f32); |
| let image_size = pack_image_size(image_resource.width, image_resource.height); |
| let image_offset = pack_image_offset(image_resource.offset[0], image_resource.offset[1]); |
| let image_params = pack_image_params( |
| image.sampler.quality as u32, |
| image.sampler.x_extend as u32, |
| image.sampler.y_extend as u32, |
| image_resource.atlas_id.as_u32(), |
| ); |
| let (tint, tint_mode) = pack_tint(image.tint); |
| |
| GpuEncodedPaint::Image(GpuEncodedImage { |
| image_params, |
| image_size, |
| image_offset, |
| transform, |
| tint, |
| tint_mode, |
| image_padding: image_resource.padding as u32, |
| }) |
| } |
| |
| fn encode_gradient_paint( |
| &self, |
| gradient: &EncodedGradient, |
| gradient_width: u32, |
| gradient_start: u32, |
| ) -> GpuEncodedPaint { |
| let 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], |
| }), |
| } |
| } |
| } |
| |
| #[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(), |
| mip_level: 0, |
| origin: wgpu::Origin3d { |
| x: rect.x as u32, |
| y: rect.y as u32, |
| z: rect.page_index, |
| }, |
| 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 { |
| /// Pipelines for rendering strips. |
| /// The first pipeline should be used for color attachments in the native pixel format, |
| /// the second for color attachments in RGBA8. |
| strip_pipelines: [RenderPipeline; 2], |
| /// Bind group layout for strip draws |
| strip_bind_group_layout: BindGroupLayout, |
| /// Bind group layout for encoded paints |
| encoded_paints_bind_group_layout: BindGroupLayout, |
| /// Bind group layout for gradient texture |
| gradient_bind_group_layout: BindGroupLayout, |
| /// Bind group layout for atlas textures |
| atlas_bind_group_layout: BindGroupLayout, |
| /// Bind group layout for filter data texture. |
| filter_bind_group_layout: BindGroupLayout, |
| /// Pipeline for applying filter effects. |
| filter_pipeline: RenderPipeline, |
| /// Bind group layouts for filter input. |
| filter_input_bind_group_layouts: [BindGroupLayout; 2], |
| /// Pipeline for clearing slots in slot textures. |
| clear_pipeline: RenderPipeline, |
| /// Pipeline for clearing atlas regions. |
| atlas_clear_pipeline: RenderPipeline, |
| /// GPU resources for rendering (created during prepare) |
| resources: GpuResources, |
| /// Dimensions of the rendering target |
| render_size: RenderSize, |
| /// Scratch buffer for staging encoded paints texture data. |
| encoded_paints_data: Vec<u8>, |
| /// Scratch buffer for staging filter data texture data. |
| filter_data: Vec<u8>, |
| } |
| |
| #[derive(Debug)] |
| struct FilterAtlasState { |
| textures: Vec<Texture>, |
| views: Vec<TextureView>, |
| input_bind_groups: Vec<BindGroup>, |
| original_bind_groups: Vec<BindGroup>, |
| sampler: Sampler, |
| atlas_size: (u32, u32), |
| } |
| |
| impl FilterAtlasState { |
| fn new(device: &Device, atlas_size: (u32, u32)) -> Self { |
| let sampler = device.create_sampler(&wgpu::SamplerDescriptor { |
| label: Some("Filter Linear Sampler"), |
| mag_filter: wgpu::FilterMode::Linear, |
| min_filter: wgpu::FilterMode::Linear, |
| ..Default::default() |
| }); |
| |
| Self { |
| textures: Vec::new(), |
| views: Vec::new(), |
| input_bind_groups: Vec::new(), |
| original_bind_groups: Vec::new(), |
| sampler, |
| atlas_size, |
| } |
| } |
| |
| fn ensure_count( |
| &mut self, |
| device: &Device, |
| required_count: u32, |
| input_layout: &BindGroupLayout, |
| original_layout: &BindGroupLayout, |
| ) { |
| let current_count = self.textures.len() as u32; |
| |
| if required_count <= current_count { |
| return; |
| } |
| let (width, height) = self.atlas_size; |
| |
| for _ in current_count..required_count { |
| let texture = device.create_texture(&wgpu::TextureDescriptor { |
| label: Some("Filter Atlas 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::RENDER_ATTACHMENT, |
| view_formats: &[], |
| }); |
| let view = texture.create_view(&TextureViewDescriptor::default()); |
| let input_bg = |
| create_filter_input_bind_group(device, input_layout, &self.sampler, &view); |
| let original_bg = device.create_bind_group(&wgpu::BindGroupDescriptor { |
| label: None, |
| layout: original_layout, |
| entries: &[wgpu::BindGroupEntry { |
| binding: 0, |
| resource: wgpu::BindingResource::TextureView(&view), |
| }], |
| }); |
| self.textures.push(texture); |
| self.views.push(view); |
| self.input_bind_groups.push(input_bg); |
| self.original_bind_groups.push(original_bg); |
| } |
| } |
| } |
| |
| /// Contains all GPU resources needed for rendering |
| #[derive(Debug)] |
| struct GpuResources { |
| /// Buffer for [`GpuStrip`] data |
| strips_buffer: Buffer, |
| /// Texture for alpha values (used by both view and slot rendering) |
| alphas_texture: Texture, |
| /// Textures for atlas data (multiple atlases supported) |
| atlas_texture_array: Texture, |
| /// View for atlas texture array |
| atlas_texture_array_view: TextureView, |
| /// Bind group for atlas textures (as texture array) |
| atlas_bind_group: BindGroup, |
| /// Filter atlas textures and their associated views/bind groups. |
| /// Lazily allocated: stays empty until the first scene with filters. |
| filter_atlas: FilterAtlasState, |
| /// 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, |
| |
| /// Config buffer for rendering wide tile commands into the view texture. |
| view_config_buffer: Buffer, |
| /// Config buffer for rendering wide tile commands into a slot texture. |
| slot_config_buffer: Buffer, |
| |
| /// Buffer for slot indices used in `clear_slots` |
| clear_slot_indices_buffer: Buffer, |
| /// Buffer holding `FilterInstanceData` for a single filter draw call. |
| filter_instance_buffer: Buffer, |
| // Bind groups for rendering with clip buffers |
| slot_bind_groups: [BindGroup; 3], |
| /// Slot texture views |
| slot_texture_views: [TextureView; 2], |
| |
| /// Bind group for clear slots operation |
| clear_bind_group: BindGroup, |
| |
| /// Placeholder atlas bind group with a 1x1 dummy texture, used during |
| /// `render_to_atlas` to avoid a read-write conflict on the real atlas texture. |
| stub_atlas_bind_group: BindGroup, |
| } |
| |
| const SIZE_OF_CONFIG: NonZeroU64 = NonZeroU64::new(size_of::<Config>() as u64).unwrap(); |
| |
| /// Config for the clear slots pipeline |
| #[repr(C)] |
| #[derive(Debug, Copy, Clone, Pod, Zeroable)] |
| struct ClearSlotsConfig { |
| /// Width of a slot |
| pub slot_width: u32, |
| /// Height of a slot |
| pub slot_height: u32, |
| /// Total height of the texture |
| pub texture_height: u32, |
| /// Padding for 16-byte alignment |
| pub _padding: u32, |
| } |
| |
| impl GpuStrip { |
| /// Vertex attributes for the strip |
| pub fn vertex_attributes() -> [wgpu::VertexAttribute; 5] { |
| wgpu::vertex_attr_array![ |
| 0 => Uint32, |
| 1 => Uint32, |
| 2 => Uint32, |
| 3 => Uint32, |
| 4 => Uint32, |
| ] |
| } |
| } |
| |
| impl Programs { |
| fn new( |
| device: &Device, |
| image_cache: &ImageCache, |
| filter_texture_cache: &ImageCache, |
| render_target_config: &RenderTargetConfig, |
| slot_count: usize, |
| ) -> Self { |
| let strip_bind_group_layout = |
| device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { |
| label: Some("Strip Bind Group Layout"), |
| entries: &[ |
| wgpu::BindGroupLayoutEntry { |
| binding: 0, |
| visibility: wgpu::ShaderStages::FRAGMENT, |
| ty: wgpu::BindingType::Texture { |
| sample_type: wgpu::TextureSampleType::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 atlas_bind_group_layout = |
| device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { |
| label: Some("Atlas Texture Bind Group Layout"), |
| entries: &[wgpu::BindGroupLayoutEntry { |
| binding: 0, |
| visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, |
| ty: wgpu::BindingType::Texture { |
| sample_type: wgpu::TextureSampleType::Float { filterable: true }, |
| view_dimension: wgpu::TextureViewDimension::D2Array, |
| multisampled: false, |
| }, |
| count: None, |
| }], |
| }); |
| |
| let encoded_paints_bind_group_layout = |
| device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { |
| label: Some("Encoded Paints Bind Group Layout"), |
| entries: &[wgpu::BindGroupLayoutEntry { |
| binding: 0, |
| visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, |
| ty: wgpu::BindingType::Texture { |
| sample_type: wgpu::TextureSampleType::Uint, |
| view_dimension: wgpu::TextureViewDimension::D2, |
| multisampled: false, |
| }, |
| count: None, |
| }], |
| }); |
| |
| let gradient_bind_group_layout = |
| device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { |
| label: Some("Gradient Bind Group Layout"), |
| entries: &[wgpu::BindGroupLayoutEntry { |
| binding: 0, |
| visibility: wgpu::ShaderStages::FRAGMENT, |
| ty: wgpu::BindingType::Texture { |
| sample_type: wgpu::TextureSampleType::Float { filterable: true }, |
| view_dimension: wgpu::TextureViewDimension::D2, |
| multisampled: false, |
| }, |
| count: None, |
| }], |
| }); |
| |
| // Create bind group layout for clearing slots |
| let clear_bind_group_layout = |
| device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { |
| label: Some("Clear Slots Bind Group Layout"), |
| entries: &[wgpu::BindGroupLayoutEntry { |
| binding: 0, |
| visibility: wgpu::ShaderStages::VERTEX, |
| ty: wgpu::BindingType::Buffer { |
| ty: wgpu::BufferBindingType::Uniform, |
| has_dynamic_offset: false, |
| min_binding_size: None, |
| }, |
| count: None, |
| }], |
| }); |
| |
| let strip_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { |
| label: Some("Strip Shader"), |
| source: wgpu::ShaderSource::Wgsl(vello_sparse_shaders::wgsl::RENDER_STRIPS.into()), |
| }); |
| |
| let clear_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { |
| label: Some("Clear Slots Shader"), |
| source: wgpu::ShaderSource::Wgsl(vello_sparse_shaders::wgsl::CLEAR_SLOTS.into()), |
| }); |
| |
| let strip_pipeline_layout = |
| device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { |
| label: Some("Strip Pipeline Layout"), |
| bind_group_layouts: &[ |
| &strip_bind_group_layout, |
| &atlas_bind_group_layout, |
| &encoded_paints_bind_group_layout, |
| &gradient_bind_group_layout, |
| ], |
| immediate_size: 0, |
| }); |
| |
| let clear_pipeline_layout = |
| device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { |
| label: Some("Clear Slots Pipeline Layout"), |
| bind_group_layouts: &[&clear_bind_group_layout], |
| immediate_size: 0, |
| }); |
| |
| let strip_formats = [render_target_config.format, wgpu::TextureFormat::Rgba8Unorm]; |
| let strip_pipelines: [RenderPipeline; 2] = core::array::from_fn(|i| { |
| device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { |
| label: Some("Strip Pipeline"), |
| layout: Some(&strip_pipeline_layout), |
| vertex: wgpu::VertexState { |
| module: &strip_shader, |
| entry_point: Some("vs_main"), |
| buffers: &[wgpu::VertexBufferLayout { |
| array_stride: size_of::<GpuStrip>() as u64, |
| step_mode: wgpu::VertexStepMode::Instance, |
| attributes: &GpuStrip::vertex_attributes(), |
| }], |
| compilation_options: PipelineCompilationOptions::default(), |
| }, |
| fragment: Some(wgpu::FragmentState { |
| module: &strip_shader, |
| entry_point: Some("fs_main"), |
| targets: &[Some(ColorTargetState { |
| format: strip_formats[i], |
| blend: Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING), |
| write_mask: ColorWrites::ALL, |
| })], |
| compilation_options: PipelineCompilationOptions::default(), |
| }), |
| primitive: wgpu::PrimitiveState { |
| topology: wgpu::PrimitiveTopology::TriangleStrip, |
| ..Default::default() |
| }, |
| depth_stencil: None, |
| multisample: wgpu::MultisampleState::default(), |
| multiview_mask: None, |
| cache: None, |
| }) |
| }); |
| |
| let clear_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { |
| label: Some("Clear Slots Pipeline"), |
| layout: Some(&clear_pipeline_layout), |
| vertex: wgpu::VertexState { |
| module: &clear_shader, |
| entry_point: Some("vs_main"), |
| buffers: &[wgpu::VertexBufferLayout { |
| array_stride: size_of::<u32>() as u64, |
| step_mode: wgpu::VertexStepMode::Instance, |
| attributes: &[wgpu::VertexAttribute { |
| format: wgpu::VertexFormat::Uint32, |
| offset: 0, |
| shader_location: 0, |
| }], |
| }], |
| compilation_options: PipelineCompilationOptions::default(), |
| }, |
| fragment: Some(wgpu::FragmentState { |
| module: &clear_shader, |
| entry_point: Some("fs_main"), |
| targets: &[Some(ColorTargetState { |
| format: render_target_config.format, |
| // No blending needed for clearing |
| blend: None, |
| write_mask: ColorWrites::ALL, |
| })], |
| compilation_options: PipelineCompilationOptions::default(), |
| }), |
| primitive: wgpu::PrimitiveState { |
| topology: wgpu::PrimitiveTopology::TriangleStrip, |
| ..Default::default() |
| }, |
| depth_stencil: None, |
| multisample: wgpu::MultisampleState::default(), |
| multiview_mask: None, |
| cache: None, |
| }); |
| |
| // Create atlas clear pipeline |
| let atlas_clear_pipeline_layout = |
| device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { |
| label: Some("Atlas Clear Pipeline Layout"), |
| bind_group_layouts: &[], |
| immediate_size: 0, |
| }); |
| let atlas_clear_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { |
| label: Some("Atlas Clear Pipeline"), |
| layout: Some(&atlas_clear_pipeline_layout), |
| vertex: wgpu::VertexState { |
| module: &clear_shader, |
| // Use a different vertex shader entry point |
| entry_point: Some("vs_main_fullscreen"), |
| buffers: &[], |
| compilation_options: PipelineCompilationOptions::default(), |
| }, |
| fragment: Some(wgpu::FragmentState { |
| module: &clear_shader, |
| entry_point: Some("fs_main"), |
| targets: &[Some(ColorTargetState { |
| format: wgpu::TextureFormat::Rgba8Unorm, |
| blend: None, |
| write_mask: ColorWrites::ALL, |
| })], |
| compilation_options: PipelineCompilationOptions::default(), |
| }), |
| primitive: wgpu::PrimitiveState { |
| topology: wgpu::PrimitiveTopology::TriangleStrip, |
| ..Default::default() |
| }, |
| depth_stencil: None, |
| multisample: wgpu::MultisampleState::default(), |
| multiview_mask: None, |
| cache: None, |
| }); |
| |
| let 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 texture. |
| device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { |
| label: Some("Filter Original 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_sparse_shaders::wgsl::FILTERS.into()), |
| }); |
| let filter_pipeline_layout = |
| device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { |
| label: Some("Filter Pipeline Layout"), |
| bind_group_layouts: &[ |
| &filter_bind_group_layout, |
| &filter_input_bind_group_layouts[0], |
| &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: &[wgpu::VertexBufferLayout { |
| array_stride: size_of::<FilterInstanceData>() as u64, |
| step_mode: wgpu::VertexStepMode::Instance, |
| attributes: &wgpu::vertex_attr_array![ |
| 0 => Uint32x2, |
| 1 => Uint32x2, |
| 2 => Uint32x2, |
| 3 => Uint32x2, |
| 4 => Uint32x2, |
| 5 => Uint32, |
| 6 => Uint32x2, |
| 7 => Uint32x2, |
| 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 slot_texture_views: [TextureView; 2] = core::array::from_fn(|_| { |
| device |
| .create_texture(&wgpu::TextureDescriptor { |
| label: Some("Slot Texture"), |
| size: Extent3d { |
| width: u32::from(WideTile::WIDTH), |
| height: u32::from(Tile::HEIGHT) * slot_count as u32, |
| depth_or_array_layers: 1, |
| }, |
| mip_level_count: 1, |
| sample_count: 1, |
| dimension: wgpu::TextureDimension::D2, |
| format: render_target_config.format, |
| usage: wgpu::TextureUsages::TEXTURE_BINDING |
| | wgpu::TextureUsages::RENDER_ATTACHMENT |
| | wgpu::TextureUsages::COPY_SRC, |
| view_formats: &[], |
| }) |
| .create_view(&TextureViewDescriptor::default()) |
| }); |
| |
| let clear_config_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { |
| label: Some("Clear Slots Config"), |
| contents: bytemuck::bytes_of(&ClearSlotsConfig { |
| slot_width: u32::from(WideTile::WIDTH), |
| slot_height: u32::from(Tile::HEIGHT), |
| texture_height: u32::from(Tile::HEIGHT) * slot_count as u32, |
| _padding: 0, |
| }), |
| usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, |
| }); |
| let clear_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { |
| label: Some("Clear Slots Bind Group"), |
| layout: &clear_bind_group_layout, |
| entries: &[wgpu::BindGroupEntry { |
| binding: 0, |
| resource: clear_config_buffer.as_entire_binding(), |
| }], |
| }); |
| let clear_slot_indices_buffer = Self::create_clear_slot_indices_buffer( |
| device, |
| slot_count as u64 * size_of::<u32>() as u64, |
| ); |
| |
| let slot_config_buffer = Self::create_config_buffer( |
| device, |
| &RenderSize { |
| width: u32::from(WideTile::WIDTH), |
| height: u32::from(Tile::HEIGHT) * slot_count as u32, |
| }, |
| device.limits().max_texture_dimension_2d, |
| ); |
| |
| let max_texture_dimension_2d = device.limits().max_texture_dimension_2d; |
| const INITIAL_ALPHA_TEXTURE_HEIGHT: u32 = 1; |
| let alphas_texture = Self::create_alphas_texture( |
| device, |
| max_texture_dimension_2d, |
| INITIAL_ALPHA_TEXTURE_HEIGHT, |
| ); |
| let view_config_buffer = Self::create_config_buffer( |
| device, |
| &RenderSize { |
| width: render_target_config.width, |
| height: render_target_config.height, |
| }, |
| max_texture_dimension_2d, |
| ); |
| |
| let AtlasConfig { |
| atlas_size: (atlas_width, atlas_height), |
| initial_atlas_count, |
| .. |
| } = image_cache.atlas_manager().config(); |
| let (atlas_texture_array, atlas_texture_array_view) = Self::create_atlas_texture_array( |
| device, |
| *atlas_width, |
| *atlas_height, |
| *initial_atlas_count as u32, |
| ); |
| let atlas_bind_group = Self::create_atlas_bind_group( |
| device, |
| &atlas_bind_group_layout, |
| &atlas_texture_array_view, |
| ); |
| |
| // Create a 1x1 stub atlas texture array for use during render_to_atlas. |
| // This avoids the read-write conflict that occurs when the real atlas is both |
| // a shader input (bind group) and render target in the same pass. |
| let (_stub_atlas_texture, stub_atlas_view) = |
| Self::create_atlas_texture_array(device, 1, 1, 1); |
| let stub_atlas_bind_group = |
| Self::create_atlas_bind_group(device, &atlas_bind_group_layout, &stub_atlas_view); |
| |
| const INITIAL_ENCODED_PAINTS_TEXTURE_HEIGHT: u32 = 1; |
| let encoded_paints_data = vec![ |
| 0; |
| ((max_texture_dimension_2d * INITIAL_ENCODED_PAINTS_TEXTURE_HEIGHT) << 4) |
| as usize |
| ]; |
| let encoded_paints_texture = Self::create_encoded_paints_texture( |
| device, |
| max_texture_dimension_2d, |
| INITIAL_ENCODED_PAINTS_TEXTURE_HEIGHT, |
| ); |
| let encoded_paints_bind_group = Self::create_encoded_paints_bind_group( |
| device, |
| &encoded_paints_bind_group_layout, |
| &encoded_paints_texture.create_view(&TextureViewDescriptor::default()), |
| ); |
| |
| const INITIAL_GRADIENT_TEXTURE_HEIGHT: u32 = 1; |
| let gradient_texture = Self::create_gradient_texture( |
| device, |
| max_texture_dimension_2d, |
| INITIAL_GRADIENT_TEXTURE_HEIGHT, |
| ); |
| let gradient_bind_group = Self::create_gradient_bind_group( |
| device, |
| &gradient_bind_group_layout, |
| &gradient_texture.create_view(&TextureViewDescriptor::default()), |
| ); |
| |
| let AtlasConfig { |
| atlas_size: (filter_atlas_width, filter_atlas_height), |
| .. |
| } = filter_texture_cache.atlas_manager().config(); |
| let filter_atlas_size = (*filter_atlas_width, *filter_atlas_height); |
| |
| // 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; ((max_texture_dimension_2d * INITIAL_FILTER_TEXTURE_HEIGHT) << 4) as usize]; |
| let filter_data_texture = Self::create_filter_data_texture( |
| device, |
| max_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 filter_atlas = FilterAtlasState::new(device, filter_atlas_size); |
| |
| let slot_bind_groups = Self::create_strip_bind_groups( |
| device, |
| &strip_bind_group_layout, |
| &alphas_texture.create_view(&TextureViewDescriptor::default()), |
| &slot_config_buffer, |
| &view_config_buffer, |
| &slot_texture_views, |
| ); |
| |
| let resources = GpuResources { |
| strips_buffer: Self::create_strips_buffer(device, 0), |
| clear_slot_indices_buffer, |
| filter_instance_buffer: Self::create_filter_instance_buffer( |
| device, |
| size_of::<FilterInstanceData>() as u64, |
| ), |
| slot_texture_views, |
| slot_config_buffer, |
| slot_bind_groups, |
| clear_bind_group, |
| alphas_texture, |
| atlas_texture_array, |
| atlas_texture_array_view, |
| atlas_bind_group, |
| filter_atlas, |
| stub_atlas_bind_group, |
| encoded_paints_texture, |
| encoded_paints_bind_group, |
| gradient_texture, |
| gradient_bind_group, |
| filter_data_texture, |
| filter_base_bind_group, |
| view_config_buffer, |
| }; |
| |
| Self { |
| strip_pipelines, |
| strip_bind_group_layout, |
| encoded_paints_bind_group_layout, |
| gradient_bind_group_layout, |
| atlas_bind_group_layout, |
| filter_bind_group_layout, |
| filter_pipeline, |
| filter_input_bind_group_layouts, |
| resources, |
| encoded_paints_data, |
| filter_data, |
| render_size: RenderSize { |
| width: render_target_config.width, |
| height: render_target_config.height, |
| }, |
| clear_pipeline, |
| atlas_clear_pipeline, |
| } |
| } |
| |
| fn create_strips_buffer(device: &Device, required_strips_size: u64) -> Buffer { |
| device.create_buffer(&wgpu::BufferDescriptor { |
| label: Some("Strips Buffer"), |
| size: required_strips_size, |
| usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, |
| mapped_at_creation: false, |
| }) |
| } |
| |
| fn create_clear_slot_indices_buffer(device: &Device, required_size: u64) -> Buffer { |
| device.create_buffer(&wgpu::BufferDescriptor { |
| label: Some("Slot Indices Buffer"), |
| size: required_size, |
| usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, |
| mapped_at_creation: false, |
| }) |
| } |
| |
| fn create_filter_instance_buffer(device: &Device, required_size: u64) -> Buffer { |
| device.create_buffer(&wgpu::BufferDescriptor { |
| label: Some("Filter Instance Buffer"), |
| size: required_size, |
| usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, |
| mapped_at_creation: false, |
| }) |
| } |
| |
| fn create_config_buffer( |
| device: &Device, |
| render_size: &RenderSize, |
| alpha_texture_width: u32, |
| ) -> Buffer { |
| device.create_buffer_init(&wgpu::util::BufferInitDescriptor { |
| label: Some("Config Buffer"), |
| contents: bytemuck::bytes_of(&Config { |
| width: render_size.width, |
| height: render_size.height, |
| strip_height: Tile::HEIGHT.into(), |
| alphas_tex_width_bits: alpha_texture_width.trailing_zeros(), |
| encoded_paints_tex_width_bits: alpha_texture_width.trailing_zeros(), |
| 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_array( |
| device: &Device, |
| width: u32, |
| height: u32, |
| atlas_count: u32, |
| ) -> (Texture, TextureView) { |
| // See the comment in `Renderer::new_with`. On WASM, we need to set this to at |
| // least 2 so it works with the wgpu WebGL backend. |
| #[cfg(target_arch = "wasm32")] |
| let depth_or_array_layers = atlas_count.max(2); |
| #[cfg(not(target_arch = "wasm32"))] |
| let depth_or_array_layers = atlas_count; |
| |
| let atlas_texture_array = device.create_texture(&wgpu::TextureDescriptor { |
| label: Some("Atlas Texture Array"), |
| size: Extent3d { |
| width, |
| height, |
| depth_or_array_layers, |
| }, |
| mip_level_count: 1, |
| sample_count: 1, |
| dimension: wgpu::TextureDimension::D2, |
| format: wgpu::TextureFormat::Rgba8Unorm, |
| usage: wgpu::TextureUsages::TEXTURE_BINDING |
| | wgpu::TextureUsages::COPY_DST |
| | wgpu::TextureUsages::COPY_SRC |
| | wgpu::TextureUsages::RENDER_ATTACHMENT, |
| view_formats: &[], |
| }); |
| |
| let atlas_texture_array_view = atlas_texture_array.create_view(&TextureViewDescriptor { |
| label: Some("Atlas Texture Array View"), |
| format: None, |
| dimension: Some(wgpu::TextureViewDimension::D2Array), |
| aspect: wgpu::TextureAspect::All, |
| base_mip_level: 0, |
| mip_level_count: None, |
| base_array_layer: 0, |
| array_layer_count: Some(atlas_count), |
| usage: None, |
| }); |
| |
| (atlas_texture_array, atlas_texture_array_view) |
| } |
| |
| fn create_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_atlas_bind_group( |
| device: &Device, |
| atlas_bind_group_layout: &BindGroupLayout, |
| atlas_texture_array_view: &TextureView, |
| ) -> BindGroup { |
| device.create_bind_group(&wgpu::BindGroupDescriptor { |
| label: Some("Atlas Bind Group"), |
| layout: atlas_bind_group_layout, |
| entries: &[wgpu::BindGroupEntry { |
| binding: 0, |
| resource: wgpu::BindingResource::TextureView(atlas_texture_array_view), |
| }], |
| }) |
| } |
| |
| fn create_encoded_paints_texture(device: &Device, width: u32, height: u32) -> Texture { |
| device.create_texture(&wgpu::TextureDescriptor { |
| label: Some("Encoded Paints Texture"), |
| size: Extent3d { |
| width, |
| height, |
| depth_or_array_layers: 1, |
| }, |
| mip_level_count: 1, |
| sample_count: 1, |
| dimension: wgpu::TextureDimension::D2, |
| format: wgpu::TextureFormat::Rgba32Uint, |
| usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, |
| view_formats: &[], |
| }) |
| } |
| |
| fn create_encoded_paints_bind_group( |
| device: &Device, |
| encoded_paints_bind_group_layout: &BindGroupLayout, |
| encoded_paints_texture_view: &TextureView, |
| ) -> BindGroup { |
| device.create_bind_group(&wgpu::BindGroupDescriptor { |
| label: Some("Encoded Paints Bind Group"), |
| layout: encoded_paints_bind_group_layout, |
| entries: &[wgpu::BindGroupEntry { |
| binding: 0, |
| resource: wgpu::BindingResource::TextureView(encoded_paints_texture_view), |
| }], |
| }) |
| } |
| |
| fn create_gradient_texture(device: &Device, width: u32, height: u32) -> Texture { |
| device.create_texture(&wgpu::TextureDescriptor { |
| label: Some("Gradient Texture"), |
| size: Extent3d { |
| width, |
| height, |
| depth_or_array_layers: 1, |
| }, |
| mip_level_count: 1, |
| sample_count: 1, |
| dimension: wgpu::TextureDimension::D2, |
| format: wgpu::TextureFormat::Rgba8Unorm, |
| usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, |
| view_formats: &[], |
| }) |
| } |
| |
| fn create_gradient_bind_group( |
| device: &Device, |
| gradient_bind_group_layout: &BindGroupLayout, |
| gradient_texture_view: &TextureView, |
| ) -> BindGroup { |
| device.create_bind_group(&wgpu::BindGroupDescriptor { |
| label: Some("Gradient Bind Group"), |
| layout: gradient_bind_group_layout, |
| entries: &[wgpu::BindGroupEntry { |
| binding: 0, |
| resource: wgpu::BindingResource::TextureView(gradient_texture_view), |
| }], |
| }) |
| } |
| |
| fn create_strip_bind_groups( |
| device: &Device, |
| strip_bind_group_layout: &BindGroupLayout, |
| alphas_texture_view: &TextureView, |
| strip_config_buffer: &Buffer, |
| config_buffer: &Buffer, |
| strip_texture_views: &[TextureView], |
| ) -> [BindGroup; 3] { |
| [ |
| Self::create_strip_bind_group( |
| device, |
| strip_bind_group_layout, |
| alphas_texture_view, |
| strip_config_buffer, |
| &strip_texture_views[1], |
| ), |
| Self::create_strip_bind_group( |
| device, |
| strip_bind_group_layout, |
| alphas_texture_view, |
| strip_config_buffer, |
| &strip_texture_views[0], |
| ), |
| Self::create_strip_bind_group( |
| device, |
| strip_bind_group_layout, |
| alphas_texture_view, |
| config_buffer, |
| &strip_texture_views[1], |
| ), |
| ] |
| } |
| |
| fn create_strip_bind_group( |
| device: &Device, |
| strip_bind_group_layout: &BindGroupLayout, |
| alphas_texture_view: &TextureView, |
| config_buffer: &Buffer, |
| strip_texture_view: &TextureView, |
| ) -> BindGroup { |
| device.create_bind_group(&wgpu::BindGroupDescriptor { |
| label: Some("Strip Bind Group"), |
| layout: strip_bind_group_layout, |
| entries: &[ |
| wgpu::BindGroupEntry { |
| binding: 0, |
| resource: wgpu::BindingResource::TextureView(alphas_texture_view), |
| }, |
| wgpu::BindGroupEntry { |
| binding: 1, |
| resource: config_buffer.as_entire_binding(), |
| }, |
| wgpu::BindGroupEntry { |
| binding: 2, |
| resource: wgpu::BindingResource::TextureView(strip_texture_view), |
| }, |
| ], |
| }) |
| } |
| |
| /// Prepare GPU buffers for rendering, given alphas. |
| /// |
| /// Specifically, updates the alpha texture with `alphas` and the config buffer when |
| /// the rendering size changes. |
| fn prepare( |
| &mut self, |
| device: &Device, |
| queue: &Queue, |
| gradient_cache: &mut GradientRampCache, |
| encoded_paints: &[GpuEncodedPaint], |
| alphas: &mut Vec<u8>, |
| new_render_size: &RenderSize, |
| paint_idxs: &[u32], |
| filter_context: &FilterContext, |
| ) { |
| let max_texture_dimension_2d = device.limits().max_texture_dimension_2d; |
| self.maybe_resize_alphas_tex(device, max_texture_dimension_2d, alphas.len()); |
| self.maybe_resize_encoded_paints_tex(device, max_texture_dimension_2d, paint_idxs); |
| self.maybe_resize_filter_tex(device, max_texture_dimension_2d, filter_context); |
| self.maybe_update_config_buffer(queue, max_texture_dimension_2d, new_render_size); |
| |
| self.upload_alpha_texture(queue, alphas); |
| self.upload_encoded_paints_texture(queue, encoded_paints); |
| self.upload_filter_texture(queue, filter_context); |
| |
| if gradient_cache.has_changed() { |
| self.maybe_resize_gradient_tex(device, max_texture_dimension_2d, gradient_cache); |
| self.upload_gradient_texture(queue, gradient_cache); |
| gradient_cache.mark_synced(); |
| } |
| } |
| |
| fn maybe_resize_filter_tex( |
| &mut self, |
| device: &Device, |
| max_texture_dimension_2d: u32, |
| filter_context: &FilterContext, |
| ) { |
| let Some(required_filter_height) = |
| filter_context.required_filter_data_height(max_texture_dimension_2d) |
| else { |
| return; |
| }; |
| debug_assert!( |
| self.resources.filter_data_texture.width() == max_texture_dimension_2d, |
| "Filter texture width must match max texture dimensions" |
| ); |
| let current_filter_height = self.resources.filter_data_texture.height(); |
| if required_filter_height > current_filter_height { |
| let required_filter_size = (max_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, |
| max_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, |
| max_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(max_texture_dimension_2d << 4); |
| debug_assert!( |
| self.resources.alphas_texture.width() == max_texture_dimension_2d, |
| "Alpha texture width must match max 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 <= max_texture_dimension_2d, |
| "Alpha texture height exceeds max 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, |
| max_texture_dimension_2d, |
| required_alpha_height, |
| ); |
| self.resources.alphas_texture = alphas_texture; |
| |
| // Since the alpha texture has changed, we need to update the clip bind groups. |
| self.resources.slot_bind_groups = Self::create_strip_bind_groups( |
| device, |
| &self.strip_bind_group_layout, |
| &self |
| .resources |
| .alphas_texture |
| .create_view(&TextureViewDescriptor::default()), |
| &self.resources.slot_config_buffer, |
| &self.resources.view_config_buffer, |
| &self.resources.slot_texture_views, |
| ); |
| } |
| } |
| |
| /// Update the encoded paints texture size if needed. |
| fn maybe_resize_encoded_paints_tex( |
| &mut self, |
| device: &Device, |
| max_texture_dimension_2d: u32, |
| paint_idxs: &[u32], |
| ) { |
| let required_texels = paint_idxs.last().unwrap(); |
| let required_encoded_paints_height = required_texels.div_ceil(max_texture_dimension_2d); |
| debug_assert!( |
| self.resources.encoded_paints_texture.width() == max_texture_dimension_2d, |
| "Encoded paints texture width must match max texture dimensions" |
| ); |
| let current_encoded_paints_height = self.resources.encoded_paints_texture.height(); |
| if required_encoded_paints_height > current_encoded_paints_height { |
| assert!( |
| required_encoded_paints_height <= max_texture_dimension_2d, |
| "Encoded paints texture height exceeds max texture dimensions" |
| ); |
| let required_encoded_paints_size = |
| (max_texture_dimension_2d * required_encoded_paints_height) << 4; |
| self.encoded_paints_data |
| .resize(required_encoded_paints_size as usize, 0); |
| let encoded_paints_texture = Self::create_encoded_paints_texture( |
| device, |
| max_texture_dimension_2d, |
| required_encoded_paints_height, |
| ); |
| self.resources.encoded_paints_texture = encoded_paints_texture; |
| |
| // Since the encoded paints texture has changed, we need to update the strip bind groups. |
| self.resources.encoded_paints_bind_group = Self::create_encoded_paints_bind_group( |
| device, |
| &self.encoded_paints_bind_group_layout, |
| &self |
| .resources |
| .encoded_paints_texture |
| .create_view(&TextureViewDescriptor::default()), |
| ); |
| } |
| } |
| |
| /// Update the gradient texture size if needed. |
| fn maybe_resize_gradient_tex( |
| &mut self, |
| device: &Device, |
| max_texture_dimension_2d: u32, |
| gradient_cache: &GradientRampCache, |
| ) { |
| let gradient_pixels = (gradient_cache.luts_size() / 4) as u32; // 4 bytes per RGBA8 pixel |
| let required_gradient_height = gradient_pixels.div_ceil(max_texture_dimension_2d); |
| debug_assert!( |
| self.resources.gradient_texture.width() == max_texture_dimension_2d, |
| "Gradient texture width must match max texture dimensions" |
| ); |
| let current_gradient_height = self.resources.gradient_texture.height(); |
| if required_gradient_height > current_gradient_height { |
| assert!( |
| required_gradient_height <= max_texture_dimension_2d, |
| "Gradient texture height exceeds max texture dimensions" |
| ); |
| let gradient_texture = Self::create_gradient_texture( |
| device, |
| max_texture_dimension_2d, |
| required_gradient_height, |
| ); |
| self.resources.gradient_texture = gradient_texture; |
| |
| // Since the gradient texture has changed, we need to update the gradient bind group. |
| self.resources.gradient_bind_group = Self::create_gradient_bind_group( |
| device, |
| &self.gradient_bind_group_layout, |
| &self |
| .resources |
| .gradient_texture |
| .create_view(&TextureViewDescriptor::default()), |
| ); |
| } |
| } |
| |
| /// Update config buffer if dimensions changed. |
| fn maybe_update_config_buffer( |
| &mut self, |
| queue: &Queue, |
| max_texture_dimension_2d: u32, |
| new_render_size: &RenderSize, |
| ) { |
| if self.render_size != *new_render_size { |
| let config = Config { |
| width: new_render_size.width, |
| height: new_render_size.height, |
| strip_height: Tile::HEIGHT.into(), |
| alphas_tex_width_bits: max_texture_dimension_2d.trailing_zeros(), |
| encoded_paints_tex_width_bits: max_texture_dimension_2d.trailing_zeros(), |
| 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(); |
| } |
| } |
| |
| /// Resize the texture array to accommodate more atlases. |
| fn maybe_resize_atlas_texture_array( |
| device: &Device, |
| encoder: &mut CommandEncoder, |
| resources: &mut GpuResources, |
| atlas_bind_group_layout: &BindGroupLayout, |
| required_atlas_count: u32, |
| ) { |
| let Extent3d { |
| width, |
| height, |
| depth_or_array_layers: current_atlas_count, |
| } = resources.atlas_texture_array.size(); |
| if required_atlas_count > current_atlas_count { |
| // Create new texture array with more layers |
| let (new_atlas_texture_array, new_atlas_texture_array_view) = |
| Self::create_atlas_texture_array(device, width, height, required_atlas_count); |
| |
| // Copy existing atlas data from old texture array to new one |
| Self::copy_atlas_texture_data( |
| encoder, |
| &resources.atlas_texture_array, |
| &new_atlas_texture_array, |
| current_atlas_count, |
| width, |
| height, |
| ); |
| |
| // Update the bind group with the new texture array view |
| let new_atlas_bind_group = Self::create_atlas_bind_group( |
| device, |
| atlas_bind_group_layout, |
| &new_atlas_texture_array_view, |
| ); |
| |
| // Replace the old resources |
| resources.atlas_texture_array = new_atlas_texture_array; |
| resources.atlas_texture_array_view = new_atlas_texture_array_view; |
| resources.atlas_bind_group = new_atlas_bind_group; |
| } |
| } |
| |
| /// Copy texture data from the old atlas texture array to a new one. |
| /// This is necessary when resizing the texture array to preserve existing atlas data. |
| fn copy_atlas_texture_data( |
| encoder: &mut CommandEncoder, |
| old_atlas_texture_array: &Texture, |
| new_atlas_texture_array: &Texture, |
| layer_count_to_copy: u32, |
| width: u32, |
| height: u32, |
| ) { |
| // Copy all layers from old texture array to new texture array |
| encoder.copy_texture_to_texture( |
| wgpu::TexelCopyTextureInfo { |
| texture: old_atlas_texture_array, |
| mip_level: 0, |
| origin: wgpu::Origin3d { x: 0, y: 0, z: 0 }, |
| aspect: wgpu::TextureAspect::All, |
| }, |
| wgpu::TexelCopyTextureInfo { |
| texture: new_atlas_texture_array, |
| mip_level: 0, |
| origin: wgpu::Origin3d { x: 0, y: 0, z: 0 }, |
| aspect: wgpu::TextureAspect::All, |
| }, |
| Extent3d { |
| width, |
| height, |
| depth_or_array_layers: layer_count_to_copy, |
| }, |
| ); |
| } |
| |
| /// Upload 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 total_size = texture_width as usize * texture_height as usize * 16; |
| |
| let original_len = alphas.len(); |
| |
| // Temporarily pad the length of the alphas to the texture size before uploading. |
| alphas.resize(total_size, 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(texture_height), |
| }, |
| Extent3d { |
| width: texture_width, |
| height: texture_height, |
| 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]) { |
| let encoded_paints_texture = &self.resources.encoded_paints_texture; |
| let encoded_paints_texture_width = encoded_paints_texture.width(); |
| let encoded_paints_texture_height = encoded_paints_texture.height(); |
| |
| GpuEncodedPaint::serialize_to_buffer(encoded_paints, &mut self.encoded_paints_data); |
| queue.write_texture( |
| wgpu::TexelCopyTextureInfo { |
| texture: encoded_paints_texture, |
| mip_level: 0, |
| origin: wgpu::Origin3d::ZERO, |
| aspect: wgpu::TextureAspect::All, |
| }, |
| &self.encoded_paints_data, |
| wgpu::TexelCopyBufferLayout { |
| offset: 0, |
| // 16 bytes per RGBA32Uint texel (4 u32s × 4 bytes each), equivalent to bit shift of 4 |
| bytes_per_row: Some(encoded_paints_texture_width << 4), |
| rows_per_image: Some(encoded_paints_texture_height), |
| }, |
| Extent3d { |
| width: encoded_paints_texture_width, |
| height: encoded_paints_texture_height, |
| depth_or_array_layers: 1, |
| }, |
| ); |
| } |
| |
| 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(); |
| |
| filter_context.serialize_to_buffer(&mut self.filter_data); |
| queue.write_texture( |
| wgpu::TexelCopyTextureInfo { |
| texture: filter_texture, |
| mip_level: 0, |
| origin: wgpu::Origin3d::ZERO, |
| aspect: wgpu::TextureAspect::All, |
| }, |
| &self.filter_data, |
| wgpu::TexelCopyBufferLayout { |
| offset: 0, |
| bytes_per_row: Some(width << 4), |
| rows_per_image: Some(height), |
| }, |
| Extent3d { |
| width, |
| height, |
| depth_or_array_layers: 1, |
| }, |
| ); |
| } |
| |
| /// Upload gradient data to the texture. |
| fn upload_gradient_texture(&mut self, queue: &Queue, gradient_cache: &mut GradientRampCache) { |
| let gradient_texture = &self.resources.gradient_texture; |
| let gradient_texture_width = gradient_texture.width(); |
| let gradient_texture_height = gradient_texture.height(); |
| |
| // Upload the gradient LUT data |
| if !gradient_cache.is_empty() { |
| let total_capacity = (gradient_texture_width * gradient_texture_height * 4) as usize; |
| |
| // Take ownership of the luts to avoid copying, then resize for texture padding |
| let mut luts = gradient_cache.take_luts(); |
| let old_luts_len = luts.len(); |
| luts.resize(total_capacity, 0); |
| |
| queue.write_texture( |
| wgpu::TexelCopyTextureInfo { |
| texture: gradient_texture, |
| mip_level: 0, |
| origin: wgpu::Origin3d::ZERO, |
| aspect: wgpu::TextureAspect::All, |
| }, |
| &luts, |
| wgpu::TexelCopyBufferLayout { |
| offset: 0, |
| // 4 bytes per RGBA8 pixel |
| bytes_per_row: Some(gradient_texture_width << 2), |
| rows_per_image: Some(gradient_texture_height), |
| }, |
| Extent3d { |
| width: gradient_texture_width, |
| height: gradient_texture_height, |
| depth_or_array_layers: 1, |
| }, |
| ); |
| |
| // Restore the luts back to the cache |
| luts.truncate(old_luts_len); |
| gradient_cache.restore_luts(luts); |
| } |
| } |
| |
| /// Upload the strip data by creating and assigning a new `self.resources.strips_buffer`. |
| fn upload_strips(&mut self, device: &Device, queue: &Queue, strips: &[GpuStrip]) { |
| let required_strips_size = size_of_val(strips) as u64; |
| self.resources.strips_buffer = Self::create_strips_buffer(device, required_strips_size); |
| // TODO: Consider using a staging belt to avoid an extra staging buffer allocation. |
| let mut buffer = queue |
| .write_buffer_with( |
| &self.resources.strips_buffer, |
| 0, |
| required_strips_size.try_into().unwrap(), |
| ) |
| .expect("Capacity handled in creation"); |
| buffer.copy_from_slice(bytemuck::cast_slice(strips)); |
| } |
| } |
| |
| /// A struct containing references to the many objects needed to get work |
| /// scheduled onto the GPU. |
| struct RendererContext<'a> { |
| programs: &'a mut Programs, |
| device: &'a Device, |
| queue: &'a Queue, |
| encoder: &'a mut CommandEncoder, |
| view: &'a TextureView, |
| image_cache: &'a ImageCache, |
| filter_context: &'a FilterContext, |
| filter_pass_state: &'a mut FilterPassState, |
| } |
| |
| impl RendererContext<'_> { |
| /// Render the strips to the specified render target. |
| fn do_strip_render_pass( |
| &mut self, |
| strips: &[GpuStrip], |
| target: StripPassRenderTarget, |
| load: wgpu::LoadOp<wgpu::Color>, |
| ) { |
| if strips.is_empty() { |
| return; |
| } |
| // TODO: We currently allocate a new strips buffer for each render pass. A more efficient |
| // approach would be to re-use buffers or slices of a larger buffer. |
| self.programs.upload_strips(self.device, self.queue, strips); |
| |
| enum MaybeOwned<'a, T> { |
| Borrowed(&'a T), |
| Owned(T), |
| } |
| |
| impl<T> AsRef<T> for MaybeOwned<'_, T> { |
| fn as_ref(&self) -> &T { |
| match self { |
| Self::Borrowed(r) => r, |
| Self::Owned(v) => v, |
| } |
| } |
| } |
| let (view, bind_group, scissor_rect): ( |
| &TextureView, |
| MaybeOwned<'_, BindGroup>, |
| Option<[u32; 4]>, |
| ) = match target { |
| StripPassRenderTarget::Root(_) => ( |
| self.view, |
| MaybeOwned::Borrowed(&self.programs.resources.slot_bind_groups[2]), |
| None, |
| ), |
| StripPassRenderTarget::FilterLayer(layer_id) => { |
| let image_id = self |
| .filter_context |
| .filter_textures |
| .get(&layer_id) |
| .unwrap() |
| .initial_image_id; |
| let resources = self.filter_context.image_cache.get(image_id).unwrap(); |
| |
| let atlas_idx = resources.atlas_id.as_u32() as usize; |
| let filter_atlas = &self.programs.resources.filter_atlas; |
| |
| let atlas_size = filter_atlas.textures[atlas_idx].size(); |
| let filter_textures = self.filter_context.filter_textures.get(&layer_id).unwrap(); |
| |
| // Two offsets are needed: |
| // 1. Account for the intermediate texture living at an offset within its atlas. |
| // 2. Account for the filter layer bbox not starting at (0, 0). |
| let strip_offset_x = resources.offset[0] as i32 |
| - (filter_textures.bbox.x0() * WideTile::WIDTH) as i32; |
| let strip_offset_y = |
| resources.offset[1] as i32 - (filter_textures.bbox.y0() * Tile::HEIGHT) as i32; |
| |
| // TODO: Cache this and bind group? See https://github.com/linebender/vello/pull/1494#discussion_r2937895891. |
| let atlas_config_buffer = |
| self.device |
| .create_buffer_init(&wgpu::util::BufferInitDescriptor { |
| label: Some("Filter Strip Config Buffer"), |
| contents: bytemuck::bytes_of(&Config { |
| width: atlas_size.width, |
| height: atlas_size.height, |
| strip_height: Tile::HEIGHT.into(), |
| alphas_tex_width_bits: self |
| .programs |
| .resources |
| .alphas_texture |
| .size() |
| .width |
| .trailing_zeros(), |
| encoded_paints_tex_width_bits: self |
| .programs |
| .resources |
| .alphas_texture |
| .size() |
| .width |
| .trailing_zeros(), |
| strip_offset_x, |
| strip_offset_y, |
| negate_ndc: 0, |
| }), |
| usage: wgpu::BufferUsages::UNIFORM, |
| }); |
| |
| let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { |
| label: Some("Filter Strip Bind Group"), |
| layout: &self.programs.strip_bind_group_layout, |
| entries: &[ |
| wgpu::BindGroupEntry { |
| binding: 0, |
| resource: wgpu::BindingResource::TextureView( |
| &self |
| .programs |
| .resources |
| .alphas_texture |
| .create_view(&TextureViewDescriptor::default()), |
| ), |
| }, |
| wgpu::BindGroupEntry { |
| binding: 1, |
| resource: atlas_config_buffer.as_entire_binding(), |
| }, |
| wgpu::BindGroupEntry { |
| binding: 2, |
| resource: wgpu::BindingResource::TextureView( |
| &self.programs.resources.slot_texture_views[1], |
| ), |
| }, |
| ], |
| }); |
| |
| ( |
| &filter_atlas.views[atlas_idx], |
| MaybeOwned::Owned(bind_group), |
| Some([ |
| resources.offset[0] as u32, |
| resources.offset[1] as u32, |
| resources.width as u32, |
| resources.height as u32, |
| ]), |
| ) |
| } |
| StripPassRenderTarget::SlotTexture(idx) => ( |
| &self.programs.resources.slot_texture_views[idx as usize], |
| MaybeOwned::Borrowed(&self.programs.resources.slot_bind_groups[idx as usize]), |
| None, |
| ), |
| }; |
| |
| let pipeline_idx = if matches!( |
| target, |
| StripPassRenderTarget::Root(RootRenderTarget::AtlasLayer) |
| | StripPassRenderTarget::FilterLayer(_) |
| ) { |
| 1 |
| } else { |
| 0 |
| }; |
| |
| 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, |
| store: wgpu::StoreOp::Store, |
| }, |
| })], |
| depth_stencil_attachment: None, |
| occlusion_query_set: None, |
| timestamp_writes: None, |
| multiview_mask: None, |
| }); |
| if let Some([x, y, width, height]) = scissor_rect { |
| render_pass.set_scissor_rect(x, y, width, height); |
| } |
| render_pass.set_pipeline(&self.programs.strip_pipelines[pipeline_idx]); |
| render_pass.set_bind_group(0, bind_group.as_ref(), &[]); |
| render_pass.set_bind_group(1, &self.programs.resources.atlas_bind_group, &[]); |
| render_pass.set_bind_group(2, &self.programs.resources.encoded_paints_bind_group, &[]); |
| render_pass.set_bind_group(3, &self.programs.resources.gradient_bind_group, &[]); |
| render_pass.set_vertex_buffer(0, self.programs.resources.strips_buffer.slice(..)); |
| render_pass.draw(0..4, 0..u32::try_from(strips.len()).unwrap()); |
| } |
| |
| /// Clear specific slots from a slot texture. |
| fn do_clear_slots_render_pass(&mut self, ix: usize, slot_indices: &[u32]) { |
| if slot_indices.is_empty() { |
| return; |
| } |
| |
| let resources = &mut self.programs.resources; |
| let size = size_of_val(slot_indices) as u64; |
| // TODO: We currently allocate a new strips buffer for each render pass. A more efficient |
| // approach would be to re-use buffers or slices of a larger buffer. |
| resources.clear_slot_indices_buffer = |
| Programs::create_clear_slot_indices_buffer(self.device, size); |
| // TODO: Consider using a staging belt to avoid an extra staging buffer allocation. |
| let mut buffer = self |
| .queue |
| .write_buffer_with( |
| &resources.clear_slot_indices_buffer, |
| 0, |
| size.try_into().unwrap(), |
| ) |
| .expect("Capacity handled in creation"); |
| buffer.copy_from_slice(bytemuck::cast_slice(slot_indices)); |
| |
| { |
| let mut render_pass = self.encoder.begin_render_pass(&RenderPassDescriptor { |
| label: Some("Clear Slots Render Pass"), |
| color_attachments: &[Some(RenderPassColorAttachment { |
| view: &resources.slot_texture_views[ix], |
| depth_slice: None, |
| resolve_target: None, |
| ops: wgpu::Operations { |
| // Don't clear the entire texture, just specific slots |
| load: wgpu::LoadOp::Load, |
| store: wgpu::StoreOp::Store, |
| }, |
| })], |
| depth_stencil_attachment: None, |
| occlusion_query_set: None, |
| timestamp_writes: None, |
| multiview_mask: None, |
| }); |
| |
| render_pass.set_pipeline(&self.programs.clear_pipeline); |
| render_pass.set_bind_group(0, &resources.clear_bind_group, &[]); |
| render_pass.set_vertex_buffer(0, resources.clear_slot_indices_buffer.slice(..)); |
| render_pass.draw(0..4, 0..u32::try_from(slot_indices.len()).unwrap()); |
| } |
| } |
| } |
| |
| impl RendererBackend for RendererContext<'_> { |
| /// Execute the render pass for clearing slots. |
| fn clear_slots(&mut self, texture_index: usize, slots: &[u32]) { |
| self.do_clear_slots_render_pass(texture_index, slots); |
| } |
| |
| /// Execute the render pass for rendering strips. |
| fn render_strips( |
| &mut self, |
| strips: &[GpuStrip], |
| target: StripPassRenderTarget, |
| load_op: LoadOp, |
| ) { |
| let wgpu_load_op = match load_op { |
| LoadOp::Load => wgpu::LoadOp::Load, |
| LoadOp::Clear => wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), |
| }; |
| self.do_strip_render_pass(strips, target, wgpu_load_op); |
| } |
| |
| fn apply_filter(&mut self, layer_id: LayerId) { |
| let filter_atlas = &self.programs.resources.filter_atlas; |
| self.filter_context.build_filter_passes( |
| self.filter_pass_state, |
| &layer_id, |
| self.image_cache, |
| |atlas_idx| { |
| let size = filter_atlas.textures[atlas_idx as usize].size(); |
| [size.width, size.height] |
| }, |
| || { |
| let size = self.programs.resources.atlas_texture_array.size(); |
| [size.width, size.height] |
| }, |
| ); |
| |
| let filter_passes = self.filter_pass_state.filter_passes(); |
| if filter_passes.is_empty() { |
| return; |
| } |
| |
| let instances = self.filter_pass_state.instances(); |
| let instance_stride = size_of::<FilterInstanceData>() as u64; |
| let total_size = instances.len() as u64 * instance_stride; |
| // TODO: Reuse buffer (https://github.com/linebender/vello/pull/1494#discussion_r2937890819) |
| self.programs.resources.filter_instance_buffer = |
| Programs::create_filter_instance_buffer(self.device, total_size); |
| self.queue.write_buffer( |
| &self.programs.resources.filter_instance_buffer, |
| 0, |
| bytemuck::cast_slice(instances), |
| ); |
| |
| let programs = &self.programs; |
| let encoder = &mut self.encoder; |
| let filter_atlas = &programs.resources.filter_atlas; |
| for (i, pass) in filter_passes.iter().enumerate() { |
| let input_bg = &filter_atlas.input_bind_groups[pass.input_atlas_idx as usize]; |
| // If this is `None`, it's unused, so we can just pass anything here. |
| let original_idx = pass.original_atlas_idx.unwrap_or(pass.input_atlas_idx) as usize; |
| let original_bg = &filter_atlas.original_bind_groups[original_idx]; |
| |
| let (output_view, target_width, target_height) = match &pass.output { |
| FilterPassTarget::FilterAtlas(idx) => { |
| let size = filter_atlas.textures[*idx as usize].size(); |
| (&filter_atlas.views[*idx as usize], size.width, size.height) |
| } |
| FilterPassTarget::MainAtlas(idx) => { |
| let size = programs.resources.atlas_texture_array.size(); |
| ( |
| &create_atlas_layer_view(&programs.resources.atlas_texture_array, *idx), |
| size.width, |
| size.height, |
| ) |
| } |
| }; |
| |
| let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor { |
| label: Some("Apply Filter Pass"), |
| color_attachments: &[Some(RenderPassColorAttachment { |
| view: output_view, |
| resolve_target: None, |
| ops: wgpu::Operations { |
| 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, |
| }); |
| let instance = &instances[i]; |
| let [x, y, width, height] = instance.scissor_rect([target_width, target_height]); |
| render_pass.set_scissor_rect(x, y, width, height); |
| render_pass.set_pipeline(&programs.filter_pipeline); |
| render_pass.set_bind_group(0, &programs.resources.filter_base_bind_group, &[]); |
| render_pass.set_bind_group(1, input_bg, &[]); |
| render_pass.set_bind_group(2, original_bg, &[]); |
| render_pass.set_vertex_buffer( |
| 0, |
| programs |
| .resources |
| .filter_instance_buffer |
| .slice((i as u64 * instance_stride)..((i as u64 + 1) * instance_stride)), |
| ); |
| render_pass.draw(0..4, 0..1); |
| } |
| } |
| } |
| |
| 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_atlas_layer_view(atlas: &Texture, layer: u32) -> TextureView { |
| atlas.create_view(&TextureViewDescriptor { |
| label: Some("Atlas Layer View"), |
| format: None, |
| dimension: Some(wgpu::TextureViewDimension::D2), |
| aspect: wgpu::TextureAspect::All, |
| base_mip_level: 0, |
| mip_level_count: None, |
| base_array_layer: layer, |
| array_layer_count: Some(1), |
| usage: None, |
| }) |
| } |
| |
| /// Trait for types that can write image data directly to the atlas texture. |
| /// |
| /// This allows efficient uploading from different sources: |
| /// - `Pixmap`: Direct upload without intermediate texture |
| /// - `Texture`: Texture-to-texture copy |
| /// - Custom implementations for other image sources |
| pub trait AtlasWriter { |
| /// Get the width of the image. |
| fn width(&self) -> u32; |
| /// Get the height of the image. |
| fn height(&self) -> u32; |
| |
| /// Write image data to a specific layer of an atlas texture array at the specified offset. |
| fn write_to_atlas_layer( |
| &self, |
| device: &Device, |
| queue: &Queue, |
| encoder: &mut CommandEncoder, |
| atlas_texture: &Texture, |
| layer: u32, |
| offset: [u32; 2], |
| width: u32, |
| height: u32, |
| ); |
| } |
| |
| /// Implementation for `wgpu::Texture` - uses texture-to-texture copy |
| impl AtlasWriter for Texture { |
| fn width(&self) -> u32 { |
| self.width() |
| } |
| |
| fn height(&self) -> u32 { |
| self.height() |
| } |
| |
| fn write_to_atlas_layer( |
| &self, |
| _device: &Device, |
| _queue: &Queue, |
| encoder: &mut CommandEncoder, |
| atlas_texture: &Texture, |
| layer: u32, |
| offset: [u32; 2], |
| width: u32, |
| height: u32, |
| ) { |
| encoder.copy_texture_to_texture( |
| wgpu::TexelCopyTextureInfo { |
| texture: self, |
| mip_level: 0, |
| origin: wgpu::Origin3d::ZERO, |
| aspect: wgpu::TextureAspect::All, |
| }, |
| wgpu::TexelCopyTextureInfo { |
| texture: atlas_texture, |
| mip_level: 0, |
| origin: wgpu::Origin3d { |
| x: offset[0], |
| y: offset[1], |
| z: layer, |
| }, |
| aspect: wgpu::TextureAspect::All, |
| }, |
| Extent3d { |
| width, |
| height, |
| depth_or_array_layers: 1, |
| }, |
| ); |
| } |
| } |
| |
| /// Implementation for `Pixmap` - direct upload to atlas |
| impl AtlasWriter for Pixmap { |
| fn width(&self) -> u32 { |
| self.width() as u32 |
| } |
| |
| fn height(&self) -> u32 { |
| self.height() as u32 |
| } |
| |
| fn write_to_atlas_layer( |
| &self, |
| _device: &Device, |
| queue: &Queue, |
| _encoder: &mut CommandEncoder, |
| atlas_texture: &Texture, |
| layer: u32, |
| offset: [u32; 2], |
| width: u32, |
| height: u32, |
| ) { |
| queue.write_texture( |
| wgpu::TexelCopyTextureInfo { |
| texture: atlas_texture, |
| mip_level: 0, |
| origin: wgpu::Origin3d { |
| x: offset[0], |
| y: offset[1], |
| z: layer, |
| }, |
| aspect: wgpu::TextureAspect::All, |
| }, |
| self.data_as_u8_slice(), |
| wgpu::TexelCopyBufferLayout { |
| offset: 0, |
| bytes_per_row: Some(4 * width), |
| rows_per_image: Some(height), |
| }, |
| Extent3d { |
| width, |
| height, |
| depth_or_array_layers: 1, |
| }, |
| ); |
| } |
| } |
| |
| /// Implementation for `Arc<Pixmap>` |
| impl AtlasWriter for Arc<Pixmap> { |
| fn width(&self) -> u32 { |
| self.as_ref().width() as u32 |
| } |
| |
| fn height(&self) -> u32 { |
| self.as_ref().height() as u32 |
| } |
| |
| fn write_to_atlas_layer( |
| &self, |
| device: &Device, |
| queue: &Queue, |
| encoder: &mut CommandEncoder, |
| atlas_texture: &Texture, |
| layer: u32, |
| offset: [u32; 2], |
| width: u32, |
| height: u32, |
| ) { |
| self.as_ref().write_to_atlas_layer( |
| device, |
| queue, |
| encoder, |
| atlas_texture, |
| layer, |
| offset, |
| width, |
| height, |
| ); |
| } |
| } |