henad_explore/testing/
device.rs1#[cfg(not(target_arch = "wasm32"))]
4use henad_compute::gpu::GpuContext;
5use henad_compute::gpu::GpuNeeds;
6
7#[derive(Debug, Clone)]
9#[non_exhaustive]
10pub struct TestDeviceRequest {
11 #[cfg_attr(
13 target_arch = "wasm32",
14 expect(dead_code, reason = "the native headless_test_device alone reads a request")
15 )]
16 needs: Option<GpuNeeds>,
17 features: wgpu::Features,
18}
19
20impl TestDeviceRequest {
21 pub fn baseline() -> Self {
23 Self {
24 needs: None,
25 features: wgpu::Features::empty(),
26 }
27 }
28
29 pub fn raised(needs: GpuNeeds) -> Self {
34 Self {
35 needs: Some(needs),
36 features: wgpu::Features::empty(),
37 }
38 }
39
40 pub fn features(mut self, features: wgpu::Features) -> Self {
45 self.features |= features;
46 self
47 }
48}
49
50#[cfg(not(target_arch = "wasm32"))]
64pub fn headless_test_device(request: &TestDeviceRequest) -> Option<GpuContext> {
65 use henad_compute::fault::FaultSink;
66 use henad_compute::runtime_info::RuntimeInfo;
67
68 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
69 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
70 power_preference: wgpu::PowerPreference::HighPerformance,
71 ..Default::default()
72 }));
73 let adapter = match adapter {
74 Ok(adapter) => adapter,
75 Err(error) => return unavailable(&format!("no adapter is available: {error}")),
76 };
77 if !adapter.features().contains(request.features) {
79 return None;
80 }
81 let raise = |baseline: &wgpu::Limits, needs| henad_compute::gpu::limits::raise(&adapter, baseline, needs);
82 let required_limits = match requested_limits(request, &adapter.limits(), raise) {
83 Ok(limits) => limits,
84 Err(limit) => {
85 return unavailable(&format!(
86 "adapter '{}' offers less than the WebGPU baseline in {limit}",
87 adapter.get_info().name
88 ));
89 }
90 };
91 let device = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
92 label: Some("henad-test"),
93 required_features: request.features,
94 required_limits,
95 ..Default::default()
96 }));
97 match device {
98 Ok((device, queue)) => {
99 let runtime = RuntimeInfo::collect(&adapter, &device);
100 Some(
101 GpuContext::new(device, queue, wgpu::TextureFormat::Rgba8Unorm, FaultSink::new())
102 .with_runtime_info(runtime),
103 )
104 }
105 Err(error) => unavailable(&format!(
106 "adapter '{}' gave no device: {error}",
107 adapter.get_info().name
108 )),
109 }
110}
111
112#[cfg(not(target_arch = "wasm32"))]
123fn requested_limits(
124 request: &TestDeviceRequest,
125 available: &wgpu::Limits,
126 raise: impl FnOnce(&wgpu::Limits, GpuNeeds) -> wgpu::Limits,
127) -> Result<wgpu::Limits, &'static str> {
128 crate::device::device_limits(available, |baseline| match request.needs {
129 Some(needs) => raise(baseline, needs),
130 None => baseline.clone(),
131 })
132}
133
134#[cfg(not(target_arch = "wasm32"))]
140fn unavailable(reason: &str) -> Option<GpuContext> {
141 assert!(!gpu_required(), "HENAD_REQUIRE_GPU is set, but {reason}");
142 None
143}
144
145pub(super) fn gpu_required() -> bool {
149 std::env::var_os("HENAD_REQUIRE_GPU").is_some_and(|value| !value.is_empty() && value != "0")
150}
151
152#[cfg(all(test, not(target_arch = "wasm32")))]
153mod tests {
154 use henad_compute::gpu::GpuNeeds;
155
156 use super::{TestDeviceRequest, requested_limits};
157
158 #[test]
161 fn an_adapter_short_of_the_baseline_gives_no_raised_device() {
162 let available = wgpu::Limits {
163 max_storage_buffers_per_shader_stage: 7,
164 ..wgpu::Limits::default()
165 };
166 let clamp = |base: &wgpu::Limits, needs: GpuNeeds| wgpu::Limits {
167 max_storage_buffers_per_shader_stage: base
168 .max_storage_buffers_per_shader_stage
169 .max(needs.storage_buffers())
170 .min(available.max_storage_buffers_per_shader_stage),
171 ..base.clone()
172 };
173 let raised = TestDeviceRequest::raised(GpuNeeds::with_storage_buffers(8));
174 for request in [TestDeviceRequest::baseline(), raised.clone()] {
175 assert_eq!(
176 requested_limits(&request, &available, clamp).err(),
177 Some("max_storage_buffers_per_shader_stage"),
178 "{request:?}"
179 );
180 }
181 assert!(requested_limits(&raised, &wgpu::Limits::default(), clamp).is_ok());
182 }
183}