diff options
Diffstat (limited to 'rust')
| -rw-r--r-- | rust/crates/gpu/Cargo.toml | 9 | ||||
| -rw-r--r-- | rust/crates/gpu/src/context.rs | 250 | ||||
| -rw-r--r-- | rust/crates/gpu/src/effect_pipeline.rs | 194 | ||||
| -rw-r--r-- | rust/crates/gpu/src/lib.rs | 40 | ||||
| -rw-r--r-- | rust/crates/gpu/src/mask_feather.rs | 259 | ||||
| -rw-r--r-- | rust/crates/gpu/src/sdf_pipeline.rs | 307 | ||||
| -rw-r--r-- | rust/crates/gpu/src/shader_registry.rs | 183 | ||||
| -rw-r--r-- | rust/crates/gpu/src/shaders/blit.wgsl | 12 | ||||
| -rw-r--r-- | rust/crates/gpu/src/shaders/fullscreen.wgsl | 12 | ||||
| -rw-r--r-- | rust/crates/gpu/src/shaders/gaussian_blur.wgsl | 34 | ||||
| -rw-r--r-- | rust/crates/gpu/src/shaders/jfa_distance.wgsl | 50 | ||||
| -rw-r--r-- | rust/crates/gpu/src/shaders/jfa_init.wgsl | 35 | ||||
| -rw-r--r-- | rust/crates/gpu/src/shaders/jfa_step.wgsl | 75 | ||||
| -rw-r--r-- | rust/wasm/Cargo.toml | 7 | ||||
| -rw-r--r-- | rust/wasm/src/gpu_bridge.rs | 256 | ||||
| -rw-r--r-- | rust/wasm/src/wasm.rs | 5 |
16 files changed, 1728 insertions, 0 deletions
diff --git a/rust/crates/gpu/Cargo.toml b/rust/crates/gpu/Cargo.toml new file mode 100644 index 00000000..d68e8a65 --- /dev/null +++ b/rust/crates/gpu/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "gpu" +version = "0.1.0" +edition = "2024" + +[dependencies] +bytemuck = { version = "1.25.0", features = ["derive"] } +thiserror = "2.0.18" +wgpu = "29.0.1" diff --git a/rust/crates/gpu/src/context.rs b/rust/crates/gpu/src/context.rs new file mode 100644 index 00000000..702f65ba --- /dev/null +++ b/rust/crates/gpu/src/context.rs @@ -0,0 +1,250 @@ +use wgpu::util::DeviceExt; + +use crate::{ + GPU_TEXTURE_FORMAT, GpuError, + effect_pipeline::{ApplyEffectsOptions, apply_effects}, + mask_feather::{ApplyMaskFeatherOptions, MaskFeatherPipeline}, + sdf_pipeline::SdfPipeline, + shader_registry::ShaderRegistry, +}; + +const FULLSCREEN_QUAD_POSITIONS: [[f32; 2]; 6] = [ + [-1.0, -1.0], + [1.0, -1.0], + [-1.0, 1.0], + [-1.0, 1.0], + [1.0, -1.0], + [1.0, 1.0], +]; + +pub struct GpuContext { + instance: wgpu::Instance, + adapter: wgpu::Adapter, + device: wgpu::Device, + queue: wgpu::Queue, + fullscreen_quad: wgpu::Buffer, + linear_sampler: wgpu::Sampler, + nearest_sampler: wgpu::Sampler, + shader_registry: ShaderRegistry, + sdf_pipeline: SdfPipeline, + mask_feather_pipeline: MaskFeatherPipeline, +} + +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 fullscreen_quad = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("gpu-fullscreen-quad-buffer"), + contents: bytemuck::cast_slice(&FULLSCREEN_QUAD_POSITIONS), + usage: wgpu::BufferUsages::VERTEX, + }); + let linear_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("gpu-linear-sampler"), + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::MipmapFilterMode::Nearest, + ..Default::default() + }); + let nearest_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("gpu-nearest-sampler"), + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Nearest, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::MipmapFilterMode::Nearest, + ..Default::default() + }); + let shader_registry = ShaderRegistry::new(&device); + let sdf_pipeline = SdfPipeline::new(&device); + let mask_feather_pipeline = MaskFeatherPipeline::new(&device); + + Ok(Self { + instance, + adapter, + device, + queue, + fullscreen_quad, + linear_sampler, + nearest_sampler, + shader_registry, + sdf_pipeline, + mask_feather_pipeline, + }) + } + + pub fn apply_effects( + &self, + options: ApplyEffectsOptions<'_>, + ) -> Result<wgpu::Texture, GpuError> { + apply_effects(self, options) + } + + pub fn apply_mask_feather( + &self, + options: ApplyMaskFeatherOptions<'_>, + ) -> wgpu::Texture { + self.mask_feather_pipeline.apply_mask_feather(self, options) + } + + pub fn create_render_texture( + &self, + width: u32, + height: u32, + label: &'static str, + ) -> wgpu::Texture { + self.device.create_texture(&wgpu::TextureDescriptor { + label: Some(label), + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: GPU_TEXTURE_FORMAT, + usage: wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }) + } + + pub fn instance(&self) -> &wgpu::Instance { + &self.instance + } + + pub fn adapter(&self) -> &wgpu::Adapter { + &self.adapter + } + + pub fn device(&self) -> &wgpu::Device { + &self.device + } + + pub fn queue(&self) -> &wgpu::Queue { + &self.queue + } + + pub fn fullscreen_quad(&self) -> &wgpu::Buffer { + &self.fullscreen_quad + } + + pub fn linear_sampler(&self) -> &wgpu::Sampler { + &self.linear_sampler + } + + pub fn nearest_sampler(&self) -> &wgpu::Sampler { + &self.nearest_sampler + } + + pub fn shader_registry(&self) -> &ShaderRegistry { + &self.shader_registry + } + + pub fn sdf_pipeline(&self) -> &SdfPipeline { + &self.sdf_pipeline + } + + pub fn render_texture_to_surface( + &self, + texture: &wgpu::Texture, + 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 { + return Err(GpuError::UnsupportedSurfaceFormat); + } + surface.configure(&self.device, &config); + + let surface_texture = match surface.get_current_texture() { + wgpu::CurrentSurfaceTexture::Success(surface_texture) + | wgpu::CurrentSurfaceTexture::Suboptimal(surface_texture) => surface_texture, + wgpu::CurrentSurfaceTexture::Timeout + | wgpu::CurrentSurfaceTexture::Occluded + | wgpu::CurrentSurfaceTexture::Outdated + | wgpu::CurrentSurfaceTexture::Lost + | wgpu::CurrentSurfaceTexture::Validation => { + return Err(GpuError::UnsupportedSurfaceFormat); + } + }; + 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.shader_registry.effect_texture_bind_group_layout(), + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&source_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&self.linear_sampler), + }, + ], + }); + 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.shader_registry.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(()) + } +} diff --git a/rust/crates/gpu/src/effect_pipeline.rs b/rust/crates/gpu/src/effect_pipeline.rs new file mode 100644 index 00000000..c600551a --- /dev/null +++ b/rust/crates/gpu/src/effect_pipeline.rs @@ -0,0 +1,194 @@ +use std::collections::HashMap; + +use bytemuck::{Pod, Zeroable}; +use wgpu::util::DeviceExt; + +use crate::{GpuError, context::GpuContext}; + +pub struct ApplyEffectsOptions<'a> { + pub source: &'a wgpu::Texture, + pub width: u32, + pub height: u32, + pub passes: &'a [EffectPass], +} + +#[derive(Clone, Debug)] +pub struct EffectPass { + pub shader: String, + pub uniforms: HashMap<String, UniformValue>, +} + +#[derive(Clone, Debug)] +pub enum UniformValue { + Number(f32), + Vector(Vec<f32>), +} + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct EffectUniformBuffer { + resolution: [f32; 2], + direction: [f32; 2], + scalars: [f32; 4], +} + +pub fn apply_effects( + context: &GpuContext, + ApplyEffectsOptions { + source, + width, + height, + passes, + }: ApplyEffectsOptions<'_>, +) -> Result<wgpu::Texture, GpuError> { + let mut current_texture: Option<wgpu::Texture> = None; + + for pass in passes { + let input_texture = current_texture.as_ref().unwrap_or(source); + let output_texture = + context.create_render_texture(width, height, "gpu-effect-pass-output"); + let input_view = input_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let texture_bind_group = context + .device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("gpu-effect-texture-bind-group"), + layout: context + .shader_registry() + .effect_texture_bind_group_layout(), + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&input_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(context.linear_sampler()), + }, + ], + }); + let uniform_buffer = context + .device() + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("gpu-effect-uniform-buffer"), + contents: bytemuck::bytes_of(&pack_effect_uniforms(pass, width, height)?), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + let uniform_bind_group = context + .device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("gpu-effect-uniform-bind-group"), + layout: context + .shader_registry() + .effect_uniform_bind_group_layout(), + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: uniform_buffer.as_entire_binding(), + }], + }); + let pipeline = context.shader_registry().get_effect_pipeline(&pass.shader)?; + let mut encoder = context + .device() + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("gpu-effect-command-encoder"), + }); + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("gpu-effect-render-pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &output_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(pipeline); + render_pass.set_vertex_buffer(0, context.fullscreen_quad().slice(..)); + render_pass.set_bind_group(0, &texture_bind_group, &[]); + render_pass.set_bind_group(1, &uniform_bind_group, &[]); + render_pass.draw(0..6, 0..1); + } + + context.queue().submit([encoder.finish()]); + current_texture = Some(output_texture); + } + + current_texture.ok_or_else(|| GpuError::UnknownEffectShader { + shader: "missing-effect-pass".to_string(), + }) +} + +fn pack_effect_uniforms( + pass: &EffectPass, + width: u32, + height: u32, +) -> Result<EffectUniformBuffer, GpuError> { + let shader = pass.shader.as_str(); + let sigma = read_number_uniform(pass, "u_sigma")?; + let step = read_number_uniform(pass, "u_step")?; + let direction = read_vec2_uniform(pass, "u_direction")?; + + for uniform in pass.uniforms.keys() { + if uniform == "u_sigma" || uniform == "u_step" || uniform == "u_direction" { + continue; + } + return Err(GpuError::UnsupportedUniform { + shader: shader.to_string(), + uniform: uniform.clone(), + }); + } + + Ok(EffectUniformBuffer { + resolution: [width as f32, height as f32], + direction, + scalars: [sigma, step, 0.0, 0.0], + }) +} + +fn read_number_uniform(pass: &EffectPass, uniform: &str) -> Result<f32, GpuError> { + let Some(value) = pass.uniforms.get(uniform) else { + return Err(GpuError::MissingUniform { + shader: pass.shader.clone(), + uniform: uniform.to_string(), + }); + }; + match value { + UniformValue::Number(value) => Ok(*value), + UniformValue::Vector(_) => Err(GpuError::InvalidNumberUniform { + shader: pass.shader.clone(), + uniform: uniform.to_string(), + }), + } +} + +fn read_vec2_uniform(pass: &EffectPass, uniform: &str) -> Result<[f32; 2], GpuError> { + let Some(value) = pass.uniforms.get(uniform) else { + return Err(GpuError::MissingUniform { + shader: pass.shader.clone(), + uniform: uniform.to_string(), + }); + }; + let UniformValue::Vector(values) = value else { + return Err(GpuError::InvalidVectorUniform { + shader: pass.shader.clone(), + uniform: uniform.to_string(), + expected_length: 2, + }); + }; + if values.len() != 2 { + return Err(GpuError::InvalidVectorUniform { + shader: pass.shader.clone(), + uniform: uniform.to_string(), + expected_length: 2, + }); + } + Ok([values[0], values[1]]) +} diff --git a/rust/crates/gpu/src/lib.rs b/rust/crates/gpu/src/lib.rs new file mode 100644 index 00000000..c53e63ab --- /dev/null +++ b/rust/crates/gpu/src/lib.rs @@ -0,0 +1,40 @@ +mod context; +mod effect_pipeline; +mod mask_feather; +mod sdf_pipeline; +mod shader_registry; + +use thiserror::Error; + +pub use wgpu; +pub use context::GpuContext; +pub use effect_pipeline::{ApplyEffectsOptions, EffectPass, UniformValue}; +pub use mask_feather::ApplyMaskFeatherOptions; + +pub const GPU_TEXTURE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8Unorm; + +#[derive(Debug, Error)] +pub enum GpuError { + #[error("No WebGPU adapter is available")] + AdapterUnavailable, + #[error("Failed to request a WebGPU device: {0}")] + RequestDevice(#[from] wgpu::RequestDeviceError), + #[error("Failed to create a WebGPU surface: {0}")] + CreateSurface(#[from] wgpu::CreateSurfaceError), + #[error("The output surface does not support the required texture format")] + UnsupportedSurfaceFormat, + #[error("Unknown effect shader '{shader}'")] + UnknownEffectShader { shader: String }, + #[error("Missing uniform '{uniform}' for shader '{shader}'")] + MissingUniform { shader: String, uniform: String }, + #[error("Uniform '{uniform}' for shader '{shader}' must be a number")] + InvalidNumberUniform { shader: String, uniform: String }, + #[error("Uniform '{uniform}' for shader '{shader}' must be a vector of length {expected_length}")] + InvalidVectorUniform { + shader: String, + uniform: String, + expected_length: usize, + }, + #[error("Shader '{shader}' does not support uniform '{uniform}'")] + UnsupportedUniform { shader: String, uniform: String }, +} diff --git a/rust/crates/gpu/src/mask_feather.rs b/rust/crates/gpu/src/mask_feather.rs new file mode 100644 index 00000000..029b59ed --- /dev/null +++ b/rust/crates/gpu/src/mask_feather.rs @@ -0,0 +1,259 @@ +use bytemuck::{Pod, Zeroable}; +use wgpu::util::DeviceExt; + +use crate::{GPU_TEXTURE_FORMAT, context::GpuContext}; + +const FULLSCREEN_SHADER_SOURCE: &str = include_str!("shaders/fullscreen.wgsl"); +const JFA_DISTANCE_SHADER_SOURCE: &str = include_str!("shaders/jfa_distance.wgsl"); + +pub struct ApplyMaskFeatherOptions<'a> { + pub mask: &'a wgpu::Texture, + pub width: u32, + pub height: u32, + pub feather: f32, +} + +pub struct MaskFeatherPipeline { + inside_texture_bind_group_layout: wgpu::BindGroupLayout, + outside_texture_bind_group_layout: wgpu::BindGroupLayout, + uniform_bind_group_layout: wgpu::BindGroupLayout, + distance_pipeline: wgpu::RenderPipeline, +} + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct DistanceUniformBuffer { + resolution: [f32; 2], + feather_half: f32, + _padding: f32, +} + +impl MaskFeatherPipeline { + pub fn new(device: &wgpu::Device) -> Self { + let inside_texture_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("gpu-mask-distance-inside-layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + let outside_texture_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("gpu-mask-distance-outside-layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + let uniform_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("gpu-mask-distance-uniform-layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("gpu-mask-distance-pipeline-layout"), + bind_group_layouts: &[ + Some(&inside_texture_bind_group_layout), + Some(&outside_texture_bind_group_layout), + Some(&uniform_bind_group_layout), + ], + immediate_size: 0, + }); + let vertex_shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("gpu-mask-distance-fullscreen-shader"), + source: wgpu::ShaderSource::Wgsl(FULLSCREEN_SHADER_SOURCE.into()), + }); + let fragment_shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("gpu-mask-distance-fragment-shader"), + source: wgpu::ShaderSource::Wgsl(JFA_DISTANCE_SHADER_SOURCE.into()), + }); + let distance_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("gpu-mask-distance-pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &vertex_shader_module, + entry_point: Some("vertex_main"), + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::<[f32; 2]>() as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[wgpu::VertexAttribute { + format: wgpu::VertexFormat::Float32x2, + offset: 0, + shader_location: 0, + }], + }], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fragment_shader_module, + entry_point: Some("fragment_main"), + targets: &[Some(wgpu::ColorTargetState { + format: GPU_TEXTURE_FORMAT, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }); + + Self { + inside_texture_bind_group_layout, + outside_texture_bind_group_layout, + uniform_bind_group_layout, + distance_pipeline, + } + } + + pub fn apply_mask_feather( + &self, + context: &GpuContext, + ApplyMaskFeatherOptions { + mask, + width, + height, + feather, + }: ApplyMaskFeatherOptions<'_>, + ) -> wgpu::Texture { + let sdf = context + .sdf_pipeline() + .compute_signed_distance_field(context, mask, width, height); + let output_texture = + context.create_render_texture(width, height, "gpu-mask-feather-output"); + let inside_view = sdf + .inside_texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let outside_view = sdf + .outside_texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let inside_bind_group = context + .device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("gpu-mask-distance-inside-bind-group"), + layout: &self.inside_texture_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&inside_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(context.nearest_sampler()), + }, + ], + }); + let outside_bind_group = context + .device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("gpu-mask-distance-outside-bind-group"), + layout: &self.outside_texture_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&outside_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(context.nearest_sampler()), + }, + ], + }); + let uniform_buffer = context + .device() + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("gpu-mask-distance-uniform-buffer"), + contents: bytemuck::bytes_of(&DistanceUniformBuffer { + resolution: [width as f32, height as f32], + feather_half: feather / 2.0, + _padding: 0.0, + }), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + let uniform_bind_group = context + .device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("gpu-mask-distance-uniform-bind-group"), + layout: &self.uniform_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + 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"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &output_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.distance_pipeline); + render_pass.set_vertex_buffer(0, context.fullscreen_quad().slice(..)); + render_pass.set_bind_group(0, &inside_bind_group, &[]); + render_pass.set_bind_group(1, &outside_bind_group, &[]); + 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/gpu/src/sdf_pipeline.rs b/rust/crates/gpu/src/sdf_pipeline.rs new file mode 100644 index 00000000..d2612fe1 --- /dev/null +++ b/rust/crates/gpu/src/sdf_pipeline.rs @@ -0,0 +1,307 @@ +use bytemuck::{Pod, Zeroable}; +use wgpu::util::DeviceExt; + +use crate::{GPU_TEXTURE_FORMAT, context::GpuContext}; + +const FULLSCREEN_SHADER_SOURCE: &str = include_str!("shaders/fullscreen.wgsl"); +const JFA_INIT_SHADER_SOURCE: &str = include_str!("shaders/jfa_init.wgsl"); +const JFA_STEP_SHADER_SOURCE: &str = include_str!("shaders/jfa_step.wgsl"); + +pub struct SignedDistanceFieldTextures { + pub inside_texture: wgpu::Texture, + pub outside_texture: wgpu::Texture, +} + +pub struct SdfPipeline { + texture_bind_group_layout: wgpu::BindGroupLayout, + uniform_bind_group_layout: wgpu::BindGroupLayout, + init_pipeline: wgpu::RenderPipeline, + step_pipeline: wgpu::RenderPipeline, +} + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct JfaInitUniformBuffer { + resolution: [f32; 2], + invert: f32, + _padding: f32, +} + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct JfaStepUniformBuffer { + resolution: [f32; 2], + step_size: f32, + _padding: f32, +} + +impl SdfPipeline { + pub fn new(device: &wgpu::Device) -> Self { + let texture_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("gpu-sdf-texture-bind-group-layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + let uniform_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("gpu-sdf-uniform-bind-group-layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("gpu-sdf-pipeline-layout"), + bind_group_layouts: &[ + Some(&texture_bind_group_layout), + Some(&uniform_bind_group_layout), + ], + immediate_size: 0, + }); + let vertex_shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("gpu-sdf-fullscreen-shader"), + source: wgpu::ShaderSource::Wgsl(FULLSCREEN_SHADER_SOURCE.into()), + }); + let init_shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("gpu-jfa-init-shader"), + source: wgpu::ShaderSource::Wgsl(JFA_INIT_SHADER_SOURCE.into()), + }); + let step_shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("gpu-jfa-step-shader"), + source: wgpu::ShaderSource::Wgsl(JFA_STEP_SHADER_SOURCE.into()), + }); + let init_pipeline = create_pipeline( + device, + "gpu-jfa-init-pipeline", + &pipeline_layout, + &vertex_shader_module, + &init_shader_module, + ); + let step_pipeline = create_pipeline( + device, + "gpu-jfa-step-pipeline", + &pipeline_layout, + &vertex_shader_module, + &step_shader_module, + ); + + Self { + texture_bind_group_layout, + uniform_bind_group_layout, + init_pipeline, + step_pipeline, + } + } + + pub fn compute_signed_distance_field( + &self, + context: &GpuContext, + 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), + } + } + + fn run_jfa( + &self, + context: &GpuContext, + source_texture: &wgpu::Texture, + width: u32, + height: u32, + is_inverted: bool, + ) -> wgpu::Texture { + let ping_texture = context.create_render_texture(width, height, "gpu-jfa-ping-texture"); + let pong_texture = context.create_render_texture(width, height, "gpu-jfa-pong-texture"); + + self.run_pass( + context, + source_texture, + &ping_texture, + &self.init_pipeline, + bytemuck::bytes_of(&JfaInitUniformBuffer { + resolution: [width as f32, height as f32], + invert: if is_inverted { 1.0 } else { 0.0 }, + _padding: 0.0, + }), + ); + + let mut source_is_ping = true; + let steps = (width.max(height) as f32).log2().ceil() as u32; + for step_index in (0..steps).rev() { + let step_size = 2u32.pow(step_index).max(1); + let input_texture = if source_is_ping { + &ping_texture + } else { + &pong_texture + }; + let output_texture = if source_is_ping { + &pong_texture + } else { + &ping_texture + }; + self.run_pass( + context, + input_texture, + output_texture, + &self.step_pipeline, + bytemuck::bytes_of(&JfaStepUniformBuffer { + resolution: [width as f32, height as f32], + step_size: step_size as f32, + _padding: 0.0, + }), + ); + source_is_ping = !source_is_ping; + } + + if source_is_ping { + ping_texture + } else { + pong_texture + } + } + + fn run_pass( + &self, + context: &GpuContext, + input_texture: &wgpu::Texture, + output_texture: &wgpu::Texture, + pipeline: &wgpu::RenderPipeline, + uniform_buffer_bytes: &[u8], + ) { + let input_view = input_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let texture_bind_group = context + .device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("gpu-sdf-texture-bind-group"), + layout: &self.texture_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&input_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(context.nearest_sampler()), + }, + ], + }); + let uniform_buffer = context + .device() + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("gpu-sdf-uniform-buffer"), + contents: uniform_buffer_bytes, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + let uniform_bind_group = context + .device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("gpu-sdf-uniform-bind-group"), + layout: &self.uniform_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + 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 { + label: Some("gpu-sdf-render-pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &output_view, + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::WHITE), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + render_pass.set_pipeline(pipeline); + render_pass.set_vertex_buffer(0, context.fullscreen_quad().slice(..)); + render_pass.set_bind_group(0, &texture_bind_group, &[]); + render_pass.set_bind_group(1, &uniform_bind_group, &[]); + render_pass.draw(0..6, 0..1); + } + + context.queue().submit([encoder.finish()]); + } +} + +fn create_pipeline( + device: &wgpu::Device, + label: &'static str, + layout: &wgpu::PipelineLayout, + vertex_shader_module: &wgpu::ShaderModule, + fragment_shader_module: &wgpu::ShaderModule, +) -> wgpu::RenderPipeline { + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some(label), + layout: Some(layout), + vertex: wgpu::VertexState { + module: vertex_shader_module, + entry_point: Some("vertex_main"), + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::<[f32; 2]>() as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[wgpu::VertexAttribute { + format: wgpu::VertexFormat::Float32x2, + offset: 0, + shader_location: 0, + }], + }], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: fragment_shader_module, + entry_point: Some("fragment_main"), + targets: &[Some(wgpu::ColorTargetState { + format: GPU_TEXTURE_FORMAT, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }) +} diff --git a/rust/crates/gpu/src/shader_registry.rs b/rust/crates/gpu/src/shader_registry.rs new file mode 100644 index 00000000..b6af173e --- /dev/null +++ b/rust/crates/gpu/src/shader_registry.rs @@ -0,0 +1,183 @@ +use std::collections::HashMap; + +use crate::{GPU_TEXTURE_FORMAT, GpuError}; + +const FULLSCREEN_SHADER_SOURCE: &str = include_str!("shaders/fullscreen.wgsl"); +const GAUSSIAN_BLUR_SHADER_SOURCE: &str = include_str!("shaders/gaussian_blur.wgsl"); +const BLIT_SHADER_SOURCE: &str = include_str!("shaders/blit.wgsl"); +pub const GAUSSIAN_BLUR_SHADER_ID: &str = "gaussian-blur"; + +pub struct ShaderRegistry { + effect_texture_bind_group_layout: wgpu::BindGroupLayout, + effect_uniform_bind_group_layout: wgpu::BindGroupLayout, + effect_pipelines: HashMap<String, wgpu::RenderPipeline>, + blit_pipeline: wgpu::RenderPipeline, +} + +impl ShaderRegistry { + pub fn new(device: &wgpu::Device) -> Self { + let effect_texture_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("gpu-effect-texture-bind-group-layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + let effect_uniform_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("gpu-effect-uniform-bind-group-layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + + let vertex_shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("gpu-fullscreen-shader"), + source: wgpu::ShaderSource::Wgsl(FULLSCREEN_SHADER_SOURCE.into()), + }); + let gaussian_blur_shader_module = + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("gpu-gaussian-blur-shader"), + source: wgpu::ShaderSource::Wgsl(GAUSSIAN_BLUR_SHADER_SOURCE.into()), + }); + let blit_shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("gpu-blit-shader"), + source: wgpu::ShaderSource::Wgsl(BLIT_SHADER_SOURCE.into()), + }); + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("gpu-effect-pipeline-layout"), + bind_group_layouts: &[ + Some(&effect_texture_bind_group_layout), + Some(&effect_uniform_bind_group_layout), + ], + immediate_size: 0, + }); + let blit_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("gpu-blit-pipeline-layout"), + bind_group_layouts: &[Some(&effect_texture_bind_group_layout)], + immediate_size: 0, + }); + let gaussian_blur_pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("gpu-gaussian-blur-pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &vertex_shader_module, + entry_point: Some("vertex_main"), + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::<[f32; 2]>() as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[wgpu::VertexAttribute { + format: wgpu::VertexFormat::Float32x2, + offset: 0, + shader_location: 0, + }], + }], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &gaussian_blur_shader_module, + entry_point: Some("fragment_main"), + targets: &[Some(wgpu::ColorTargetState { + format: GPU_TEXTURE_FORMAT, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }); + let blit_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("gpu-blit-pipeline"), + layout: Some(&blit_pipeline_layout), + vertex: wgpu::VertexState { + module: &vertex_shader_module, + entry_point: Some("vertex_main"), + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::<[f32; 2]>() as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[wgpu::VertexAttribute { + format: wgpu::VertexFormat::Float32x2, + offset: 0, + shader_location: 0, + }], + }], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &blit_shader_module, + entry_point: Some("fragment_main"), + targets: &[Some(wgpu::ColorTargetState { + format: GPU_TEXTURE_FORMAT, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }); + + let effect_pipelines = HashMap::from([( + GAUSSIAN_BLUR_SHADER_ID.to_string(), + gaussian_blur_pipeline, + )]); + + Self { + effect_texture_bind_group_layout, + effect_uniform_bind_group_layout, + effect_pipelines, + blit_pipeline, + } + } + + pub fn get_effect_pipeline(&self, shader: &str) -> Result<&wgpu::RenderPipeline, GpuError> { + self.effect_pipelines + .get(shader) + .ok_or_else(|| GpuError::UnknownEffectShader { + shader: shader.to_string(), + }) + } + + pub fn effect_texture_bind_group_layout(&self) -> &wgpu::BindGroupLayout { + &self.effect_texture_bind_group_layout + } + + pub fn effect_uniform_bind_group_layout(&self) -> &wgpu::BindGroupLayout { + &self.effect_uniform_bind_group_layout + } + + pub fn blit_pipeline(&self) -> &wgpu::RenderPipeline { + &self.blit_pipeline + } +} diff --git a/rust/crates/gpu/src/shaders/blit.wgsl b/rust/crates/gpu/src/shaders/blit.wgsl new file mode 100644 index 00000000..2be67288 --- /dev/null +++ b/rust/crates/gpu/src/shaders/blit.wgsl @@ -0,0 +1,12 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +@group(0) @binding(0) var input_texture: texture_2d<f32>; +@group(0) @binding(1) var input_sampler: sampler; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + return textureSample(input_texture, input_sampler, input.tex_coord); +} diff --git a/rust/crates/gpu/src/shaders/fullscreen.wgsl b/rust/crates/gpu/src/shaders/fullscreen.wgsl new file mode 100644 index 00000000..1ef26e48 --- /dev/null +++ b/rust/crates/gpu/src/shaders/fullscreen.wgsl @@ -0,0 +1,12 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +@vertex +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); + return output; +} diff --git a/rust/crates/gpu/src/shaders/gaussian_blur.wgsl b/rust/crates/gpu/src/shaders/gaussian_blur.wgsl new file mode 100644 index 00000000..6c8d311d --- /dev/null +++ b/rust/crates/gpu/src/shaders/gaussian_blur.wgsl @@ -0,0 +1,34 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct EffectUniforms { + resolution: vec2f, + direction: vec2f, + scalars: vec4f, +} + +@group(0) @binding(0) var input_texture: texture_2d<f32>; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var<uniform> uniforms: EffectUniforms; + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let texel_size = vec2f(1.0, 1.0) / uniforms.resolution; + let sigma = uniforms.scalars.x; + let step_size = uniforms.scalars.y; + + var color = vec4f(0.0, 0.0, 0.0, 0.0); + var total_weight = 0.0; + + for (var index = -30; index <= 30; index = index + 1) { + let position = f32(index) * step_size; + let weight = exp(-(position * position) / (2.0 * sigma * sigma)); + let sample_uv = input.tex_coord + (texel_size * uniforms.direction * position); + color = color + textureSample(input_texture, input_sampler, sample_uv) * weight; + total_weight = total_weight + weight; + } + + return color / total_weight; +} diff --git a/rust/crates/gpu/src/shaders/jfa_distance.wgsl b/rust/crates/gpu/src/shaders/jfa_distance.wgsl new file mode 100644 index 00000000..e65a7023 --- /dev/null +++ b/rust/crates/gpu/src/shaders/jfa_distance.wgsl @@ -0,0 +1,50 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct DistanceUniforms { + resolution: vec2f, + feather_half: f32, + _padding: f32, +} + +@group(0) @binding(0) var inside_texture: texture_2d<f32>; +@group(0) @binding(1) var inside_sampler: sampler; +@group(1) @binding(0) var outside_texture: texture_2d<f32>; +@group(1) @binding(1) var outside_sampler: sampler; +@group(2) @binding(0) var<uniform> uniforms: DistanceUniforms; + +fn decode_seed(encoded: vec4f) -> vec2f { + let x = floor(encoded.r * 255.0 + 0.5) * 256.0 + floor(encoded.g * 255.0 + 0.5); + let y = floor(encoded.b * 255.0 + 0.5) * 256.0 + floor(encoded.a * 255.0 + 0.5); + return vec2f(x, y); +} + +fn is_no_seed(encoded: vec4f) -> bool { + return encoded.r > 0.99 && encoded.g > 0.99 && encoded.b > 0.99 && encoded.a > 0.99; +} + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let pixel_coord = floor(input.tex_coord * uniforms.resolution); + let inside_encoded = textureSample(inside_texture, inside_sampler, input.tex_coord); + let outside_encoded = textureSample(outside_texture, outside_sampler, input.tex_coord); + + let has_inside = !is_no_seed(inside_encoded); + let has_outside = !is_no_seed(outside_encoded); + let distance_to_inside = select( + 100000.0, + distance(pixel_coord, decode_seed(inside_encoded)), + has_inside, + ); + let distance_to_outside = select( + 100000.0, + distance(pixel_coord, decode_seed(outside_encoded)), + has_outside, + ); + let signed_distance = distance_to_outside - distance_to_inside; + let alpha = smoothstep(-uniforms.feather_half, uniforms.feather_half, signed_distance); + + return vec4f(alpha, alpha, alpha, alpha); +} diff --git a/rust/crates/gpu/src/shaders/jfa_init.wgsl b/rust/crates/gpu/src/shaders/jfa_init.wgsl new file mode 100644 index 00000000..29aa38c7 --- /dev/null +++ b/rust/crates/gpu/src/shaders/jfa_init.wgsl @@ -0,0 +1,35 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct JfaInitUniforms { + resolution: vec2f, + invert: f32, + _padding: f32, +} + +@group(0) @binding(0) var input_texture: texture_2d<f32>; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var<uniform> uniforms: JfaInitUniforms; + +fn encode_seed(seed: vec2f) -> vec4f { + let x_hi = floor(seed.x / 256.0); + let x_lo = seed.x - (x_hi * 256.0); + let y_hi = floor(seed.y / 256.0); + let y_lo = seed.y - (y_hi * 256.0); + return vec4f(x_hi / 255.0, x_lo / 255.0, y_hi / 255.0, y_lo / 255.0); +} + +@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); + + if (is_seed) { + let pixel_coord = floor(input.tex_coord * uniforms.resolution); + return encode_seed(pixel_coord); + } + + return vec4f(1.0, 1.0, 1.0, 1.0); +} diff --git a/rust/crates/gpu/src/shaders/jfa_step.wgsl b/rust/crates/gpu/src/shaders/jfa_step.wgsl new file mode 100644 index 00000000..e20d7fb0 --- /dev/null +++ b/rust/crates/gpu/src/shaders/jfa_step.wgsl @@ -0,0 +1,75 @@ +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, +} + +struct JfaStepUniforms { + resolution: vec2f, + step_size: f32, + _padding: f32, +} + +@group(0) @binding(0) var input_texture: texture_2d<f32>; +@group(0) @binding(1) var input_sampler: sampler; +@group(1) @binding(0) var<uniform> uniforms: JfaStepUniforms; + +fn decode_seed(encoded: vec4f) -> vec2f { + let x = floor(encoded.r * 255.0 + 0.5) * 256.0 + floor(encoded.g * 255.0 + 0.5); + let y = floor(encoded.b * 255.0 + 0.5) * 256.0 + floor(encoded.a * 255.0 + 0.5); + return vec2f(x, y); +} + +fn encode_seed(seed: vec2f) -> vec4f { + let x_hi = floor(seed.x / 256.0); + let x_lo = seed.x - (x_hi * 256.0); + let y_hi = floor(seed.y / 256.0); + let y_lo = seed.y - (y_hi * 256.0); + return vec4f(x_hi / 255.0, x_lo / 255.0, y_hi / 255.0, y_lo / 255.0); +} + +fn is_no_seed(encoded: vec4f) -> bool { + return encoded.r > 0.99 && encoded.g > 0.99 && encoded.b > 0.99 && encoded.a > 0.99; +} + +@fragment +fn fragment_main(input: VertexOutput) -> @location(0) vec4f { + let pixel_coord = floor(input.tex_coord * uniforms.resolution); + let texel_size = vec2f(1.0, 1.0) / uniforms.resolution; + + var best_distance = 10000000000.0; + var best_seed = vec2f(65535.0, 65535.0); + + for (var y = -1; y <= 1; y = y + 1) { + for (var x = -1; x <= 1; x = x + 1) { + let offset = vec2f(f32(x), f32(y)) * uniforms.step_size; + let sample_uv = input.tex_coord + (offset * texel_size); + + if ( + sample_uv.x < 0.0 || + sample_uv.x > 1.0 || + sample_uv.y < 0.0 || + sample_uv.y > 1.0 + ) { + continue; + } + + let encoded = textureSample(input_texture, input_sampler, sample_uv); + if (is_no_seed(encoded)) { + continue; + } + + let seed = decode_seed(encoded); + let distance_to_seed = distance(pixel_coord, seed); + if (distance_to_seed < best_distance) { + best_distance = distance_to_seed; + best_seed = seed; + } + } + } + + if (best_distance < 1000000000.0) { + return encode_seed(best_seed); + } + + return vec4f(1.0, 1.0, 1.0, 1.0); +} diff --git a/rust/wasm/Cargo.toml b/rust/wasm/Cargo.toml index 031437e5..06be926f 100644 --- a/rust/wasm/Cargo.toml +++ b/rust/wasm/Cargo.toml @@ -11,4 +11,11 @@ path = "src/wasm.rs" crate-type = ["cdylib", "rlib"] [dependencies] +gpu = { version = "0.1.0", path = "../crates/gpu" } +js-sys = "0.3.93" +serde = { version = "1.0.228", features = ["derive"] } +serde-wasm-bindgen = "0.6.5" time = { version = "0.1.0", path = "../crates/time", features = ["wasm"] } +wasm-bindgen = "0.2.116" +wasm-bindgen-futures = "0.4.66" +web-sys = { version = "0.3.93", features = ["OffscreenCanvas", "HtmlCanvasElement", "CanvasRenderingContext2d", "Document", "Window"] } diff --git a/rust/wasm/src/gpu_bridge.rs b/rust/wasm/src/gpu_bridge.rs new file mode 100644 index 00000000..85db0c8b --- /dev/null +++ b/rust/wasm/src/gpu_bridge.rs @@ -0,0 +1,256 @@ +#![cfg(target_arch = "wasm32")] + +use std::cell::RefCell; + +use gpu::{EffectPass, GpuContext, UniformValue, wgpu}; +use js_sys::{Object, Reflect}; +use serde::Deserialize; +use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen}; + +thread_local! { + static GPU_CONTEXT: RefCell<Option<GpuContext>> = const { RefCell::new(None) }; +} + +struct ApplyEffectPassesOptions { + source: wgpu::web_sys::OffscreenCanvas, + width: u32, + height: u32, + passes: Vec<EffectPassInput>, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct EffectPassInput { + shader: String, + uniforms: Vec<EffectUniformInput>, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct EffectUniformInput { + name: String, + value: Vec<f32>, +} + +struct ApplyMaskFeatherOptions { + mask: wgpu::web_sys::OffscreenCanvas, + width: u32, + height: u32, + feather: f32, +} + +#[wasm_bindgen(js_name = initializeGpu)] +pub async fn initialize_gpu() -> Result<(), JsValue> { + if GPU_CONTEXT.with(|context| context.borrow().is_some()) { + return Ok(()); + } + + let context = GpuContext::new() + .await + .map_err(|error| JsValue::from_str(&error.to_string()))?; + GPU_CONTEXT.with(|gpu_context| { + gpu_context.replace(Some(context)); + }); + Ok(()) +} + +#[wasm_bindgen(js_name = applyEffectPasses)] +pub fn apply_effect_passes( + options: JsValue, +) -> Result<wgpu::web_sys::OffscreenCanvas, JsValue> { + let ApplyEffectPassesOptions { + source, + width, + height, + passes, + } = parse_apply_effect_passes_options(options)?; + + with_gpu_context(|context| { + let source_texture = import_canvas_texture(context, &source, width, height)?; + let effect_passes = map_effect_passes(passes); + let result_texture = context + .apply_effects(gpu::ApplyEffectsOptions { + source: &source_texture, + width, + height, + passes: &effect_passes, + }) + .map_err(|error| JsValue::from_str(&error.to_string()))?; + let output_canvas = wgpu::web_sys::OffscreenCanvas::new(width, height)?; + let surface = context + .instance() + .create_surface(wgpu::SurfaceTarget::OffscreenCanvas(output_canvas.clone())) + .map_err(|error| JsValue::from_str(&error.to_string()))?; + + context + .render_texture_to_surface(&result_texture, &surface, width, height) + .map_err(|error| JsValue::from_str(&error.to_string()))?; + Ok(output_canvas) + }) +} + +#[wasm_bindgen(js_name = applyMaskFeather)] +pub fn apply_mask_feather( + options: JsValue, +) -> Result<wgpu::web_sys::OffscreenCanvas, JsValue> { + let ApplyMaskFeatherOptions { + mask, + width, + height, + feather, + } = parse_apply_mask_feather_options(options)?; + + with_gpu_context(|context| { + let mask_texture = import_canvas_texture(context, &mask, width, height)?; + let result_texture = context.apply_mask_feather(gpu::ApplyMaskFeatherOptions { + mask: &mask_texture, + width, + height, + feather, + }); + let output_canvas = wgpu::web_sys::OffscreenCanvas::new(width, height)?; + let surface = context + .instance() + .create_surface(wgpu::SurfaceTarget::OffscreenCanvas(output_canvas.clone())) + .map_err(|error| JsValue::from_str(&error.to_string()))?; + + context + .render_texture_to_surface(&result_texture, &surface, width, height) + .map_err(|error| JsValue::from_str(&error.to_string()))?; + Ok(output_canvas) + }) +} + +fn with_gpu_context<T>( + action: impl FnOnce(&GpuContext) -> Result<T, JsValue>, +) -> Result<T, JsValue> { + GPU_CONTEXT.with(|context| { + let borrow = context.borrow(); + let Some(gpu_context) = borrow.as_ref() else { + return Err(JsValue::from_str( + "GPU context not initialized. Call initializeGpu() first.", + )); + }; + action(gpu_context) + }) +} + +fn import_canvas_texture( + context: &GpuContext, + canvas: &wgpu::web_sys::OffscreenCanvas, + width: u32, + height: u32, +) -> Result<wgpu::Texture, JsValue> { + let texture = context.create_render_texture(width, height, "gpu-bridge-input-texture"); + context.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, + }, + ); + Ok(texture) +} + +fn map_effect_passes(effect_passes: Vec<EffectPassInput>) -> Vec<EffectPass> { + effect_passes + .into_iter() + .map(|pass| EffectPass { + shader: pass.shader, + uniforms: pass + .uniforms + .into_iter() + .map(|uniform| { + let value = if uniform.value.len() == 1 { + UniformValue::Number(uniform.value[0]) + } else { + UniformValue::Vector(uniform.value) + }; + (uniform.name, value) + }) + .collect(), + }) + .collect() +} + +fn parse_apply_effect_passes_options(value: JsValue) -> Result<ApplyEffectPassesOptions, JsValue> { + let object: Object = value + .dyn_into() + .map_err(|_| JsValue::from_str("applyEffectPasses expects an options object"))?; + + Ok(ApplyEffectPassesOptions { + source: read_offscreen_canvas_property(&object, "source")?, + width: read_u32_property(&object, "width")?, + height: read_u32_property(&object, "height")?, + passes: read_serde_property(&object, "passes")?, + }) +} + +fn parse_apply_mask_feather_options(value: JsValue) -> Result<ApplyMaskFeatherOptions, JsValue> { + let object: Object = value + .dyn_into() + .map_err(|_| JsValue::from_str("applyMaskFeather expects an options object"))?; + + Ok(ApplyMaskFeatherOptions { + mask: read_offscreen_canvas_property(&object, "mask")?, + width: read_u32_property(&object, "width")?, + height: read_u32_property(&object, "height")?, + feather: read_f32_property(&object, "feather")?, + }) +} + +fn read_property(object: &Object, name: &str) -> Result<JsValue, JsValue> { + Reflect::get(object, &JsValue::from_str(name)) + .map_err(|_| JsValue::from_str(&format!("Missing property '{name}'"))) +} + +fn read_offscreen_canvas_property( + object: &Object, + name: &str, +) -> Result<wgpu::web_sys::OffscreenCanvas, JsValue> { + read_property(object, name)? + .dyn_into::<wgpu::web_sys::OffscreenCanvas>() + .map_err(|_| JsValue::from_str(&format!("Property '{name}' must be an OffscreenCanvas"))) +} + +fn read_u32_property(object: &Object, name: &str) -> Result<u32, JsValue> { + let value = read_property(object, name)?; + let Some(number) = value.as_f64() else { + return Err(JsValue::from_str(&format!( + "Property '{name}' must be a number" + ))); + }; + Ok(number as u32) +} + +fn read_f32_property(object: &Object, name: &str) -> Result<f32, JsValue> { + let value = read_property(object, name)?; + let Some(number) = value.as_f64() else { + return Err(JsValue::from_str(&format!( + "Property '{name}' must be a number" + ))); + }; + Ok(number as f32) +} + +fn read_serde_property<T>(object: &Object, name: &str) -> Result<T, JsValue> +where + T: for<'de> Deserialize<'de>, +{ + let value = read_property(object, name)?; + serde_wasm_bindgen::from_value(value) + .map_err(|error| JsValue::from_str(&format!("Invalid property '{name}': {error}"))) +} diff --git a/rust/wasm/src/wasm.rs b/rust/wasm/src/wasm.rs index e852df5f..0a78a314 100644 --- a/rust/wasm/src/wasm.rs +++ b/rust/wasm/src/wasm.rs @@ -1 +1,6 @@ +#[cfg(target_arch = "wasm32")] +mod gpu_bridge; + +#[cfg(target_arch = "wasm32")] +pub use gpu_bridge::*; pub use time::*; |
