From dbd68ff1396bf498eb97f095810b78d2bddd0011 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 13 Apr 2026 04:40:19 +0200 Subject: refactor: gpu webgl fallback and batched command encoding Made-with: Cursor --- rust/crates/gpu/src/context.rs | 413 ++++++++++++++++++++++------ rust/crates/gpu/src/shaders/fullscreen.wgsl | 2 +- 2 files changed, 326 insertions(+), 89 deletions(-) (limited to 'rust/crates/gpu/src') 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::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 { - 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,36 +298,72 @@ 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, surface: &wgpu::Surface<'_>, 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 { + 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; } -- cgit v1.3.1