From dbd68ff1396bf498eb97f095810b78d2bddd0011 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 13 Apr 2026 04:40:19 +0200 Subject: [PATCH] refactor: gpu webgl fallback and batched command encoding Made-with: Cursor --- rust/crates/effects/src/pipeline.rs | 42 +- rust/crates/gpu/Cargo.toml | 6 +- rust/crates/gpu/src/context.rs | 413 +++++++++++++++----- rust/crates/gpu/src/shaders/fullscreen.wgsl | 2 +- rust/crates/masks/src/feather.rs | 46 ++- rust/crates/masks/src/sdf.rs | 48 ++- rust/crates/masks/src/shaders/jfa_init.wgsl | 5 +- rust/crates/masks/src/shaders/jfa_step.wgsl | 2 +- 8 files changed, 439 insertions(+), 125 deletions(-) diff --git a/rust/crates/effects/src/pipeline.rs b/rust/crates/effects/src/pipeline.rs index 58692bde..1ea49053 100644 --- a/rust/crates/effects/src/pipeline.rs +++ b/rust/crates/effects/src/pipeline.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use bytemuck::{Pod, Zeroable}; -use gpu::{FULLSCREEN_SHADER_SOURCE, GPU_TEXTURE_FORMAT, GpuContext}; +use gpu::{FULLSCREEN_SHADER_SOURCE, GpuContext}; use thiserror::Error; use wgpu::util::DeviceExt; @@ -119,7 +119,7 @@ impl EffectPipeline { module: &gaussian_blur_shader_module, entry_point: Some("fragment_main"), targets: &[Some(wgpu::ColorTargetState { - format: GPU_TEXTURE_FORMAT, + format: context.texture_format(), blend: None, write_mask: wgpu::ColorWrites::ALL, })], @@ -149,6 +149,37 @@ impl EffectPipeline { height, passes, }: ApplyEffectsOptions<'_>, + ) -> Result { + let mut encoder = + context + .device() + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("effects-command-encoder"), + }); + let output = self.apply_with_encoder( + context, + &mut encoder, + ApplyEffectsOptions { + source, + width, + height, + passes, + }, + )?; + context.queue().submit([encoder.finish()]); + Ok(output) + } + + pub fn apply_with_encoder( + &self, + context: &GpuContext, + encoder: &mut wgpu::CommandEncoder, + ApplyEffectsOptions { + source, + width, + height, + passes, + }: ApplyEffectsOptions<'_>, ) -> Result { let mut current_texture: Option = None; @@ -199,12 +230,6 @@ impl EffectPipeline { shader: pass.shader.clone(), } })?; - let mut encoder = - context - .device() - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("effects-command-encoder"), - }); { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { @@ -230,7 +255,6 @@ impl EffectPipeline { render_pass.draw(0..6, 0..1); } - context.queue().submit([encoder.finish()]); current_texture = Some(output_texture); } diff --git a/rust/crates/gpu/Cargo.toml b/rust/crates/gpu/Cargo.toml index 4be1e8d6..4ee7571e 100644 --- a/rust/crates/gpu/Cargo.toml +++ b/rust/crates/gpu/Cargo.toml @@ -8,6 +8,10 @@ bytemuck = { version = "1.25.0", features = ["derive"] } thiserror = "2.0.18" wgpu = "29.0.1" +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2.116" +web-sys = { version = "0.3.93", features = ["Document", "Window", "HtmlCanvasElement", "OffscreenCanvasRenderingContext2d", "ImageData"] } + [features] default = [] -wasm = [] +wasm = ["wgpu/webgl"] diff --git a/rust/crates/gpu/src/context.rs b/rust/crates/gpu/src/context.rs index 1526a67d..e72a959b 100644 --- a/rust/crates/gpu/src/context.rs +++ b/rust/crates/gpu/src/context.rs @@ -1,6 +1,21 @@ use wgpu::util::DeviceExt; -use crate::{FULLSCREEN_SHADER_SOURCE, GPU_TEXTURE_FORMAT, GpuError}; +#[cfg(all(feature = "wasm", target_arch = "wasm32"))] +use wasm_bindgen::JsCast; + +use crate::{FULLSCREEN_SHADER_SOURCE, GpuError}; + +#[cfg(all(feature = "wasm", target_arch = "wasm32"))] +#[derive(Debug)] +struct WebDisplay; + +#[cfg(all(feature = "wasm", target_arch = "wasm32"))] +impl wgpu::rwh::HasDisplayHandle for WebDisplay { + fn display_handle(&self) -> Result, wgpu::rwh::HandleError> { + let raw = wgpu::rwh::WebDisplayHandle::new(); + Ok(unsafe { wgpu::rwh::DisplayHandle::borrow_raw(raw.into()) }) + } +} const BLIT_SHADER_SOURCE: &str = include_str!("shaders/blit.wgsl"); @@ -18,35 +33,23 @@ pub struct GpuContext { adapter: wgpu::Adapter, device: wgpu::Device, queue: wgpu::Queue, + texture_format: wgpu::TextureFormat, fullscreen_quad: wgpu::Buffer, linear_sampler: wgpu::Sampler, nearest_sampler: wgpu::Sampler, texture_sampler_bind_group_layout: wgpu::BindGroupLayout, blit_pipeline: wgpu::RenderPipeline, + supports_external_texture_copies: bool, } impl GpuContext { pub async fn new() -> Result { - let instance = wgpu::Instance::default(); - let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::HighPerformance, - compatible_surface: None, - force_fallback_adapter: false, - }) - .await - .map_err(|_| GpuError::AdapterUnavailable)?; - let (device, queue) = adapter - .request_device(&wgpu::DeviceDescriptor { - label: Some("gpu-device"), - required_features: wgpu::Features::empty(), - required_limits: wgpu::Limits::downlevel_webgl2_defaults() - .using_resolution(adapter.limits()), - memory_hints: wgpu::MemoryHints::Performance, - experimental_features: wgpu::ExperimentalFeatures::disabled(), - trace: wgpu::Trace::Off, - }) - .await?; + let (instance, adapter, device, queue) = Self::acquire_device().await?; + let texture_format = if adapter.get_info().backend == wgpu::Backend::Gl { + wgpu::TextureFormat::Rgba8Unorm + } else { + wgpu::TextureFormat::Bgra8Unorm + }; let fullscreen_quad = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("gpu-fullscreen-quad-buffer"), contents: bytemuck::cast_slice(&FULLSCREEN_QUAD_POSITIONS), @@ -128,7 +131,7 @@ impl GpuContext { module: &blit_shader_module, entry_point: Some("fragment_main"), targets: &[Some(wgpu::ColorTargetState { - format: GPU_TEXTURE_FORMAT, + format: texture_format, blend: None, write_mask: wgpu::ColorWrites::ALL, })], @@ -141,19 +144,99 @@ impl GpuContext { cache: None, }); + let supports_external_texture_copies = adapter + .get_downlevel_capabilities() + .flags + .contains(wgpu::DownlevelFlags::UNRESTRICTED_EXTERNAL_TEXTURE_COPIES); + Ok(Self { instance, adapter, device, queue, + texture_format, fullscreen_quad, linear_sampler, nearest_sampler, texture_sampler_bind_group_layout, blit_pipeline, + supports_external_texture_copies, }) } + async fn acquire_device() + -> Result<(wgpu::Instance, wgpu::Adapter, wgpu::Device, wgpu::Queue), GpuError> { + let instance = wgpu::util::new_instance_with_webgpu_detection( + wgpu::InstanceDescriptor::new_without_display_handle(), + ) + .await; + + match Self::try_request_device(&instance, None).await { + Ok((adapter, device, queue)) => return Ok((instance, adapter, device, queue)), + Err(_) => {} + } + + Self::try_gl_fallback().await + } + + #[cfg(all(feature = "wasm", target_arch = "wasm32"))] + async fn try_gl_fallback() + -> Result<(wgpu::Instance, wgpu::Adapter, wgpu::Device, wgpu::Queue), GpuError> { + let mut gl_desc = wgpu::InstanceDescriptor::new_without_display_handle(); + gl_desc.backends = wgpu::Backends::GL; + gl_desc.display = Some(Box::new(WebDisplay)); + let gl_instance = wgpu::Instance::new(gl_desc); + + let document = web_sys::window() + .and_then(|w| w.document()) + .ok_or(GpuError::AdapterUnavailable)?; + let canvas: web_sys::HtmlCanvasElement = document + .create_element("canvas") + .map_err(|_| GpuError::AdapterUnavailable)? + .unchecked_into(); + canvas.set_width(1); + canvas.set_height(1); + let surface = gl_instance.create_surface(wgpu::SurfaceTarget::Canvas(canvas))?; + + let (adapter, device, queue) = + Self::try_request_device(&gl_instance, Some(&surface)).await?; + Ok((gl_instance, adapter, device, queue)) + } + + #[cfg(not(all(feature = "wasm", target_arch = "wasm32")))] + async fn try_gl_fallback() + -> Result<(wgpu::Instance, wgpu::Adapter, wgpu::Device, wgpu::Queue), GpuError> { + Err(GpuError::AdapterUnavailable) + } + + async fn try_request_device( + instance: &wgpu::Instance, + compatible_surface: Option<&wgpu::Surface<'_>>, + ) -> Result<(wgpu::Adapter, wgpu::Device, wgpu::Queue), GpuError> { + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface, + force_fallback_adapter: false, + }) + .await + .map_err(|_| GpuError::AdapterUnavailable)?; + + let (device, queue) = adapter + .request_device(&wgpu::DeviceDescriptor { + label: Some("gpu-device"), + required_features: wgpu::Features::empty(), + required_limits: wgpu::Limits::downlevel_webgl2_defaults() + .using_resolution(adapter.limits()), + memory_hints: wgpu::MemoryHints::Performance, + experimental_features: wgpu::ExperimentalFeatures::disabled(), + trace: wgpu::Trace::Off, + }) + .await?; + + Ok((adapter, device, queue)) + } + pub fn create_render_texture( &self, width: u32, @@ -170,7 +253,7 @@ impl GpuContext { mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: GPU_TEXTURE_FORMAT, + format: self.texture_format, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC @@ -195,6 +278,10 @@ impl GpuContext { &self.queue } + pub fn texture_format(&self) -> wgpu::TextureFormat { + self.texture_format + } + pub fn fullscreen_quad(&self) -> &wgpu::Buffer { &self.fullscreen_quad } @@ -211,36 +298,72 @@ impl GpuContext { &self.texture_sampler_bind_group_layout } + pub fn blit_pipeline(&self) -> &wgpu::RenderPipeline { + &self.blit_pipeline + } + pub fn render_texture_to_surface( &self, texture: &wgpu::Texture, surface: &wgpu::Surface<'_>, width: u32, height: u32, + ) -> Result<(), GpuError> { + self.configure_surface(surface, width, height)?; + let surface_texture = self.acquire_surface_texture(surface)?; + let target_view = surface_texture + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("gpu-surface-blit-encoder"), + }); + self.encode_texture_blit_to_view(&mut encoder, texture, &target_view, "gpu-surface-blit"); + self.queue.submit([encoder.finish()]); + surface_texture.present(); + Ok(()) + } + + pub fn configure_surface( + &self, + surface: &wgpu::Surface<'_>, + width: u32, + height: u32, ) -> Result<(), GpuError> { let Some(config) = surface.get_default_config(&self.adapter, width, height) else { return Err(GpuError::UnsupportedSurfaceFormat); }; - if config.format != GPU_TEXTURE_FORMAT { + if config.format != self.texture_format { return Err(GpuError::UnsupportedSurfaceFormat); } surface.configure(&self.device, &config); + Ok(()) + } - let surface_texture = match surface.get_current_texture() { + pub fn acquire_surface_texture( + &self, + surface: &wgpu::Surface<'_>, + ) -> Result { + match surface.get_current_texture() { wgpu::CurrentSurfaceTexture::Success(surface_texture) - | wgpu::CurrentSurfaceTexture::Suboptimal(surface_texture) => surface_texture, + | wgpu::CurrentSurfaceTexture::Suboptimal(surface_texture) => Ok(surface_texture), wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded | wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost - | wgpu::CurrentSurfaceTexture::Validation => { - return Err(GpuError::UnsupportedSurfaceFormat); - } - }; + | wgpu::CurrentSurfaceTexture::Validation => Err(GpuError::UnsupportedSurfaceFormat), + } + } + + pub fn encode_texture_blit_to_view( + &self, + encoder: &mut wgpu::CommandEncoder, + texture: &wgpu::Texture, + target_view: &wgpu::TextureView, + label: &'static str, + ) { let source_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - let target_view = surface_texture - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("gpu-blit-bind-group"), layout: &self.texture_sampler_bind_group_layout, @@ -255,38 +378,27 @@ impl GpuContext { }, ], }); - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("gpu-surface-blit-encoder"), - }); - { - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("gpu-surface-blit-pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &target_view, - resolve_target: None, - depth_slice: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - occlusion_query_set: None, - timestamp_writes: None, - multiview_mask: None, - }); - render_pass.set_pipeline(&self.blit_pipeline); - render_pass.set_vertex_buffer(0, self.fullscreen_quad.slice(..)); - render_pass.set_bind_group(0, &bind_group, &[]); - render_pass.draw(0..6, 0..1); - } - - self.queue.submit([encoder.finish()]); - surface_texture.present(); - Ok(()) + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: target_view, + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + render_pass.set_pipeline(&self.blit_pipeline); + render_pass.set_vertex_buffer(0, self.fullscreen_quad.slice(..)); + render_pass.set_bind_group(0, &bind_group, &[]); + render_pass.draw(0..6, 0..1); } #[cfg(all(feature = "wasm", target_arch = "wasm32"))] @@ -298,26 +410,71 @@ impl GpuContext { label: &'static str, ) -> wgpu::Texture { let texture = self.create_render_texture(width, height, label); - self.queue.copy_external_image_to_texture( - &wgpu::CopyExternalImageSourceInfo { - source: wgpu::ExternalImageSource::OffscreenCanvas(canvas.clone()), - origin: wgpu::Origin2d::ZERO, - flip_y: true, - }, - wgpu::CopyExternalImageDestInfo { - texture: &texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - color_space: wgpu::PredefinedColorSpace::Srgb, - premultiplied_alpha: false, - }, - wgpu::Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - ); + + if self.supports_external_texture_copies { + self.queue.copy_external_image_to_texture( + &wgpu::CopyExternalImageSourceInfo { + source: wgpu::ExternalImageSource::OffscreenCanvas(canvas.clone()), + origin: wgpu::Origin2d::ZERO, + flip_y: false, + }, + wgpu::CopyExternalImageDestInfo { + texture: &texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + color_space: wgpu::PredefinedColorSpace::Srgb, + premultiplied_alpha: false, + }, + wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + } else { + let ctx: web_sys::OffscreenCanvasRenderingContext2d = canvas + .get_context("2d") + .ok() + .flatten() + .expect("Failed to get 2d context for texture import") + .unchecked_into(); + let image_data = ctx + .get_image_data(0.0, 0.0, width as f64, height as f64) + .expect("Failed to read pixel data from canvas"); + let rgba_bytes = image_data.data(); + + let pixel_bytes = if self.texture_format == wgpu::TextureFormat::Bgra8Unorm { + let mut bytes = rgba_bytes.to_vec(); + for pixel in bytes.chunks_exact_mut(4) { + pixel.swap(0, 2); + } + bytes + } else { + rgba_bytes.to_vec() + }; + + self.queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: &texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &pixel_bytes, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(width * 4), + rows_per_image: Some(height), + }, + wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + } + texture } @@ -329,9 +486,89 @@ impl GpuContext { width: u32, height: u32, ) -> Result<(), GpuError> { - let surface = self - .instance - .create_surface(wgpu::SurfaceTarget::OffscreenCanvas(canvas.clone()))?; - self.render_texture_to_surface(texture, &surface, width, height) + if self.supports_external_texture_copies { + let surface = self + .instance + .create_surface(wgpu::SurfaceTarget::OffscreenCanvas(canvas.clone()))?; + return self.render_texture_to_surface(texture, &surface, width, height); + } + + self.readback_texture_to_offscreen_canvas(texture, canvas, width, height) + } + + #[cfg(all(feature = "wasm", target_arch = "wasm32"))] + fn readback_texture_to_offscreen_canvas( + &self, + texture: &wgpu::Texture, + canvas: &wgpu::web_sys::OffscreenCanvas, + width: u32, + height: u32, + ) -> Result<(), GpuError> { + let buffer_size = (width * height * 4) as u64; + let buffer = self.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("gpu-readback-buffer"), + size: buffer_size, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, + mapped_at_creation: false, + }); + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("gpu-readback-encoder"), + }); + encoder.copy_texture_to_buffer( + wgpu::TexelCopyTextureInfo { + texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + wgpu::TexelCopyBufferInfo { + buffer: &buffer, + layout: wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(width * 4), + rows_per_image: Some(height), + }, + }, + wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + self.queue.submit([encoder.finish()]); + + let slice = buffer.slice(..); + slice.map_async(wgpu::MapMode::Read, |_| {}); + let _ = self.device.poll(wgpu::PollType::wait_indefinitely()); + + let data = slice.get_mapped_range(); + let mut rgba_bytes = data.to_vec(); + drop(data); + buffer.unmap(); + + if self.texture_format == wgpu::TextureFormat::Bgra8Unorm { + for pixel in rgba_bytes.chunks_exact_mut(4) { + pixel.swap(0, 2); + } + } + + let clamped = wasm_bindgen::Clamped(&rgba_bytes[..]); + let image_data = + web_sys::ImageData::new_with_u8_clamped_array_and_sh(clamped, width, height) + .map_err(|_| GpuError::AdapterUnavailable)?; + + let ctx: web_sys::OffscreenCanvasRenderingContext2d = canvas + .get_context("2d") + .ok() + .flatten() + .ok_or(GpuError::AdapterUnavailable)? + .unchecked_into(); + ctx.put_image_data(&image_data, 0.0, 0.0) + .map_err(|_| GpuError::AdapterUnavailable)?; + + Ok(()) } } diff --git a/rust/crates/gpu/src/shaders/fullscreen.wgsl b/rust/crates/gpu/src/shaders/fullscreen.wgsl index 1ef26e48..dce878e4 100644 --- a/rust/crates/gpu/src/shaders/fullscreen.wgsl +++ b/rust/crates/gpu/src/shaders/fullscreen.wgsl @@ -7,6 +7,6 @@ struct VertexOutput { fn vertex_main(@location(0) position: vec2f) -> VertexOutput { var output: VertexOutput; output.position = vec4f(position, 0.0, 1.0); - output.tex_coord = (position * 0.5) + vec2f(0.5, 0.5); + output.tex_coord = vec2f(position.x * 0.5 + 0.5, 0.5 - position.y * 0.5); return output; } diff --git a/rust/crates/masks/src/feather.rs b/rust/crates/masks/src/feather.rs index e58572f4..5ae71b6f 100644 --- a/rust/crates/masks/src/feather.rs +++ b/rust/crates/masks/src/feather.rs @@ -1,5 +1,5 @@ use bytemuck::{Pod, Zeroable}; -use gpu::{FULLSCREEN_SHADER_SOURCE, GPU_TEXTURE_FORMAT, GpuContext}; +use gpu::{FULLSCREEN_SHADER_SOURCE, GpuContext}; use wgpu::util::DeviceExt; use crate::SdfPipeline; @@ -128,7 +128,7 @@ impl MaskFeatherPipeline { module: &fragment_shader_module, entry_point: Some("fragment_main"), targets: &[Some(wgpu::ColorTargetState { - format: GPU_TEXTURE_FORMAT, + format: context.texture_format(), blend: None, write_mask: wgpu::ColorWrites::ALL, })], @@ -159,10 +159,41 @@ impl MaskFeatherPipeline { height, feather, }: ApplyMaskFeatherOptions<'_>, + ) -> wgpu::Texture { + let mut encoder = + context + .device() + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("gpu-mask-distance-command-encoder"), + }); + let output = self.apply_mask_feather_with_encoder( + context, + &mut encoder, + ApplyMaskFeatherOptions { + mask, + width, + height, + feather, + }, + ); + context.queue().submit([encoder.finish()]); + output + } + + pub fn apply_mask_feather_with_encoder( + &self, + context: &GpuContext, + encoder: &mut wgpu::CommandEncoder, + ApplyMaskFeatherOptions { + mask, + width, + height, + feather, + }: ApplyMaskFeatherOptions<'_>, ) -> wgpu::Texture { let sdf = self .sdf_pipeline - .compute_signed_distance_field(context, mask, width, height); + .compute_signed_distance_field_with_encoder(context, encoder, mask, width, height); let output_texture = context.create_render_texture(width, height, "masks-feather-output"); let inside_view = sdf .inside_texture @@ -225,13 +256,6 @@ impl MaskFeatherPipeline { resource: uniform_buffer.as_entire_binding(), }], }); - let mut encoder = - context - .device() - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("gpu-mask-distance-command-encoder"), - }); - { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("gpu-mask-distance-render-pass"), @@ -256,8 +280,6 @@ impl MaskFeatherPipeline { render_pass.set_bind_group(2, &uniform_bind_group, &[]); render_pass.draw(0..6, 0..1); } - - context.queue().submit([encoder.finish()]); output_texture } } diff --git a/rust/crates/masks/src/sdf.rs b/rust/crates/masks/src/sdf.rs index 0e7ec01a..f0d93212 100644 --- a/rust/crates/masks/src/sdf.rs +++ b/rust/crates/masks/src/sdf.rs @@ -1,5 +1,5 @@ use bytemuck::{Pod, Zeroable}; -use gpu::{FULLSCREEN_SHADER_SOURCE, GPU_TEXTURE_FORMAT, GpuContext}; +use gpu::{FULLSCREEN_SHADER_SOURCE, GpuContext}; use wgpu::util::DeviceExt; const JFA_INIT_SHADER_SOURCE: &str = include_str!("shaders/jfa_init.wgsl"); @@ -98,6 +98,7 @@ impl SdfPipeline { &pipeline_layout, &vertex_shader_module, &init_shader_module, + context.texture_format(), ); let step_pipeline = create_pipeline( device, @@ -105,6 +106,7 @@ impl SdfPipeline { &pipeline_layout, &vertex_shader_module, &step_shader_module, + context.texture_format(), ); Self { @@ -121,16 +123,42 @@ impl SdfPipeline { source_texture: &wgpu::Texture, width: u32, height: u32, + ) -> SignedDistanceFieldTextures { + let mut encoder = + context + .device() + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("gpu-sdf-command-encoder"), + }); + let textures = self.compute_signed_distance_field_with_encoder( + context, + &mut encoder, + source_texture, + width, + height, + ); + context.queue().submit([encoder.finish()]); + textures + } + + pub fn compute_signed_distance_field_with_encoder( + &self, + context: &GpuContext, + encoder: &mut wgpu::CommandEncoder, + source_texture: &wgpu::Texture, + width: u32, + height: u32, ) -> SignedDistanceFieldTextures { SignedDistanceFieldTextures { - inside_texture: self.run_jfa(context, source_texture, width, height, false), - outside_texture: self.run_jfa(context, source_texture, width, height, true), + inside_texture: self.run_jfa(context, encoder, source_texture, width, height, false), + outside_texture: self.run_jfa(context, encoder, source_texture, width, height, true), } } fn run_jfa( &self, context: &GpuContext, + encoder: &mut wgpu::CommandEncoder, source_texture: &wgpu::Texture, width: u32, height: u32, @@ -141,6 +169,7 @@ impl SdfPipeline { self.run_pass( context, + encoder, source_texture, &ping_texture, &self.init_pipeline, @@ -167,6 +196,7 @@ impl SdfPipeline { }; self.run_pass( context, + encoder, input_texture, output_texture, &self.step_pipeline, @@ -189,6 +219,7 @@ impl SdfPipeline { fn run_pass( &self, context: &GpuContext, + encoder: &mut wgpu::CommandEncoder, input_texture: &wgpu::Texture, output_texture: &wgpu::Texture, pipeline: &wgpu::RenderPipeline, @@ -230,12 +261,6 @@ impl SdfPipeline { resource: uniform_buffer.as_entire_binding(), }], }); - let mut encoder = - context - .device() - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("gpu-sdf-command-encoder"), - }); { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { @@ -260,8 +285,6 @@ impl SdfPipeline { render_pass.set_bind_group(1, &uniform_bind_group, &[]); render_pass.draw(0..6, 0..1); } - - context.queue().submit([encoder.finish()]); } } @@ -271,6 +294,7 @@ fn create_pipeline( layout: &wgpu::PipelineLayout, vertex_shader_module: &wgpu::ShaderModule, fragment_shader_module: &wgpu::ShaderModule, + texture_format: wgpu::TextureFormat, ) -> wgpu::RenderPipeline { device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some(label), @@ -293,7 +317,7 @@ fn create_pipeline( module: fragment_shader_module, entry_point: Some("fragment_main"), targets: &[Some(wgpu::ColorTargetState { - format: GPU_TEXTURE_FORMAT, + format: texture_format, blend: None, write_mask: wgpu::ColorWrites::ALL, })], diff --git a/rust/crates/masks/src/shaders/jfa_init.wgsl b/rust/crates/masks/src/shaders/jfa_init.wgsl index 29aa38c7..e224db3d 100644 --- a/rust/crates/masks/src/shaders/jfa_init.wgsl +++ b/rust/crates/masks/src/shaders/jfa_init.wgsl @@ -24,7 +24,10 @@ fn encode_seed(seed: vec2f) -> vec4f { @fragment fn fragment_main(input: VertexOutput) -> @location(0) vec4f { let mask = textureSample(input_texture, input_sampler, input.tex_coord).r; - let is_seed = select(mask > 0.5, mask < 0.5, uniforms.invert > 0.5); + let above = mask > 0.5; + let below = mask < 0.5; + let inverted = uniforms.invert > 0.5; + let is_seed = select(above, below, inverted); if (is_seed) { let pixel_coord = floor(input.tex_coord * uniforms.resolution); diff --git a/rust/crates/masks/src/shaders/jfa_step.wgsl b/rust/crates/masks/src/shaders/jfa_step.wgsl index e20d7fb0..bf348217 100644 --- a/rust/crates/masks/src/shaders/jfa_step.wgsl +++ b/rust/crates/masks/src/shaders/jfa_step.wgsl @@ -53,7 +53,7 @@ fn fragment_main(input: VertexOutput) -> @location(0) vec4f { continue; } - let encoded = textureSample(input_texture, input_sampler, sample_uv); + let encoded = textureSampleLevel(input_texture, input_sampler, sample_uv, 0.0); if (is_no_seed(encoded)) { continue; }