diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-01 13:57:32 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-01 13:57:32 +0200 |
| commit | e579ae120290329fd697b50ea9138ffccb65a805 (patch) | |
| tree | 12a63ee60b00e5004a1806ddf3ebd70c012e3e28 /rust/crates/gpu/src/context.rs | |
| parent | b048b739bd598f9d9ff97166f29bcb6e37c40607 (diff) | |
| download | opencut-e579ae120290329fd697b50ea9138ffccb65a805.tar.gz | |
feat: migrate GPU renderer from WebGL to wgpu/WASM
Diffstat (limited to 'rust/crates/gpu/src/context.rs')
| -rw-r--r-- | rust/crates/gpu/src/context.rs | 250 |
1 files changed, 250 insertions, 0 deletions
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(()) + } +} |
