summaryrefslogtreecommitdiff
path: root/rust
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-13 04:40:19 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-13 05:01:51 +0200
commitdbd68ff1396bf498eb97f095810b78d2bddd0011 (patch)
treef1d555425899ff1ecb969a94dc1ea5d9b84868f7 /rust
parent0d0d7375635733435922a697d91601206cc38b68 (diff)
downloadopencut-dbd68ff1396bf498eb97f095810b78d2bddd0011.tar.gz
refactor: gpu webgl fallback and batched command encoding
Made-with: Cursor
Diffstat (limited to 'rust')
-rw-r--r--rust/crates/effects/src/pipeline.rs42
-rw-r--r--rust/crates/gpu/Cargo.toml6
-rw-r--r--rust/crates/gpu/src/context.rs413
-rw-r--r--rust/crates/gpu/src/shaders/fullscreen.wgsl2
-rw-r--r--rust/crates/masks/src/feather.rs46
-rw-r--r--rust/crates/masks/src/sdf.rs48
-rw-r--r--rust/crates/masks/src/shaders/jfa_init.wgsl5
-rw-r--r--rust/crates/masks/src/shaders/jfa_step.wgsl2
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,
})],
@@ -150,6 +150,37 @@ impl EffectPipeline {
passes,
}: ApplyEffectsOptions<'_>,
) -> Result<wgpu::Texture, EffectsError> {
+ 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<wgpu::Texture, EffectsError> {
let mut current_texture: Option<wgpu::Texture> = None;
for pass in passes {
@@ -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::DisplayHandle<'_>, 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<Self, GpuError> {
- 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,6 +298,10 @@ 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,
@@ -218,29 +309,61 @@ impl GpuContext {
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<wgpu::SurfaceTexture, GpuError> {
+ 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,
})],
@@ -160,9 +160,40 @@ impl MaskFeatherPipeline {
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 {
@@ -122,15 +124,41 @@ impl SdfPipeline {
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;
}