From e579ae120290329fd697b50ea9138ffccb65a805 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 1 Apr 2026 13:57:32 +0200 Subject: feat: migrate GPU renderer from WebGL to wgpu/WASM --- rust/crates/gpu/src/effect_pipeline.rs | 194 +++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 rust/crates/gpu/src/effect_pipeline.rs (limited to 'rust/crates/gpu/src/effect_pipeline.rs') 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, +} + +#[derive(Clone, Debug)] +pub enum UniformValue { + Number(f32), + Vector(Vec), +} + +#[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 { + let mut current_texture: Option = 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 { + 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 { + 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]]) +} -- cgit v1.3.1