use wgpu::util::DeviceExt; use crate::{FULLSCREEN_SHADER_SOURCE, GPU_TEXTURE_FORMAT, GpuError}; 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, fullscreen_quad: wgpu::Buffer, linear_sampler: wgpu::Sampler, nearest_sampler: wgpu::Sampler, texture_sampler_bind_group_layout: wgpu::BindGroupLayout, blit_pipeline: wgpu::RenderPipeline, } 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 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: GPU_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, }); Ok(Self { instance, adapter, device, queue, fullscreen_quad, linear_sampler, nearest_sampler, texture_sampler_bind_group_layout, blit_pipeline, }) } 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 texture_sampler_bind_group_layout(&self) -> &wgpu::BindGroupLayout { &self.texture_sampler_bind_group_layout } 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.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 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(()) } #[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); 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, }, ); 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> { let surface = self .instance .create_surface(wgpu::SurfaceTarget::OffscreenCanvas(canvas.clone()))?; self.render_texture_to_surface(texture, &surface, width, height) } }