1use crate::kernels;
6use crate::oneshot;
7use flashraw_core::{Error, Result};
8use std::collections::HashMap;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::{Arc, Mutex, OnceLock};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum AdapterClass {
15 Discrete,
16 Integrated,
18 Virtual,
19 Software,
21 Unknown,
23}
24
25impl AdapterClass {
26 pub fn name(self) -> &'static str {
27 match self {
28 AdapterClass::Discrete => "discrete",
29 AdapterClass::Integrated => "integrated",
30 AdapterClass::Virtual => "virtual",
31 AdapterClass::Software => "software",
32 AdapterClass::Unknown => "unknown",
33 }
34 }
35}
36
37#[derive(Debug, Clone, PartialEq)]
39pub struct GpuAdapterInfo {
40 pub name: String,
41 pub backend: String,
42 pub driver: String,
43 pub class: AdapterClass,
44 pub timestamps: bool,
46 pub max_buffer_size: u64,
47 pub max_storage_binding: u64,
48}
49
50impl std::fmt::Display for GpuAdapterInfo {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 write!(f, "{} ({}, {})", self.name, self.backend, self.class.name())
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
58pub(crate) enum Kernel {
59 Unpack,
60 Normalize,
61 Develop,
62 DevelopHalf,
63 EdgeMosaic,
65 EdgeG0,
66 EdgeRgb,
67 EdgeBayerD1,
68 EdgeBayerV2,
69 EdgeXtransD1,
70 AhdGreen,
72 AhdLab,
73 AhdHom,
74 AhdBlend,
75 EdgeDevelop,
76}
77
78impl Kernel {
79 fn is_edge(self) -> bool {
80 matches!(
81 self,
82 Kernel::EdgeMosaic
83 | Kernel::EdgeG0
84 | Kernel::EdgeRgb
85 | Kernel::EdgeBayerD1
86 | Kernel::EdgeBayerV2
87 | Kernel::EdgeXtransD1
88 | Kernel::AhdGreen
89 | Kernel::AhdLab
90 | Kernel::AhdHom
91 | Kernel::AhdBlend
92 | Kernel::EdgeDevelop
93 )
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
99pub(crate) struct PipelineKey {
100 pub kernel: Kernel,
101 pub input_mode: u32,
102 pub order: u32,
103 pub use_lut: bool,
104 pub layout: u32,
106 pub out_mode: u32,
107}
108
109pub struct GpuContext {
115 pub(crate) adapter: Option<wgpu::Adapter>,
116 pub(crate) device: wgpu::Device,
117 pub(crate) queue: wgpu::Queue,
118 pub(crate) info: GpuAdapterInfo,
119 pub(crate) layout: wgpu::BindGroupLayout,
120 pipeline_layout: wgpu::PipelineLayout,
121 pub(crate) edge_layout: wgpu::BindGroupLayout,
123 edge_pipeline_layout: wgpu::PipelineLayout,
124 edge_tables: Mutex<HashMap<u32, Arc<wgpu::Buffer>>>,
126 pub(crate) tex_layout: wgpu::BindGroupLayout,
127 tex_pipeline: OnceLock<wgpu::ComputePipeline>,
128 modules: Mutex<HashMap<Kernel, wgpu::ShaderModule>>,
129 pipelines: Mutex<HashMap<PipelineKey, wgpu::ComputePipeline>>,
130 pub(crate) srgb: Vec<u32>,
131 pub(crate) last_error: Arc<Mutex<Option<String>>>,
132 pub(crate) calibration: OnceLock<crate::policy::GpuCalibration>,
133 pub(crate) uma: bool,
135 pool: Mutex<Vec<wgpu::Buffer>>,
137 binding_cap: AtomicU64,
139 buffer_cap: AtomicU64,
140}
141
142const POOL_MAX_BYTES: u64 = 1 << 30;
144
145impl std::fmt::Debug for GpuContext {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 f.debug_struct("GpuContext").field("adapter", &self.info).finish()
148 }
149}
150
151fn classify(t: wgpu::DeviceType) -> AdapterClass {
152 match t {
153 wgpu::DeviceType::DiscreteGpu => AdapterClass::Discrete,
154 wgpu::DeviceType::IntegratedGpu => AdapterClass::Integrated,
155 wgpu::DeviceType::VirtualGpu => AdapterClass::Virtual,
156 wgpu::DeviceType::Cpu => AdapterClass::Software,
157 wgpu::DeviceType::Other => AdapterClass::Unknown,
158 }
159}
160
161fn is_software_name(name: &str) -> bool {
162 let n = name.to_ascii_lowercase();
163 ["llvmpipe", "lavapipe", "softpipe", "swiftshader", "microsoft basic render", "warp"].iter().any(|s| n.contains(s))
164}
165
166fn storage_entry(binding: u32, read_only: bool) -> wgpu::BindGroupLayoutEntry {
167 wgpu::BindGroupLayoutEntry {
168 binding,
169 visibility: wgpu::ShaderStages::COMPUTE,
170 ty: wgpu::BindingType::Buffer {
171 ty: wgpu::BufferBindingType::Storage { read_only },
172 has_dynamic_offset: false,
173 min_binding_size: None,
174 },
175 count: None,
176 }
177}
178
179fn uniform_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
180 wgpu::BindGroupLayoutEntry {
181 binding,
182 visibility: wgpu::ShaderStages::COMPUTE,
183 ty: wgpu::BindingType::Buffer {
184 ty: wgpu::BufferBindingType::Uniform,
185 has_dynamic_offset: false,
186 min_binding_size: None,
187 },
188 count: None,
189 }
190}
191
192impl GpuContext {
193 pub async fn new() -> Result<Self> {
195 if disabled_by_env() {
196 return Err(Error::GpuUnavailable("disabled by `FLASHRAW_GPU`".into()));
197 }
198 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
199 let adapter = instance
200 .request_adapter(&wgpu::RequestAdapterOptions {
201 power_preference: wgpu::PowerPreference::HighPerformance,
202 force_fallback_adapter: false,
203 compatible_surface: None,
204 apply_limit_buckets: false,
205 })
206 .await
207 .map_err(|e| Error::GpuUnavailable(format!("no GPU adapter: {e}")))?;
208 let mut features = wgpu::Features::empty();
209 if adapter.features().contains(wgpu::Features::TIMESTAMP_QUERY) {
210 features |= wgpu::Features::TIMESTAMP_QUERY;
211 }
212 let integrated = adapter.get_info().device_type == wgpu::DeviceType::IntegratedGpu;
215 if integrated && adapter.features().contains(wgpu::Features::MAPPABLE_PRIMARY_BUFFERS) {
216 features |= wgpu::Features::MAPPABLE_PRIMARY_BUFFERS;
217 }
218 let limits = adapter.limits();
220 let (device, queue) = adapter
221 .request_device(&wgpu::DeviceDescriptor {
222 label: Some("flashraw"),
223 required_features: features,
224 required_limits: limits,
225 experimental_features: wgpu::ExperimentalFeatures::default(),
226 memory_hints: wgpu::MemoryHints::Performance,
227 trace: wgpu::Trace::Off,
228 })
229 .await
230 .map_err(|e| Error::GpuUnavailable(format!("device request failed: {e}")))?;
231 let info = adapter.get_info();
232 Ok(Self::build(Some(adapter), device, queue, &info))
233 }
234
235 pub fn from_device(device: wgpu::Device, queue: wgpu::Queue, info: &wgpu::AdapterInfo) -> Self {
239 Self::build(None, device, queue, info)
240 }
241
242 fn build(adapter: Option<wgpu::Adapter>, device: wgpu::Device, queue: wgpu::Queue, ai: &wgpu::AdapterInfo) -> Self {
243 let mut class = classify(ai.device_type);
244 if is_software_name(&ai.name) {
245 class = AdapterClass::Software;
246 }
247 let limits = device.limits();
248 let uma = device.features().contains(wgpu::Features::MAPPABLE_PRIMARY_BUFFERS);
249 let info = GpuAdapterInfo {
250 name: if ai.name.is_empty() { "WebGPU adapter".into() } else { ai.name.clone() },
251 backend: format!("{:?}", ai.backend),
252 driver: [ai.driver.as_str(), ai.driver_info.as_str()]
253 .iter()
254 .filter(|s| !s.is_empty())
255 .cloned()
256 .collect::<Vec<_>>()
257 .join(" "),
258 class,
259 timestamps: device.features().contains(wgpu::Features::TIMESTAMP_QUERY),
260 max_buffer_size: limits.max_buffer_size,
261 max_storage_binding: limits.max_storage_buffer_binding_size,
262 };
263 let last_error: Arc<Mutex<Option<String>>> = Arc::default();
264 {
265 let slot = last_error.clone();
268 device.on_uncaptured_error(Arc::new(move |e: wgpu::Error| {
269 if let Ok(mut s) = slot.lock() {
270 *s = Some(e.to_string());
271 }
272 }));
273 }
274 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
275 label: Some("flashraw.kernels"),
276 entries: &[
277 uniform_entry(0),
278 storage_entry(1, true),
279 storage_entry(2, true),
280 storage_entry(3, true),
281 storage_entry(4, false),
282 ],
283 });
284 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
285 label: Some("flashraw.kernels"),
286 bind_group_layouts: &[Some(&layout)],
287 immediate_size: 0,
288 });
289 let edge_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
290 label: Some("flashraw.edge"),
291 entries: &[uniform_entry(0), storage_entry(1, true), storage_entry(2, false), storage_entry(3, true)],
292 });
293 let edge_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
294 label: Some("flashraw.edge"),
295 bind_group_layouts: &[Some(&layout), Some(&edge_layout)],
296 immediate_size: 0,
297 });
298 let tex_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
299 label: Some("flashraw.to_texture"),
300 entries: &[
301 uniform_entry(0),
302 storage_entry(1, true),
303 wgpu::BindGroupLayoutEntry {
304 binding: 2,
305 visibility: wgpu::ShaderStages::COMPUTE,
306 ty: wgpu::BindingType::StorageTexture {
307 access: wgpu::StorageTextureAccess::WriteOnly,
308 format: wgpu::TextureFormat::Rgba8Unorm,
309 view_dimension: wgpu::TextureViewDimension::D2,
310 },
311 count: None,
312 },
313 ],
314 });
315 GpuContext {
316 adapter,
317 device,
318 queue,
319 info,
320 layout,
321 pipeline_layout,
322 edge_layout,
323 edge_pipeline_layout,
324 edge_tables: Mutex::default(),
325 tex_layout,
326 tex_pipeline: OnceLock::new(),
327 modules: Mutex::default(),
328 pipelines: Mutex::default(),
329 srgb: kernels::srgb_table(),
330 last_error,
331 calibration: OnceLock::new(),
332 uma,
333 pool: Mutex::default(),
334 binding_cap: AtomicU64::new(0),
335 buffer_cap: AtomicU64::new(0),
336 }
337 }
338
339 pub(crate) fn acquire(&self, label: &str, size: u64, usage: wgpu::BufferUsages) -> wgpu::Buffer {
342 let size = size.max(4);
343 {
344 let mut pool = self.pool.lock().unwrap_or_else(|e| e.into_inner());
345 let best = pool
346 .iter()
347 .enumerate()
348 .filter(|(_, b)| b.usage() == usage && b.size() >= size && b.size() <= size + size / 4 + (16 << 20))
349 .min_by_key(|(_, b)| b.size())
350 .map(|(i, _)| i);
351 if let Some(i) = best {
352 return pool.swap_remove(i);
353 }
354 }
355 let granule: u64 = if size > (64 << 20) { 4 << 20 } else { 256 };
356 let rounded = (size.div_ceil(granule) * granule).min(self.info.max_buffer_size & !3).max(size);
358 self.device.create_buffer(&wgpu::BufferDescriptor {
359 label: Some(label),
360 size: rounded,
361 usage,
362 mapped_at_creation: false,
363 })
364 }
365
366 pub(crate) fn release(&self, b: wgpu::Buffer) {
368 if b.size() < (1 << 20) {
369 return; }
371 let mut pool = self.pool.lock().unwrap_or_else(|e| e.into_inner());
372 pool.push(b);
373 let mut total: u64 = pool.iter().map(|b| b.size()).sum();
374 while total > POOL_MAX_BYTES && !pool.is_empty() {
375 let old = pool.remove(0);
376 total -= old.size();
377 old.destroy();
378 }
379 }
380
381 pub fn trim(&self) {
383 let mut pool = self.pool.lock().unwrap_or_else(|e| e.into_inner());
384 for b in pool.drain(..) {
385 b.destroy();
386 }
387 }
388
389 pub fn unified_memory(&self) -> bool {
391 self.uma
392 }
393
394 pub fn info(&self) -> &GpuAdapterInfo {
395 &self.info
396 }
397 pub fn device(&self) -> &wgpu::Device {
398 &self.device
399 }
400 pub fn queue(&self) -> &wgpu::Queue {
401 &self.queue
402 }
403 pub fn adapter(&self) -> Option<&wgpu::Adapter> {
405 self.adapter.as_ref()
406 }
407
408 fn module(&self, k: Kernel) -> wgpu::ShaderModule {
409 let mut m = self.modules.lock().unwrap_or_else(|e| e.into_inner());
410 m.entry(k)
411 .or_insert_with(|| {
412 let (label, src) = match k {
413 Kernel::Unpack => ("flashraw.unpack", kernels::UNPACK_WGSL),
414 Kernel::Normalize => ("flashraw.normalize", kernels::NORMALIZE_WGSL),
415 Kernel::Develop | Kernel::DevelopHalf => ("flashraw.develop", kernels::DEVELOP_WGSL),
416 k if k.is_edge() => ("flashraw.edge", kernels::EDGE_WGSL),
417 _ => unreachable!("every kernel has a module"),
418 };
419 self.device.create_shader_module(wgpu::ShaderModuleDescriptor {
420 label: Some(label),
421 source: wgpu::ShaderSource::Wgsl(src.into()),
422 })
423 })
424 .clone()
425 }
426
427 pub(crate) fn pipeline(&self, key: PipelineKey) -> wgpu::ComputePipeline {
429 if let Some(p) = self.pipelines.lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
430 return p.clone();
431 }
432 let module = self.module(if key.kernel.is_edge() { Kernel::EdgeMosaic } else { key.kernel });
434 let entry = match key.kernel {
435 Kernel::Unpack => "unpack",
436 Kernel::Normalize => "normalize",
437 Kernel::Develop => "develop",
438 Kernel::DevelopHalf => "develop_half",
439 Kernel::EdgeMosaic => "edge_mosaic",
440 Kernel::EdgeG0 => "edge_g0",
441 Kernel::EdgeRgb => "edge_rgb",
442 Kernel::EdgeBayerD1 => "edge_bayer_d1",
443 Kernel::EdgeBayerV2 => "edge_bayer_v2",
444 Kernel::EdgeXtransD1 => "edge_xtrans_d1",
445 Kernel::AhdGreen => "ahd_green",
446 Kernel::AhdLab => "ahd_lab",
447 Kernel::AhdHom => "ahd_hom",
448 Kernel::AhdBlend => "ahd_blend",
449 Kernel::EdgeDevelop => "develop_edge",
450 };
451 let mut constants: Vec<(&str, f64)> = vec![
452 ("INPUT_MODE", key.input_mode as f64),
453 ("ORDER", key.order as f64),
454 ("USE_LUT", if key.use_lut { 1.0 } else { 0.0 }),
455 ];
456 if matches!(key.kernel, Kernel::Develop | Kernel::DevelopHalf) || key.kernel.is_edge() {
457 constants.push(("LAYOUT", key.layout as f64));
458 constants.push(("OUT_MODE", key.out_mode as f64));
459 }
460 let p = self.device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
461 label: Some(entry),
462 layout: Some(if key.kernel.is_edge() { &self.edge_pipeline_layout } else { &self.pipeline_layout }),
463 module: &module,
464 entry_point: Some(entry),
465 compilation_options: wgpu::PipelineCompilationOptions {
466 constants: &constants,
467 zero_initialize_workgroup_memory: false,
469 },
470 cache: None,
471 });
472 self.pipelines.lock().unwrap_or_else(|e| e.into_inner()).insert(key, p.clone());
473 p
474 }
475
476 pub(crate) fn edge_table(&self, period: u32, build: impl FnOnce() -> Vec<f32>) -> Arc<wgpu::Buffer> {
480 let mut m = self.edge_tables.lock().unwrap_or_else(|e| e.into_inner());
481 m.entry(period)
482 .or_insert_with(|| {
483 Arc::new(crate::run::buffer_init(
484 &self.device,
485 "flashraw.edge_tables",
486 bytemuck::cast_slice(&build()),
487 wgpu::BufferUsages::STORAGE,
488 ))
489 })
490 .clone()
491 }
492
493 pub(crate) fn tex_pipeline(&self) -> &wgpu::ComputePipeline {
494 self.tex_pipeline.get_or_init(|| {
495 let module = self.device.create_shader_module(wgpu::ShaderModuleDescriptor {
496 label: Some("flashraw.to_texture"),
497 source: wgpu::ShaderSource::Wgsl(kernels::TO_TEXTURE_WGSL.into()),
498 });
499 let layout = self.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
500 label: Some("flashraw.to_texture"),
501 bind_group_layouts: &[Some(&self.tex_layout)],
502 immediate_size: 0,
503 });
504 self.device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
505 label: Some("to_texture"),
506 layout: Some(&layout),
507 module: &module,
508 entry_point: Some("to_texture"),
509 compilation_options: Default::default(),
510 cache: None,
511 })
512 })
513 }
514
515 pub async fn wait_idle(&self) -> Result<()> {
518 let (tx, rx) = oneshot::channel();
519 self.queue.on_submitted_work_done(move || tx.send(()));
520 #[cfg(not(target_arch = "wasm32"))]
521 {
522 self.device
523 .poll(wgpu::PollType::wait_indefinitely())
524 .map_err(|e| Error::GpuUnavailable(format!("device poll failed: {e}")))?;
525 if rx.try_take().is_some() {
526 return Ok(());
527 }
528 }
529 rx.await.ok_or_else(|| Error::GpuUnavailable("GPU work did not complete (device lost?)".into()))
530 }
531
532 pub(crate) async fn read_mapped<T>(
534 &self,
535 buffer: &wgpu::Buffer,
536 size: u64,
537 f: impl FnOnce(&[u8]) -> T,
538 ) -> Result<T> {
539 let (tx, rx) = oneshot::channel();
540 buffer.map_async(wgpu::MapMode::Read, ..size, move |r| tx.send(r));
541 #[cfg(not(target_arch = "wasm32"))]
542 self.device
543 .poll(wgpu::PollType::wait_indefinitely())
544 .map_err(|e| Error::GpuUnavailable(format!("device poll failed: {e}")))?;
545 let r = match rx.try_take() {
546 Some(r) => Some(r),
547 None => rx.await,
548 };
549 match r {
550 Some(Ok(())) => {}
551 Some(Err(e)) => return Err(Error::GpuUnavailable(format!("buffer mapping failed: {e}"))),
552 None => return Err(Error::GpuUnavailable("buffer mapping was cancelled".into())),
553 }
554 let out = {
555 let view = buffer
556 .get_mapped_range(..size)
557 .map_err(|e| Error::GpuUnavailable(format!("mapped range unavailable: {e}")))?;
558 f(&view)
559 };
560 buffer.unmap();
561 Ok(out)
562 }
563
564 pub(crate) async fn write_mapped(
571 &self,
572 buffer: &wgpu::Buffer,
573 data: &[u8],
574 after: Option<wgpu::SubmissionIndex>,
575 ) -> Result<f64> {
576 let size = (data.len() as u64).div_ceil(4).max(1) * 4;
577 let (tx, rx) = oneshot::channel();
578 buffer.map_async(wgpu::MapMode::Write, ..size, move |r| tx.send(r));
579 #[cfg(not(target_arch = "wasm32"))]
580 {
581 let poll = match after {
582 Some(i) => wgpu::PollType::Wait { submission_index: Some(i), timeout: None },
583 None => wgpu::PollType::Poll,
584 };
585 self.device.poll(poll).map_err(|e| Error::GpuUnavailable(format!("device poll failed: {e}")))?;
586 if !rx.is_ready() {
587 self.device
588 .poll(wgpu::PollType::wait_indefinitely())
589 .map_err(|e| Error::GpuUnavailable(format!("device poll failed: {e}")))?;
590 }
591 }
592 #[cfg(target_arch = "wasm32")]
593 let _ = after;
594 let r = match rx.try_take() {
595 Some(r) => Some(r),
596 None => rx.await,
597 };
598 match r {
599 Some(Ok(())) => {}
600 Some(Err(e)) => return Err(Error::GpuUnavailable(format!("buffer mapping failed: {e}"))),
601 None => return Err(Error::GpuUnavailable("buffer mapping was cancelled".into())),
602 }
603 let t0 = flashraw_core::profile::now_ms();
604 {
605 let mut view = buffer
606 .get_mapped_range_mut(..size)
607 .map_err(|e| Error::GpuUnavailable(format!("mapped range unavailable: {e}")))?;
608 let bulk = data.len();
609 view.slice(..bulk).copy_from_slice(data);
610 if (bulk as u64) < size {
611 view.slice(bulk..).fill(0);
612 }
613 }
614 buffer.unmap();
615 Ok(flashraw_core::profile::now_ms() - t0)
616 }
617
618 pub(crate) fn take_error(&self) -> Option<String> {
620 self.last_error.lock().ok()?.take()
621 }
622
623 pub fn override_limits(&self, max_binding: Option<u64>, max_buffer: Option<u64>) {
629 let v = |o: Option<u64>| o.map(|b| (b & !255).max(4096)).unwrap_or(0);
630 self.binding_cap.store(v(max_binding), Ordering::Relaxed);
631 self.buffer_cap.store(v(max_buffer), Ordering::Relaxed);
632 }
633
634 pub fn max_binding_bytes(&self) -> u64 {
636 let mut b = self.info.max_storage_binding.min(self.info.max_buffer_size).min(u32::MAX as u64 & !255);
637 let cap = self.binding_cap.load(Ordering::Relaxed);
638 if cap > 0 {
639 b = b.min(cap);
640 }
641 b.min(self.max_buffer_bytes())
642 }
643
644 pub fn max_buffer_bytes(&self) -> u64 {
646 let cap = self.buffer_cap.load(Ordering::Relaxed);
647 if cap > 0 { self.info.max_buffer_size.min(cap) } else { self.info.max_buffer_size }
648 }
649
650 pub fn estimate_bands(&self, input_bytes: u64, output_bytes: u64) -> u32 {
653 let b = self.max_binding_bytes().max(1);
654 let out = if output_bytes <= b { 1 } else { output_bytes.div_ceil(b) };
655 input_bytes.div_ceil(b).max(out).max(1).min(u32::MAX as u64) as u32
656 }
657}
658
659fn disabled_by_env() -> bool {
661 #[cfg(not(target_arch = "wasm32"))]
662 {
663 matches!(
664 std::env::var("FLASHRAW_GPU").map(|v| v.to_ascii_lowercase()).as_deref(),
665 Ok("0" | "off" | "false" | "none" | "cpu")
666 )
667 }
668 #[cfg(target_arch = "wasm32")]
669 {
670 false
671 }
672}
673
674#[derive(Default)]
677struct Slot {
678 ctx: Option<Arc<GpuContext>>,
679 error: Option<String>,
680 tried: bool,
681}
682
683#[cfg(not(target_arch = "wasm32"))]
684fn with_slot<T>(f: impl FnOnce(&mut Slot) -> T) -> T {
685 static SLOT: OnceLock<Mutex<Slot>> = OnceLock::new();
686 let mut g = SLOT.get_or_init(Default::default).lock().unwrap_or_else(|e| e.into_inner());
687 f(&mut g)
688}
689
690#[cfg(target_arch = "wasm32")]
691fn with_slot<T>(f: impl FnOnce(&mut Slot) -> T) -> T {
692 thread_local! { static SLOT: std::cell::RefCell<Slot> = Default::default(); }
694 SLOT.with(|s| f(&mut s.borrow_mut()))
695}
696
697pub async fn init() -> Result<Arc<GpuContext>> {
700 if let Some(r) = with_slot(|s| s.tried.then(|| s.ctx.clone().ok_or_else(|| s.error.clone().unwrap_or_default()))) {
701 return r.map_err(Error::GpuUnavailable);
702 }
703 let r = GpuContext::new().await.map(Arc::new);
704 with_slot(|s| {
705 if let Some(c) = &s.ctx {
706 return Ok(c.clone()); }
708 s.tried = true;
709 match r {
710 Ok(c) => {
711 s.ctx = Some(c.clone());
712 Ok(c)
713 }
714 Err(e) => {
715 s.error = Some(e.to_string());
716 Err(e)
717 }
718 }
719 })
720}
721
722pub fn context() -> Result<Arc<GpuContext>> {
725 #[cfg(not(target_arch = "wasm32"))]
726 {
727 if let Some(r) =
728 with_slot(|s| s.tried.then(|| s.ctx.clone().ok_or_else(|| s.error.clone().unwrap_or_default())))
729 {
730 return r.map_err(Error::GpuUnavailable);
731 }
732 pollster::block_on(init())
733 }
734 #[cfg(target_arch = "wasm32")]
735 {
736 with_slot(|s| s.ctx.clone())
737 .ok_or_else(|| Error::GpuUnavailable("call `flashraw_gpu::init().await` before using the GPU".into()))
738 }
739}
740
741pub fn try_context() -> Option<Arc<GpuContext>> {
743 with_slot(|s| s.ctx.clone())
744}
745
746pub fn set_context(ctx: Arc<GpuContext>) {
749 with_slot(|s| {
750 s.ctx = Some(ctx);
751 s.tried = true;
752 s.error = None;
753 })
754}
755
756pub fn available() -> bool {
758 context().is_ok()
759}
760
761pub fn adapter_info() -> Option<GpuAdapterInfo> {
763 context().ok().map(|c| c.info.clone())
764}