use wgpu::util::DeviceExt; #[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"); 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, 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, /// The HTML canvas that the WebGL context is bound to. Only populated on the WebGL /// fallback path. Used by render_texture_via_gl_canvas to output frames on WebGL. #[cfg(all(feature = "wasm", target_arch = "wasm32"))] gl_canvas: Option, } impl GpuContext { pub async fn new() -> Result { #[cfg(all(feature = "wasm", target_arch = "wasm32"))] let (instance, adapter, device, queue, gl_canvas) = Self::acquire_device().await?; #[cfg(not(all(feature = "wasm", target_arch = "wasm32")))] 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), 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 texture_sampler_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("gpu-texture-sampler-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 vertex_shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("gpu-fullscreen-shader"), source: wgpu::ShaderSource::Wgsl(FULLSCREEN_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 blit_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("gpu-blit-pipeline-layout"), bind_group_layouts: &[Some(&texture_sampler_bind_group_layout)], immediate_size: 0, }); 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: 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 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, #[cfg(all(feature = "wasm", target_arch = "wasm32"))] gl_canvas, }) } #[cfg(all(feature = "wasm", target_arch = "wasm32"))] async fn acquire_device() -> Result< ( wgpu::Instance, wgpu::Adapter, wgpu::Device, wgpu::Queue, Option, ), GpuError, > { // Temporary fix: force the wasm renderer onto the WebGL backend even when // WebGPU is available while a WebGPU bug is being investigated. // 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, None)), // Err(_) => {} // } let (gl_instance, adapter, device, queue, canvas) = Self::try_gl_fallback().await?; Ok((gl_instance, adapter, device, queue, Some(canvas))) } #[cfg(not(all(feature = "wasm", target_arch = "wasm32")))] 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, web_sys::HtmlCanvasElement, ), 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.clone()))?; let (adapter, device, queue) = Self::try_request_device(&gl_instance, Some(&surface)).await?; Ok((gl_instance, adapter, device, queue, canvas)) } #[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, 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: self.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 texture_format(&self) -> wgpu::TextureFormat { self.texture_format } 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 texture_sampler_bind_group_layout(&self) -> &wgpu::BindGroupLayout { &self.texture_sampler_bind_group_layout } pub fn blit_pipeline(&self) -> &wgpu::RenderPipeline { &self.blit_pipeline } /// Whether the GPU backend can render to arbitrary canvas surfaces. /// True for WebGPU, false for WebGL which can only surface-render to /// the specific canvas its GL context was originally created on. pub fn supports_surface_rendering(&self) -> bool { self.supports_external_texture_copies } 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 != self.texture_format { return Err(GpuError::UnsupportedSurfaceFormat); } surface.configure(&self.device, &config); Ok(()) } 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) => Ok(surface_texture), wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded | wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost | 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 bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("gpu-blit-bind-group"), layout: &self.texture_sampler_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 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"))] pub fn import_offscreen_canvas_texture( &self, canvas: &wgpu::web_sys::OffscreenCanvas, width: u32, height: u32, label: &'static str, ) -> wgpu::Texture { let texture = self.create_render_texture(width, height, label); 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 } #[cfg(all(feature = "wasm", target_arch = "wasm32"))] pub fn render_texture_to_offscreen_canvas( &self, texture: &wgpu::Texture, canvas: &wgpu::web_sys::OffscreenCanvas, width: u32, height: u32, ) -> Result<(), GpuError> { 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.render_texture_to_offscreen_canvas_via_gl_canvas(texture, canvas, width, height) } #[cfg(all(feature = "wasm", target_arch = "wasm32"))] fn render_texture_to_offscreen_canvas_via_gl_canvas( &self, texture: &wgpu::Texture, canvas: &wgpu::web_sys::OffscreenCanvas, width: u32, height: u32, ) -> Result<(), GpuError> { let gl_canvas = self.render_texture_to_gl_canvas_surface(texture, width, height)?; let ctx: web_sys::OffscreenCanvasRenderingContext2d = canvas .get_context("2d") .ok() .flatten() .ok_or(GpuError::AdapterUnavailable)? .unchecked_into(); ctx.clear_rect(0.0, 0.0, width as f64, height as f64); ctx.draw_image_with_html_canvas_element(gl_canvas, 0.0, 0.0) .map_err(|_| GpuError::AdapterUnavailable)?; Ok(()) } #[cfg(all(feature = "wasm", target_arch = "wasm32"))] fn render_texture_to_gl_canvas_surface( &self, texture: &wgpu::Texture, width: u32, height: u32, ) -> Result<&web_sys::HtmlCanvasElement, GpuError> { let gl_canvas = self .gl_canvas .as_ref() .ok_or(GpuError::AdapterUnavailable)?; gl_canvas.set_width(width); gl_canvas.set_height(height); let surface = self .instance .create_surface(wgpu::SurfaceTarget::Canvas(gl_canvas.clone()))?; let caps = surface.get_capabilities(&self.adapter); let surface_format = if caps.formats.contains(&self.texture_format) { self.texture_format } else if !caps.formats.is_empty() { caps.formats[0] } else { return Err(GpuError::UnsupportedSurfaceFormat); }; if surface_format != self.texture_format { return Err(GpuError::UnsupportedSurfaceFormat); } let config = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: surface_format, width, height, present_mode: wgpu::PresentMode::Fifo, alpha_mode: caps .alpha_modes .first() .copied() .unwrap_or(wgpu::CompositeAlphaMode::Auto), view_formats: vec![], desired_maximum_frame_latency: 2, }; surface.configure(&self.device, &config); let surface_texture = self.acquire_surface_texture(&surface)?; let surface_view = surface_texture .texture .create_view(&wgpu::TextureViewDescriptor::default()); let mut encoder = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("gpu-gl-canvas-blit-encoder"), }); self.encode_texture_blit_to_view( &mut encoder, texture, &surface_view, "gpu-gl-canvas-blit", ); self.queue.submit([encoder.finish()]); surface_texture.present(); Ok(gl_canvas) } /// Renders a texture to an arbitrary HTML canvas on the WebGL backend. /// /// WebGL can only surface-render to the canvas its GL context was originally created on. /// This method renders the texture to the GL canvas, then uses drawImage to copy the /// result to the target canvas — avoiding the async buffer readback issue entirely. /// /// The HTML canvas default color space is sRGB, so the surface may report /// `Rgba8UnormSrgb` as its preferred format even though our render textures use /// `Rgba8Unorm`. We explicitly select `Rgba8Unorm` from the surface's supported /// format list (WebGL2 supports both) to avoid the format mismatch. #[cfg(all(feature = "wasm", target_arch = "wasm32"))] pub fn render_texture_via_gl_canvas( &self, texture: &wgpu::Texture, target_canvas: &web_sys::HtmlCanvasElement, width: u32, height: u32, ) -> Result<(), GpuError> { let gl_canvas = self.render_texture_to_gl_canvas_surface(texture, width, height)?; let ctx: web_sys::CanvasRenderingContext2d = target_canvas .get_context("2d") .ok() .flatten() .ok_or(GpuError::AdapterUnavailable)? .unchecked_into(); ctx.draw_image_with_html_canvas_element(gl_canvas, 0.0, 0.0) .map_err(|_| GpuError::AdapterUnavailable)?; Ok(()) } }