use std::sync::Arc;
use crate::backend;
use crate::exit;
#[derive(Debug, Clone)]
pub struct ContextOptions {
pub backends: wgpu::Backends,
pub adapter_name: Option<String>,
pub low_power: bool,
pub min_storage_buffers_per_stage: u32,
pub limits_cap: Option<LimitsCap>,
pub label: String,
}
impl ContextOptions {
pub fn new(backends: wgpu::Backends, label: impl Into<String>) -> Self {
Self {
backends,
adapter_name: None,
low_power: false,
min_storage_buffers_per_stage: 8,
limits_cap: None,
label: label.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LimitsCap {
pub storage_binding_bytes: u64,
pub buffer_bytes: u64,
}
pub fn parse_limits_cap(text: &str) -> Option<LimitsCap> {
let v = text.trim().to_ascii_lowercase();
match v.as_str() {
"" => None,
"webgpu" | "downlevel" | "default" => {
let d = wgpu::Limits::default();
Some(LimitsCap {
storage_binding_bytes: d.max_storage_buffer_binding_size,
buffer_bytes: d.max_buffer_size,
})
}
n => {
let mb: u64 = n
.trim_end_matches("mib")
.trim_end_matches("mb")
.trim()
.parse()
.ok()?;
let bytes = mb.max(1) << 20;
Some(LimitsCap {
storage_binding_bytes: bytes,
buffer_bytes: 2 * bytes,
})
}
}
}
pub fn pick_adapter(names: &[String], wanted: &str) -> Option<usize> {
names.iter().position(|n| n == wanted)
}
pub struct GpuContext {
pub device: wgpu::Device,
pub queue: wgpu::Queue,
pub info: wgpu::AdapterInfo,
pub max_buffer: u64,
}
impl std::fmt::Debug for GpuContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GpuContext")
.field("adapter", &self.info.name)
.field("backend", &self.info.backend)
.field("max_buffer", &self.max_buffer)
.finish()
}
}
fn new_instance(backends: wgpu::Backends) -> wgpu::Instance {
let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
desc.backends = backends;
desc.backend_options = backend::backend_options();
desc.flags = backend::instance_flags_from_env(desc.flags);
wgpu::Instance::new(desc)
}
fn usable_adapters(instance: &wgpu::Instance, backends: wgpu::Backends) -> Vec<wgpu::Adapter> {
pollster::block_on(instance.enumerate_adapters(backends))
.into_iter()
.filter(|a| a.get_info().device_type != wgpu::DeviceType::Cpu)
.collect()
}
pub fn adapter_names(backends: wgpu::Backends) -> Vec<String> {
usable_adapters(&new_instance(backends), backends)
.iter()
.map(|a| a.get_info().name)
.collect()
}
impl GpuContext {
pub fn create(
options: &ContextOptions,
on_failure: impl Fn(String) + Send + Sync + 'static,
) -> Result<GpuContext, String> {
let Some(_work) = exit::enter() else {
return Err("the process is ending".into());
};
let on_failure: Arc<dyn Fn(String) + Send + Sync> = Arc::new(on_failure);
backend::with_init_marker(options.backends, || {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
create_unguarded(options, &on_failure)
}))
.unwrap_or_else(|_| Err("device creation panicked".into()))
})
}
}
fn create_unguarded(
options: &ContextOptions,
on_failure: &Arc<dyn Fn(String) + Send + Sync>,
) -> Result<GpuContext, String> {
let backends = options.backends;
let instance = new_instance(backends);
let power_preference = if options.low_power {
wgpu::PowerPreference::LowPower
} else {
wgpu::PowerPreference::HighPerformance
};
let by_name = options.adapter_name.as_deref().and_then(|wanted| {
let found = usable_adapters(&instance, backends);
let names: Vec<String> = found.iter().map(|a| a.get_info().name).collect();
let index = pick_adapter(&names, wanted);
if index.is_none() {
log::warn!(
"gpu: no adapter named {wanted:?} among {names:?}; using the default choice"
);
}
index.and_then(|i| found.into_iter().nth(i))
});
let adapter = match by_name {
Some(adapter) => adapter,
None => pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference,
..Default::default()
}))
.map_err(|e| format!("no GPU adapter among {backends:?} ({e})"))?,
};
let info = adapter.get_info();
if info.device_type == wgpu::DeviceType::Cpu {
log::info!("gpu: only a software adapter ({})", info.name);
return Err(format!("software adapter ({}) skipped", info.name));
}
let mut limits = adapter.limits();
if limits.max_storage_buffers_per_shader_stage < options.min_storage_buffers_per_stage {
log::info!("gpu: {} has too few storage buffers per stage", info.name);
return Err(format!(
"{} has too few storage buffers per shader stage ({} < {})",
info.name,
limits.max_storage_buffers_per_shader_stage,
options.min_storage_buffers_per_stage
));
}
if let Some(cap) = options.limits_cap {
limits.max_storage_buffer_binding_size = limits
.max_storage_buffer_binding_size
.min(cap.storage_binding_bytes);
limits.max_buffer_size = limits.max_buffer_size.min(cap.buffer_bytes);
log::info!(
"gpu: limits capped: storage bindings <= {} MiB, buffers <= {} MiB",
cap.storage_binding_bytes >> 20,
cap.buffer_bytes >> 20
);
}
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some(options.label.as_str()),
required_limits: limits.clone(),
..Default::default()
}))
.map_err(|e| format!("{}: device creation failed ({e})", info.name))?;
let uncaptured = Arc::clone(on_failure);
device.on_uncaptured_error(Arc::new(move |e| {
log::error!("gpu: {e}");
uncaptured(format!("device error: {e}"));
}));
let lost = Arc::clone(on_failure);
device.set_device_lost_callback(move |reason, msg| {
log::error!("gpu: device lost ({reason:?}): {msg}");
lost(format!("device lost ({reason:?}): {msg}"));
});
let max_buffer = limits
.max_storage_buffer_binding_size
.min(limits.max_buffer_size);
log::info!(
"gpu: {} ({:?}), buffers <= {} MiB",
info.name,
info.backend,
max_buffer >> 20
);
Ok(GpuContext {
device,
queue,
info,
max_buffer,
})
}