use core::fmt;
use std::sync::{Arc, Mutex};
use winit::window::Window;
use crate::Error;
use crate::math::UVec2;
use crate::platform::{AFTER_LOSS, AFTER_OUT_OF_MEMORY};
pub(crate) const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
pub(crate) const SMALLEST_VALUES: wgpu::BufferAddress = 16;
pub(crate) struct Gpu {
instance: wgpu::Instance,
window: Arc<Window>,
device: wgpu::Device,
queue: wgpu::Queue,
surface: wgpu::Surface<'static>,
format: wgpu::TextureFormat,
physical_size: UVec2,
faults: FaultSlot,
adapter: String,
}
impl Gpu {
pub(crate) async fn new(
instance: wgpu::Instance,
window: Arc<Window>,
facts: Option<String>,
) -> Result<Self, Error> {
let surface = instance
.create_surface(Arc::clone(&window))
.map_err(|error| Error::msg(format!("no rendering surface for the window: {error}")))?;
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
..Default::default()
})
.await
.map_err(|error| Error::msg(format!("no usable graphics adapter: {error}")))?;
let named = named(&adapter.get_info(), facts);
log::info!("graphics adapter: {named}");
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("mirage-engine"),
..Default::default()
})
.await
.map_err(|error| {
Error::msg(format!("the graphics adapter refused a device: {error}"))
})?;
let faults = FaultSlot::watching(&device);
let capabilities = surface.get_capabilities(&adapter);
let format = capabilities
.formats
.first()
.copied()
.ok_or_else(|| Error::msg("the rendering surface supports no texture format"))?;
let physical_size = physical_size(&window);
let mut gpu = Self {
instance,
window,
device,
queue,
surface,
format,
physical_size,
faults,
adapter: named,
};
gpu.configure_surface();
Ok(gpu)
}
pub(crate) fn adapter(&self) -> &str {
&self.adapter
}
pub(crate) fn device(&self) -> &wgpu::Device {
&self.device
}
pub(crate) fn queue(&self) -> &wgpu::Queue {
&self.queue
}
pub(crate) fn window(&self) -> Arc<Window> {
Arc::clone(&self.window)
}
pub(crate) fn target_format(&self) -> wgpu::TextureFormat {
self.format.add_srgb_suffix()
}
pub(crate) fn overlay_format(&self) -> wgpu::TextureFormat {
self.format.remove_srgb_suffix()
}
pub(crate) fn physical_size(&self) -> UVec2 {
self.physical_size
}
pub(crate) fn fault(&self) -> Option<Fault> {
self.faults.taken()
}
pub(crate) fn request_frame(&self) {
if self.drawable_size().is_some() {
self.window.request_redraw();
}
}
pub(crate) fn resize(&mut self, physical_size: UVec2) {
if physical_size == self.physical_size {
return;
}
self.physical_size = physical_size;
self.configure_surface();
}
pub(crate) fn begin_frame(&mut self) -> Option<Frame> {
let size = self.drawable_size()?;
let surface = self.acquire_surface_texture()?;
let view = |format| {
surface.texture.create_view(&wgpu::TextureViewDescriptor {
format: Some(format),
..Default::default()
})
};
let target = Target::new(
view(self.target_format()),
view(self.overlay_format()),
size,
);
Some(Frame { surface, target })
}
pub(crate) fn present(&self, frame: Frame) {
self.window.pre_present_notify();
frame.surface.present();
}
fn acquire_surface_texture(&mut self) -> Option<wgpu::SurfaceTexture> {
match self.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(frame) => Some(frame),
wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => None,
wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
drop(frame);
self.configure_surface();
None
}
wgpu::CurrentSurfaceTexture::Outdated => {
self.configure_surface();
None
}
wgpu::CurrentSurfaceTexture::Lost => {
self.rebuild_surface();
None
}
wgpu::CurrentSurfaceTexture::Validation => {
log::error!("the graphics driver rejected the request for a frame");
None
}
}
}
fn drawable_size(&self) -> Option<UVec2> {
let size = self.physical_size;
(size.x > 0 && size.y > 0).then_some(size)
}
fn configure_surface(&mut self) {
let Some(size) = self.drawable_size() else {
return;
};
self.surface.configure(
&self.device,
&wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: self.format,
view_formats: vec![self.target_format(), self.overlay_format()],
alpha_mode: wgpu::CompositeAlphaMode::Auto,
width: size.x,
height: size.y,
desired_maximum_frame_latency: 2,
present_mode: wgpu::PresentMode::AutoVsync,
},
);
}
fn rebuild_surface(&mut self) {
match self.instance.create_surface(Arc::clone(&self.window)) {
Ok(surface) => {
self.surface = surface;
self.configure_surface();
}
Err(error) => log::error!("the rendering surface could not be rebuilt: {error}"),
}
}
}
pub(crate) enum Fault {
Lost(String),
OutOfMemory(String),
}
impl Fault {
fn lost(reason: wgpu::DeviceLostReason, text: String) -> Self {
match text.is_empty() {
true => Self::Lost(format!("{reason:?}")),
false => Self::Lost(text),
}
}
fn of(error: &wgpu::Error) -> Option<Self> {
match error {
wgpu::Error::OutOfMemory { source } => Some(Self::OutOfMemory(source.to_string())),
wgpu::Error::Validation { .. } | wgpu::Error::Internal { .. } => None,
}
}
pub(crate) fn told(&self, adapter: &str) -> String {
match self {
Self::Lost(_) => format!(
"The graphics device was lost on {adapter}: the graphics driver stopped responding. {AFTER_LOSS}"
),
Self::OutOfMemory(_) => {
format!("The graphics device on {adapter} ran out of memory. {AFTER_OUT_OF_MEMORY}")
}
}
}
}
fn named(info: &wgpu::AdapterInfo, facts: Option<String>) -> String {
let kind = match info.device_type {
wgpu::DeviceType::IntegratedGpu => Some("integrated"),
wgpu::DeviceType::DiscreteGpu => Some("discrete"),
wgpu::DeviceType::VirtualGpu => Some("virtual"),
wgpu::DeviceType::Cpu => Some("software"),
wgpu::DeviceType::Other => None,
};
let backend = match info.backend {
wgpu::Backend::Vulkan => "Vulkan",
wgpu::Backend::Metal => "Metal",
wgpu::Backend::Dx12 => "Direct3D 12",
wgpu::Backend::Gl => "OpenGL",
wgpu::Backend::BrowserWebGpu => "the browser's WebGPU",
wgpu::Backend::Noop => "no backend",
};
let mut parts: Vec<String> = [Some(info.name.clone()), facts]
.into_iter()
.flatten()
.filter(|part| !part.is_empty())
.collect();
if parts.is_empty() {
parts.push("(no name given)".to_owned());
}
parts.extend(kind.map(str::to_owned));
parts.push(backend.to_owned());
parts.join(", ")
}
impl fmt::Display for Fault {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Lost(text) => write!(f, "the graphics device was lost: {text}"),
Self::OutOfMemory(text) => write!(f, "the graphics device ran out of memory: {text}"),
}
}
}
#[derive(Clone)]
pub(crate) struct FaultSlot(Arc<Mutex<Option<Fault>>>);
impl FaultSlot {
fn watching(device: &wgpu::Device) -> Self {
let slot = Self(Arc::default());
let lost = slot.clone();
device.set_device_lost_callback(move |reason, text| lost.set(Fault::lost(reason, text)));
let uncaught = slot.clone();
device.on_uncaptured_error(Arc::new(move |error: wgpu::Error| {
match Fault::of(&error) {
Some(fault) => uncaught.set(fault),
None => log::error!("the graphics device refused a call: {error}"),
}
}));
slot
}
fn set(&self, fault: Fault) {
let Ok(mut slot) = self.0.lock() else {
return;
};
slot.get_or_insert(fault);
}
fn taken(&self) -> Option<Fault> {
self.0.lock().ok()?.take()
}
}
pub(crate) struct Target {
color: wgpu::TextureView,
encoded: wgpu::TextureView,
size: UVec2,
}
impl Target {
pub(crate) fn new(color: wgpu::TextureView, encoded: wgpu::TextureView, size: UVec2) -> Self {
Self {
color,
encoded,
size,
}
}
pub(crate) fn color(&self) -> &wgpu::TextureView {
&self.color
}
pub(crate) fn encoded(&self) -> &wgpu::TextureView {
&self.encoded
}
pub(crate) fn size(&self) -> UVec2 {
self.size
}
pub(crate) fn aspect(&self) -> f32 {
let size = self.size();
size.x as f32 / size.y as f32
}
}
pub(crate) struct Frame {
surface: wgpu::SurfaceTexture,
target: Target,
}
impl Frame {
pub(crate) fn target(&self) -> &Target {
&self.target
}
}
pub(crate) fn depth_texture(device: &wgpu::Device, size: UVec2, samples: u32) -> wgpu::Texture {
texture(
device,
"mirage-engine depth",
size,
samples,
DEPTH_FORMAT,
&[],
)
}
pub(crate) fn texture(
device: &wgpu::Device,
label: &str,
size: UVec2,
samples: u32,
format: wgpu::TextureFormat,
views: &[wgpu::TextureFormat],
) -> wgpu::Texture {
device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d {
width: size.x,
height: size.y,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: samples,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: views,
})
}
pub(crate) fn buffer(
device: &wgpu::Device,
label: &str,
size: wgpu::BufferAddress,
usage: wgpu::BufferUsages,
) -> wgpu::Buffer {
device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size,
usage: usage | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
}
pub(crate) fn pipeline_layout(
device: &wgpu::Device,
label: &str,
groups: &[Option<&wgpu::BindGroupLayout>],
) -> wgpu::PipelineLayout {
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some(label),
bind_group_layouts: groups,
immediate_size: 0,
})
}
pub(crate) fn sampled(binding: u32, filterable: bool) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
}
}
pub(crate) fn clamped_sampler(device: &wgpu::Device, label: &str) -> wgpu::Sampler {
device.create_sampler(&wgpu::SamplerDescriptor {
label: Some(label),
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,
..Default::default()
})
}
pub(crate) fn sampler(binding: u32) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
}
}
pub(crate) fn uniform(binding: u32, visibility: wgpu::ShaderStages) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}
}
fn physical_size(window: &Window) -> UVec2 {
let size = window.inner_size();
UVec2::new(size.width, size.height)
}
#[cfg(test)]
mod tests {
use super::*;
fn source(text: &'static str) -> wgpu::ErrorSource {
Box::<dyn std::error::Error + Send + Sync>::from(text)
}
#[test]
fn a_device_out_of_memory_ends_the_run_with_wgpu_s_own_text() {
let error = wgpu::Error::OutOfMemory {
source: source("the heap has no room left"),
};
match Fault::of(&error) {
Some(fault) => assert_eq!(
fault.to_string(),
"the graphics device ran out of memory: the heap has no room left"
),
None => panic!("running out of memory ends the run"),
}
}
#[test]
fn a_validation_or_internal_error_leaves_the_run_going() {
let validation = wgpu::Error::Validation {
source: source("the bind group is not the layout's"),
description: "binding 3 is missing".to_owned(),
};
let internal = wgpu::Error::Internal {
source: source("the backend stopped"),
description: "a limit inside wgpu was reached".to_owned(),
};
assert!(
Fault::of(&validation).is_none(),
"only the call a validation error names is dropped"
);
assert!(Fault::of(&internal).is_none(), "and an internal one too");
}
#[test]
fn a_lost_device_states_a_reason_whatever_wgpu_stated() {
assert_eq!(
Fault::lost(
wgpu::DeviceLostReason::Unknown,
"the driver timed out".to_owned()
)
.to_string(),
"the graphics device was lost: the driver timed out"
);
assert_eq!(
Fault::lost(wgpu::DeviceLostReason::Destroyed, String::new()).to_string(),
"the graphics device was lost: Destroyed",
"and wgpu naming no text of its own leaves the debug reason"
);
}
#[test]
fn the_slot_holds_the_first_fault_and_hands_it_over_once() {
let slot = FaultSlot(Arc::default());
slot.set(Fault::Lost("the driver timed out".to_owned()));
slot.set(Fault::OutOfMemory("the heap has no room left".to_owned()));
match slot.taken() {
Some(fault) => assert_eq!(
fault.to_string(),
"the graphics device was lost: the driver timed out",
"the fault that ended the run is the first one"
),
None => panic!("a slot written to holds a fault"),
}
assert!(slot.taken().is_none(), "and it is taken out only once");
}
fn info(
name: &str,
device_type: wgpu::DeviceType,
backend: wgpu::Backend,
) -> wgpu::AdapterInfo {
wgpu::AdapterInfo {
name: name.to_owned(),
vendor: 0,
device: 0,
device_type,
device_pci_bus_id: String::new(),
driver: String::new(),
driver_info: String::new(),
backend,
subgroup_min_size: 0,
subgroup_max_size: 0,
transient_saves_memory: false,
}
}
#[test]
fn the_adapter_line_states_what_is_known_and_never_an_empty_name() {
assert_eq!(
named(
&info(
"NVIDIA GeForce RTX 3060 Laptop GPU",
wgpu::DeviceType::DiscreteGpu,
wgpu::Backend::Vulkan
),
None
),
"NVIDIA GeForce RTX 3060 Laptop GPU, discrete, Vulkan"
);
assert_eq!(
named(
&info("", wgpu::DeviceType::Other, wgpu::Backend::BrowserWebGpu),
Some("amd gcn-5".to_owned())
),
"amd gcn-5, the browser's WebGPU",
"a browser names no adapter, so what it states of it stands in"
);
assert_eq!(
named(
&info("", wgpu::DeviceType::Other, wgpu::Backend::BrowserWebGpu),
None
),
"(no name given), the browser's WebGPU"
);
}
#[test]
fn a_fault_tells_the_player_the_adapter_and_the_next_step() {
let told = Fault::Lost("Device is lost".to_owned()).told("amd gcn-5, the browser's WebGPU");
assert_eq!(
told,
format!(
"The graphics device was lost on amd gcn-5, the browser's WebGPU: the graphics driver stopped responding. {AFTER_LOSS}"
)
);
let told =
Fault::OutOfMemory("no room".to_owned()).told("Intel UHD 620, integrated, Vulkan");
assert_eq!(
told,
format!(
"The graphics device on Intel UHD 620, integrated, Vulkan ran out of memory. {AFTER_OUT_OF_MEMORY}"
)
);
}
}