summaryrefslogtreecommitdiff
path: root/rust/crates/gpu/src/context.rs
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-01 13:57:32 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-01 13:57:32 +0200
commite579ae120290329fd697b50ea9138ffccb65a805 (patch)
tree12a63ee60b00e5004a1806ddf3ebd70c012e3e28 /rust/crates/gpu/src/context.rs
parentb048b739bd598f9d9ff97166f29bcb6e37c40607 (diff)
downloadopencut-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.rs250
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(())
+ }
+}