1#[cfg(not(target_arch = "wasm32"))]
2use std::sync::Arc;
3use std::sync::OnceLock;
4
5#[cfg(feature = "gpu-image")]
6use std::collections::HashMap;
7#[cfg(feature = "gpu-image")]
8use std::sync::Mutex;
9
10use spatialrust_core::{DeviceKind, SpatialError, SpatialResult, SpatialRuntime};
11
12use crate::pipeline_cache::ComputePipelineCache;
13use crate::upload_cache::GpuBufferPool;
14
15#[cfg(feature = "gpu-wgpu")]
17pub struct WgpuRuntime {
18 _instance: wgpu::Instance,
19 device: wgpu::Device,
20 queue: wgpu::Queue,
21 pipelines: OnceLock<ComputePipelineCache>,
22 max_gather_channels: u32,
23 upload_pool: GpuBufferPool,
24 adapter_info: WgpuAdapterInfo,
25 #[cfg(feature = "gpu-image")]
26 image_texture_pool: Mutex<HashMap<(u32, u32), Vec<wgpu::Texture>>>,
27 #[cfg(feature = "gpu-image")]
28 pub(crate) image_gray_pipeline: OnceLock<crate::image::kernels::gray::GrayPipeline>,
29 #[cfg(feature = "gpu-image")]
30 pub(crate) image_blur_pipeline: OnceLock<crate::image::kernels::box_blur::BlurPipeline>,
31 #[cfg(feature = "gpu-image")]
32 pub(crate) image_spatial_pipelines: OnceLock<crate::image::kernels::spatial::SpatialPipelines>,
33 #[cfg(feature = "gpu-image")]
34 pub(crate) image_ai_pack_pipeline: OnceLock<crate::image::ai_tensor::AiPackPipeline>,
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct WgpuAdapterInfo {
40 pub name: String,
42 pub backend: String,
44 pub device_type: String,
46 pub driver: String,
48 pub driver_info: String,
50}
51
52#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
54pub enum WgpuPowerPreference {
55 #[default]
57 HighPerformance,
58 LowPower,
60}
61
62#[cfg(feature = "gpu-wgpu")]
64pub const MULTI_GATHER4_STORAGE_BUFFERS: u32 = 10;
65
66#[cfg(feature = "gpu-wgpu")]
68pub const MULTI_GATHER2_STORAGE_BUFFERS: u32 = 6;
69
70#[cfg(feature = "gpu-wgpu")]
71#[cfg(not(target_arch = "wasm32"))]
72static SHARED_RUNTIME: OnceLock<Result<Arc<WgpuRuntime>, String>> = OnceLock::new();
73
74#[cfg(feature = "gpu-wgpu")]
75impl WgpuRuntime {
76 pub fn new_headless() -> SpatialResult<Self> {
80 Self::new_headless_with_preference(WgpuPowerPreference::HighPerformance)
81 }
82
83 pub fn new_headless_with_preference(preference: WgpuPowerPreference) -> SpatialResult<Self> {
85 pollster::block_on(Self::new_headless_async(preference))
86 }
87
88 #[cfg(not(target_arch = "wasm32"))]
90 pub fn shared() -> SpatialResult<Arc<Self>> {
91 match SHARED_RUNTIME.get_or_init(init_shared_runtime) {
92 Ok(runtime) => Ok(Arc::clone(runtime)),
93 Err(message) => Err(SpatialError::InvalidArgument(message.clone())),
94 }
95 }
96
97 #[must_use]
99 pub fn device(&self) -> &wgpu::Device {
100 &self.device
101 }
102
103 #[must_use]
105 pub fn queue(&self) -> &wgpu::Queue {
106 &self.queue
107 }
108
109 #[must_use]
111 pub const fn adapter_info(&self) -> &WgpuAdapterInfo {
112 &self.adapter_info
113 }
114
115 pub fn wait_idle(&self) {
120 self.device.poll(wgpu::Maintain::Wait);
121 }
122
123 #[cfg(feature = "gpu-image")]
124 pub(crate) fn acquire_image_texture(
125 &self,
126 width: u32,
127 height: u32,
128 label: &'static str,
129 ) -> wgpu::Texture {
130 if let Some(texture) = self
131 .image_texture_pool
132 .lock()
133 .expect("image texture pool poisoned")
134 .get_mut(&(width, height))
135 .and_then(Vec::pop)
136 {
137 return texture;
138 }
139 self.device.create_texture(&wgpu::TextureDescriptor {
140 label: Some(label),
141 size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
142 mip_level_count: 1,
143 sample_count: 1,
144 dimension: wgpu::TextureDimension::D2,
145 format: wgpu::TextureFormat::Rgba8Uint,
146 usage: wgpu::TextureUsages::COPY_SRC
147 | wgpu::TextureUsages::COPY_DST
148 | wgpu::TextureUsages::TEXTURE_BINDING
149 | wgpu::TextureUsages::STORAGE_BINDING,
150 view_formats: &[],
151 })
152 }
153
154 #[cfg(feature = "gpu-image")]
155 pub(crate) fn recycle_image_texture(&self, width: u32, height: u32, texture: wgpu::Texture) {
156 let mut pool = self.image_texture_pool.lock().expect("image texture pool poisoned");
157 let textures = pool.entry((width, height)).or_default();
158 if textures.len() < 8 {
159 textures.push(texture);
160 } else {
161 texture.destroy();
162 }
163 }
164
165 #[cfg(feature = "gpu-image")]
167 #[must_use]
168 pub fn cached_image_texture_count(&self) -> usize {
169 self.image_texture_pool.lock().map(|pool| pool.values().map(Vec::len).sum()).unwrap_or(0)
170 }
171
172 #[cfg(feature = "gpu-image")]
174 #[must_use]
175 pub fn initialized_image_pipeline_count(&self) -> usize {
176 usize::from(self.image_gray_pipeline.get().is_some())
177 + usize::from(self.image_blur_pipeline.get().is_some())
178 + usize::from(self.image_spatial_pipelines.get().is_some())
179 + usize::from(self.image_ai_pack_pipeline.get().is_some())
180 }
181
182 #[must_use]
184 pub fn pipelines(&self) -> &ComputePipelineCache {
185 self.pipelines.get_or_init(|| ComputePipelineCache::new(&self.device))
186 }
187
188 #[must_use]
190 pub fn max_gather_channels(&self) -> u32 {
191 self.max_gather_channels
192 }
193
194 #[must_use]
196 pub fn buffer_pool(&self) -> &GpuBufferPool {
197 &self.upload_pool
198 }
199
200 pub fn upload_pod_storage<T: bytemuck::Pod>(
202 &self,
203 label: &'static str,
204 data: &[T],
205 ) -> SpatialResult<wgpu::Buffer> {
206 self.upload_pool.upload_pod_storage(self, label, data)
207 }
208
209 pub fn upload_f32_storage(
211 &self,
212 label: &'static str,
213 data: &[f32],
214 ) -> SpatialResult<wgpu::Buffer> {
215 self.upload_pool.upload_f32_storage(self, label, data)
216 }
217
218 pub fn upload_u32_storage(
220 &self,
221 label: &'static str,
222 data: &[u32],
223 ) -> SpatialResult<wgpu::Buffer> {
224 self.upload_pool.upload_u32_storage(self, label, data)
225 }
226
227 pub fn recycle_storage(&self, byte_len: u64, buffer: wgpu::Buffer) {
229 self.upload_pool.recycle(byte_len, buffer);
230 }
231
232 pub fn clear_buffer_pool(&self) {
234 self.upload_pool.clear();
235 }
236
237 pub async fn new_headless_async(preference: WgpuPowerPreference) -> SpatialResult<Self> {
243 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
244 backends: wgpu::Backends::PRIMARY,
245 ..Default::default()
246 });
247
248 let adapter = instance
249 .request_adapter(&wgpu::RequestAdapterOptions {
250 power_preference: match preference {
251 WgpuPowerPreference::HighPerformance => wgpu::PowerPreference::HighPerformance,
252 WgpuPowerPreference::LowPower => wgpu::PowerPreference::LowPower,
253 },
254 compatible_surface: None,
255 force_fallback_adapter: false,
256 })
257 .await
258 .ok_or_else(|| {
259 SpatialError::InvalidArgument(
260 "no compatible wgpu adapter found for headless compute".to_owned(),
261 )
262 })?;
263
264 let raw_adapter_info = adapter.get_info();
265 let adapter_info = WgpuAdapterInfo {
266 name: raw_adapter_info.name,
267 backend: format!("{:?}", raw_adapter_info.backend),
268 device_type: format!("{:?}", raw_adapter_info.device_type),
269 driver: raw_adapter_info.driver,
270 driver_info: raw_adapter_info.driver_info,
271 };
272 let (device, queue) = adapter
273 .request_device(
274 &wgpu::DeviceDescriptor {
275 label: Some("spatialrust-wgpu"),
276 required_features: wgpu::Features::empty(),
277 required_limits: adapter.limits(),
278 memory_hints: wgpu::MemoryHints::Performance,
279 },
280 None,
281 )
282 .await
283 .map_err(|error| {
284 SpatialError::InvalidArgument(format!("failed to create wgpu device: {error}"))
285 })?;
286
287 let max_gather_channels =
288 max_gather_channels_for_limit(device.limits().max_storage_buffers_per_shader_stage);
289
290 Ok(Self {
291 _instance: instance,
292 device,
293 queue,
294 pipelines: OnceLock::new(),
295 max_gather_channels,
296 upload_pool: GpuBufferPool::default(),
297 adapter_info,
298 #[cfg(feature = "gpu-image")]
299 image_texture_pool: Mutex::new(HashMap::new()),
300 #[cfg(feature = "gpu-image")]
301 image_gray_pipeline: OnceLock::new(),
302 #[cfg(feature = "gpu-image")]
303 image_blur_pipeline: OnceLock::new(),
304 #[cfg(feature = "gpu-image")]
305 image_spatial_pipelines: OnceLock::new(),
306 #[cfg(feature = "gpu-image")]
307 image_ai_pack_pipeline: OnceLock::new(),
308 })
309 }
310}
311
312#[cfg(feature = "gpu-wgpu")]
313impl SpatialRuntime for WgpuRuntime {
314 fn device_kind(&self) -> DeviceKind {
315 DeviceKind::Wgpu
316 }
317}
318
319#[cfg(feature = "gpu-wgpu")]
320fn max_gather_channels_for_limit(storage_buffers_per_stage: u32) -> u32 {
321 if storage_buffers_per_stage >= MULTI_GATHER4_STORAGE_BUFFERS {
322 4
323 } else if storage_buffers_per_stage >= MULTI_GATHER2_STORAGE_BUFFERS {
324 2
325 } else {
326 1
327 }
328}
329
330#[cfg(feature = "gpu-wgpu")]
331#[cfg(not(target_arch = "wasm32"))]
332fn init_shared_runtime() -> Result<Arc<WgpuRuntime>, String> {
333 WgpuRuntime::new_headless().map(Arc::new).map_err(|error| error.to_string())
334}
335
336#[cfg(all(feature = "gpu-wgpu", test, not(target_arch = "wasm32")))]
337mod tests {
338 use super::WgpuRuntime;
339 use crate::pipeline_cache::ComputePipelineCache;
340 use std::sync::Arc;
341
342 #[test]
343 fn shared_runtime_is_singleton() {
344 let first = WgpuRuntime::shared().expect("shared runtime");
345 let second = WgpuRuntime::shared().expect("shared runtime");
346 assert!(Arc::ptr_eq(&first, &second));
347 }
348
349 #[test]
350 fn shared_and_headless_use_same_device_type() {
351 let shared = WgpuRuntime::shared().expect("shared runtime");
352 let local = WgpuRuntime::new_headless().expect("local runtime");
353 assert_eq!(
354 shared.device().limits().max_storage_buffers_per_shader_stage,
355 local.device().limits().max_storage_buffers_per_shader_stage
356 );
357 }
358
359 #[test]
360 fn pipeline_cache_is_initialized_once_per_runtime() {
361 let runtime = WgpuRuntime::new_headless().expect("wgpu runtime");
362 let first = runtime.pipelines() as *const ComputePipelineCache;
363 let second = runtime.pipelines() as *const ComputePipelineCache;
364 assert_eq!(first, second);
365 }
366
367 #[test]
368 fn adapter_limits_enable_multi_channel_gather() {
369 let runtime = WgpuRuntime::new_headless().expect("wgpu runtime");
370 let limit = runtime.device().limits().max_storage_buffers_per_shader_stage;
371 assert!(
372 limit >= super::MULTI_GATHER2_STORAGE_BUFFERS,
373 "expected at least {} storage buffers per stage, got {limit}",
374 super::MULTI_GATHER2_STORAGE_BUFFERS
375 );
376 assert!(runtime.max_gather_channels() >= 2);
377 assert_eq!(
378 runtime.max_gather_channels(),
379 runtime.pipelines().voxel_gather.multi_max_channels
380 );
381 }
382}