#[cfg(not(target_arch = "wasm32"))]
use henad_compute::gpu::GpuContext;
use henad_compute::gpu::GpuNeeds;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct TestDeviceRequest {
#[cfg_attr(
target_arch = "wasm32",
expect(dead_code, reason = "the native headless_test_device alone reads a request")
)]
needs: Option<GpuNeeds>,
features: wgpu::Features,
}
impl TestDeviceRequest {
pub fn baseline() -> Self {
Self {
needs: None,
features: wgpu::Features::empty(),
}
}
pub fn raised(needs: GpuNeeds) -> Self {
Self {
needs: Some(needs),
features: wgpu::Features::empty(),
}
}
pub fn features(mut self, features: wgpu::Features) -> Self {
self.features |= features;
self
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn headless_test_device(request: &TestDeviceRequest) -> Option<GpuContext> {
use henad_compute::fault::FaultSink;
use henad_compute::runtime_info::RuntimeInfo;
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
..Default::default()
}));
let adapter = match adapter {
Ok(adapter) => adapter,
Err(error) => return unavailable(&format!("no adapter is available: {error}")),
};
if !adapter.features().contains(request.features) {
return None;
}
let raise = |baseline: &wgpu::Limits, needs| henad_compute::gpu::limits::raise(&adapter, baseline, needs);
let required_limits = match requested_limits(request, &adapter.limits(), raise) {
Ok(limits) => limits,
Err(limit) => {
return unavailable(&format!(
"adapter '{}' offers less than the WebGPU baseline in {limit}",
adapter.get_info().name
));
}
};
let device = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("henad-test"),
required_features: request.features,
required_limits,
..Default::default()
}));
match device {
Ok((device, queue)) => {
let runtime = RuntimeInfo::collect(&adapter, &device);
Some(
GpuContext::new(device, queue, wgpu::TextureFormat::Rgba8Unorm, FaultSink::new())
.with_runtime_info(runtime),
)
}
Err(error) => unavailable(&format!(
"adapter '{}' gave no device: {error}",
adapter.get_info().name
)),
}
}
#[cfg(not(target_arch = "wasm32"))]
fn requested_limits(
request: &TestDeviceRequest,
available: &wgpu::Limits,
raise: impl FnOnce(&wgpu::Limits, GpuNeeds) -> wgpu::Limits,
) -> Result<wgpu::Limits, &'static str> {
crate::device::device_limits(available, |baseline| match request.needs {
Some(needs) => raise(baseline, needs),
None => baseline.clone(),
})
}
#[cfg(not(target_arch = "wasm32"))]
fn unavailable(reason: &str) -> Option<GpuContext> {
assert!(!gpu_required(), "HENAD_REQUIRE_GPU is set, but {reason}");
None
}
pub(super) fn gpu_required() -> bool {
std::env::var_os("HENAD_REQUIRE_GPU").is_some_and(|value| !value.is_empty() && value != "0")
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use henad_compute::gpu::GpuNeeds;
use super::{TestDeviceRequest, requested_limits};
#[test]
fn an_adapter_short_of_the_baseline_gives_no_raised_device() {
let available = wgpu::Limits {
max_storage_buffers_per_shader_stage: 7,
..wgpu::Limits::default()
};
let clamp = |base: &wgpu::Limits, needs: GpuNeeds| wgpu::Limits {
max_storage_buffers_per_shader_stage: base
.max_storage_buffers_per_shader_stage
.max(needs.storage_buffers())
.min(available.max_storage_buffers_per_shader_stage),
..base.clone()
};
let raised = TestDeviceRequest::raised(GpuNeeds::with_storage_buffers(8));
for request in [TestDeviceRequest::baseline(), raised.clone()] {
assert_eq!(
requested_limits(&request, &available, clamp).err(),
Some("max_storage_buffers_per_shader_stage"),
"{request:?}"
);
}
assert!(requested_limits(&raised, &wgpu::Limits::default(), clamp).is_ok());
}
}