2026-04-01 13:57:32 +02:00
|
|
|
use wgpu::util::DeviceExt;
|
|
|
|
|
|
2026-04-07 01:09:13 +02:00
|
|
|
use crate::{FULLSCREEN_SHADER_SOURCE, GPU_TEXTURE_FORMAT, GpuError};
|
|
|
|
|
|
|
|
|
|
const BLIT_SHADER_SOURCE: &str = include_str!("shaders/blit.wgsl");
|
2026-04-01 13:57:32 +02:00
|
|
|
|
|
|
|
|
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,
|
2026-04-07 01:09:13 +02:00
|
|
|
texture_sampler_bind_group_layout: wgpu::BindGroupLayout,
|
|
|
|
|
blit_pipeline: wgpu::RenderPipeline,
|
2026-04-01 13:57:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
});
|
2026-04-07 01:09:13 +02:00
|
|
|
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,
|
|
|
|
|
});
|
2026-04-01 13:57:32 +02:00
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
|
instance,
|
|
|
|
|
adapter,
|
|
|
|
|
device,
|
|
|
|
|
queue,
|
|
|
|
|
fullscreen_quad,
|
|
|
|
|
linear_sampler,
|
|
|
|
|
nearest_sampler,
|
2026-04-07 01:09:13 +02:00
|
|
|
texture_sampler_bind_group_layout,
|
|
|
|
|
blit_pipeline,
|
2026-04-01 13:57:32 +02:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 01:09:13 +02:00
|
|
|
pub fn texture_sampler_bind_group_layout(&self) -> &wgpu::BindGroupLayout {
|
|
|
|
|
&self.texture_sampler_bind_group_layout
|
2026-04-01 13:57:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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"),
|
2026-04-07 01:09:13 +02:00
|
|
|
layout: &self.texture_sampler_bind_group_layout,
|
2026-04-01 13:57:32 +02:00
|
|
|
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,
|
|
|
|
|
});
|
2026-04-07 01:09:13 +02:00
|
|
|
render_pass.set_pipeline(&self.blit_pipeline);
|
2026-04-01 13:57:32 +02:00
|
|
|
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(())
|
|
|
|
|
}
|
2026-04-07 01:09:13 +02:00
|
|
|
|
|
|
|
|
#[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)
|
|
|
|
|
}
|
2026-04-01 13:57:32 +02:00
|
|
|
}
|