OpenCut/rust/crates/gpu/src/context.rs

674 lines
24 KiB
Rust

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::DisplayHandle<'_>, 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<web_sys::HtmlCanvasElement>,
}
impl GpuContext {
pub async fn new() -> Result<Self, GpuError> {
#[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<web_sys::HtmlCanvasElement>,
),
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, 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<wgpu::SurfaceTexture, GpuError> {
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(())
}
}