gpu-handle-types 0.1.0

Typed, owned native GPU resource handles (Vulkan, D3D11/12, Metal, OpenGL, CUDA, OpenCL, DMA-BUF, IOSurface, AHardwareBuffer, WebGPU, ...), cross-API sync points and video pixel formats, for passing GPU resources between libraries.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
// SPDX-License-Identifier: MIT OR Apache-2.0

//! `GpuContext` — host-provided GPU context. The same value can feed a
//! decoder's, an encoder's and an interop layer's external-context
//! constructor.
//!
//! `GpuContext` is **not `Clone`**. Share it with `Arc<GpuContext>` — the
//! identity of the context is meaningful, and cloning a raw D3D11 device
//! pointer without matching `AddRef` is a use-after-free waiting to
//! happen. Wrapping in `Arc` gives reference semantics without a clone
//! footgun.

use std::ffi::c_void;

use crate::{BackendKind, DeviceId};

#[non_exhaustive]
pub enum GpuContext {
    Cpu,
    OpenCl {
        context: *mut c_void,
        queue: *mut c_void,
        device: *mut c_void,
        /// Caller-asserted GL-sharing properties for `context`.
        ///
        /// When set, the caller asserts that `context` was created with
        /// `CL_GL_CONTEXT_KHR` + the matching platform property
        /// (`CL_WGL_HDC_KHR` on Windows desktop, `CL_GLX_DISPLAY_KHR`
        /// on Linux X11, `CL_EGL_DISPLAY_KHR` on EGL/ANGLE,
        /// `CL_CGL_SHAREGROUP_KHR` on macOS CGL) pointing at the GL
        /// display + context recorded here. Required by the wgpu-GL
        /// import path (`cl_khr_gl_sharing` route) — without it, the
        /// downstream consumer would have to take the
        /// `cl_khr_external_memory` cross-context fallback (with its
        /// ~250–400 µs extra setup cost) or the explicit CPU-bounce
        /// opt-in.
        ///
        /// `None` is correct when the caller's CL context is not
        /// GL-shared (compute-only pipelines that never feed wgpu-GL).
        /// SDK decoders that feed wgpu-GL can refuse to start with a
        /// non-GL-shared CL context.
        gl_sharing: Option<OpenClGlSharing>,
    },
    /// External CUDA context + stream owned by the caller.
    ///
    /// Lifetime contract: `context` and `stream` must remain valid for
    /// the lifetime of every resource/handle minted through an interop
    /// / decoder / encoder built on this context — including
    /// `Drop`-time teardown, which may run on arbitrary threads long
    /// after the mint. Downstream keep-alives capture the raw
    /// `CUcontext` and re-bind it to destroy per-context tokens
    /// (e.g. `CUsurfObject`); a context destroyed early cannot be
    /// detected — the driver may recycle the address, resolving those
    /// tokens against an unrelated context.
    Cuda {
        context: *mut c_void,
        stream: *mut c_void,
        device_id: i32,
        uuid: Option<[u8; 16]>,
    },
    Metal {
        device: *mut c_void,
        queue: *mut c_void,
        registry_id: Option<u64>,
    },
    D3D11 {
        device: *mut c_void,
        device_context: *mut c_void,
        luid: Option<(i32, u32)>,
    },
    D3D12 {
        device: *mut c_void,
        queue: *mut c_void,
        luid: Option<(i32, u32)>,
    },
    Vulkan {
        instance: *mut c_void,
        physical_device: *mut c_void,
        device: *mut c_void,
        queue: u64,
        queue_family_index: u32,
        uuid: Option<[u8; 16]>,
    },
    /// External OpenGL / OpenGL ES context owned by the caller.
    ///
    /// `display` / `context` are the caller's platform-native identifiers
    /// — `EGLDisplay`+`EGLContext` on EGL, `HDC`+`HGLRC` on WGL,
    /// `CGLContextObj` on CGL (with `display` mirroring `context`).
    ///
    /// `share_group` is an opaque identifier the caller uses to stamp
    /// share-group identity. Two `OpenGL` contexts with the same non-
    /// `None` `share_group` are assumed to share GL object names;
    /// `None` means "solo context, no sharing guarantees". There is no
    /// runtime query for this on EGL or WGL, so the value is purely
    /// caller-supplied.
    OpenGL {
        display: *mut c_void,
        context: *mut c_void,
        share_group: Option<u64>,
        backend: GlBackend,
    },
    /// Caller-supplied `wgpu` device, queue, and the adapter the device
    /// was created from.
    ///
    /// `adapter` is load-bearing for per-format capability probes:
    /// `wgpu::Adapter::get_texture_format_features(...)` is the only
    /// reliable way to ask whether a given format supports
    /// `STORAGE_BINDING`, `RENDER_ATTACHMENT`, etc. on the caller's
    /// physical device. `wgpu::Device::features()` reports the
    /// device-creation-time *requested* feature set, which is a strict
    /// subset of what the adapter can actually do per format —
    /// inadequate for the probe (e.g. on a desktop Vulkan adapter R8
    /// storage is available without any feature flag, so `Device::features()`
    /// can't tell you).
    ///
    /// External-context-mode factories run their per-format capability
    /// detection against the borrowed `adapter`; the alternative —
    /// enumerating fresh adapters from a new `wgpu::Instance` and
    /// guessing which one matches the device — is incorrect on
    /// multi-GPU systems, which is why this field exists.
    #[cfg(feature = "wgpu")]
    Wgpu {
        device: std::sync::Arc<wgpu::Device>,
        queue: std::sync::Arc<wgpu::Queue>,
        adapter: std::sync::Arc<wgpu::Adapter>,
    },

    /// Caller-supplied `wgpu` device + queue running the **WebGPU**
    /// backend (`wgpu::Backend::BrowserWebGpu`). wasm-only.
    ///
    /// `raw_device` is the caller's own `web_sys::GpuDevice`, retained so
    /// same-device `GPUTexture` imports can be identity-compared
    /// (`Object.is`) against it — the WebGPU spec exposes no
    /// `GPUTexture.device` reflection. `None` on owned-device mode (we
    /// created the device, the identity check is automatic); `Some` on
    /// external-context mode (the host hands us their handle).
    #[cfg(all(target_family = "wasm", feature = "wgpu", feature = "web"))]
    WebGpu {
        device: std::sync::Arc<wgpu::Device>,
        queue: std::sync::Arc<wgpu::Queue>,
        raw_device: Option<web_sys::GpuDevice>,
    },

    /// Caller-supplied `wgpu` device + queue running the **WebGL2**
    /// backend (`wgpu::Backend::Gl` on wasm). wasm-only.
    ///
    /// `raw_context` is the caller's own `web_sys::WebGl2RenderingContext`,
    /// retained so same-context `WebGLTexture` imports and exports can be
    /// identity-compared against it. `None` on owned-device mode; `Some`
    /// on external-context mode.
    ///
    /// Gated on `webgl` (not `web`): this variant exists to construct a
    /// WebGL2-backed interop, which needs the wgpu WebGL accessors — merged
    /// upstream, but in no wgpu release yet, so a git `[patch]` on wgpu trunk.
    #[cfg(all(target_family = "wasm", feature = "wgpu", feature = "webgl"))]
    WebGl {
        device: std::sync::Arc<wgpu::Device>,
        queue: std::sync::Arc<wgpu::Queue>,
        raw_context: Option<web_sys::WebGl2RenderingContext>,
    },
}

/// Caller-asserted GL-sharing handles for an `OpenCl` context.
///
/// Mirrors the (display, context, backend) triple from
/// [`GpuContext::OpenGL`] — these are the same values the caller
/// passed (or would pass) into `clCreateContext`'s
/// `cl_context_properties[]` array as the values of
/// `CL_GL_CONTEXT_KHR` and the matching platform property.
///
/// **No share-group**: `cl_khr_gl_sharing`'s property array binds the
/// CL context to a specific (display, context) pair, not to a share
/// group. Cross-share-group import is not standardised at the CL spec
/// level — the right escape hatch is `cl_khr_external_memory` (an
/// interop bridge layer's job), not a share-group field here.
#[derive(Copy, Clone)]
pub struct OpenClGlSharing {
    pub display: *mut c_void,
    pub context: *mut c_void,
    pub backend: GlBackend,
}

impl core::fmt::Debug for OpenClGlSharing {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("OpenClGlSharing").field("backend", &self.backend).finish_non_exhaustive()
    }
}

// Same Send/Sync stance as the parent `GpuContext` — the handles are
// opaque pointers + plain data, caller owns lifecycle.
unsafe impl Send for OpenClGlSharing {}
unsafe impl Sync for OpenClGlSharing {}

/// Which GL context flavour [`GpuContext::OpenGL::context`] points at.
///
/// `Desktop` / `Egl` / `Angle(_)` distinguish which procedure-loader an
/// interop layer uses (`wglGetProcAddress` + `opengl32.dll`, `eglGetProcAddress`,
/// `dlsym` on the relevant `.framework`). `Web` is reserved for wasm /
/// WebGL2 callers; cross-API interop isn't reachable there.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum GlBackend {
    Desktop,
    Egl,
    Angle(AngleBackend),
    Web,
}

/// Underlying implementation of an ANGLE "GL" context. Probed via
/// `glGetString(GL_RENDERER)` + the matching `EGL_ANGLE_*` extension
/// when `GlBackend::Angle(_)` is selected.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum AngleBackend {
    Vulkan,
    D3D11,
    D3D12,
    Metal,
}

/// Host-supplied scheduler that posts a closure onto the thread that
/// owns a particular OpenGL context.
///
/// ## Why this exists
///
/// `GLuint` deletion (`glDeleteSemaphoresEXT`, `glDeleteTextures`, etc.)
/// MUST run on a thread where the owning GL context is current — the GL
/// namespace is thread-local and a delete dispatched against a foreign
/// or null context is rejected as `GL_INVALID_OPERATION`. An interop
/// layer's GL bridges (`wgpu-interop`'s, for instance) produce GL-side
/// objects whose `Drop` may run on arbitrary threads in finalizer-driven
/// hosts (JNI / .NET / GC-managed wrappers): producer-side
/// `Arc<dyn SyncWaiter>` keep-alives, retained imports and process-wide
/// producer-identity caches all hold strong references that are
/// routinely released by threads with no GL context current.
///
/// Without an executor, those Drops can only queue the GL name onto a
/// pending-delete bucket that is drained when:
///
/// - any thread that currently holds the matching GL context current
///   reaches one of the GL-side bridge entry points (an opportunistic
///   drain), or
/// - the host explicitly asks the interop layer to prune its GL bridge
///   objects from a thread that holds the matching context current.
///
/// Hosts whose pipeline cannot guarantee either path (e.g. a shutdown
/// flow where the bridge layer has already stopped accepting work but
/// the language-runtime GC keeps releasing the parked Arcs) should
/// implement this trait and register an instance with the interop
/// layer. Once registered, off-thread `Drop`s for that context are
/// routed through the executor and run on the GL-owning thread directly.
///
/// ## Contract
///
/// `execute(target_ctx, task)` MUST eventually run `task` on a thread
/// where the GL context identified by `target_ctx` is current. The
/// implementation is allowed to:
///
/// - drop the closure unrun if the GL context is being torn down
///   (the namespace dies with the context, so any leaked GL name is
///   reclaimed); the only consequence is a small bump in the deferred-
///   delete bucket until the next bridge call drains it.
/// - run the closure synchronously when the calling thread already
///   holds the right context current. The closure does not depend on
///   any state outside its capture.
///
/// `target_ctx` is the raw `EGLContext` / `HGLRC` / `CGLContextObj`
/// cast to `usize`. Hosts running a single GL thread can ignore the
/// argument and post unconditionally; multi-context hosts use it to
/// route to the correct thread/queue.
pub trait GlContextExecutor: Send + Sync + 'static {
    /// Schedule `task` to run on the GL-owning thread for the context
    /// identified by `target_ctx`. See trait docs for the contract.
    fn execute(&self, target_ctx: usize, task: Box<dyn FnOnce() + Send + 'static>);
}

// Caller asserts host platform permits cross-thread sharing.
//
// `Sync` is asserted on the same grounds as `Send`: once an external
// caller hands us a `GpuContext`, the handles inside are immutable
// pointers + plain data. Cross-thread access to those pointers is
// safe — what is NOT safe is calling the *underlying device* from
// multiple threads simultaneously, which is the caller's
// responsibility regardless of our Sync stance. Without `Sync`,
// `Arc<GpuContext>` would not be `Send`, breaking the `Send` invariant
// of any decoder / encoder / resampler that holds an
// `Option<Arc<GpuContext>>` field and must cross a thread handoff
// (a prefetch thread, say).
//
// **wasm carve-out.** These blanket impls are `cfg(not(target_family =
// "wasm"))`: on wasm the `WebGpu` / `WebGl` variants carry
// `web_sys::GpuDevice` / `web_sys::WebGl2RenderingContext` (and
// `Arc<wgpu::Device>`, which is itself `!Send` on wasm) — all
// inherently thread-affine `JsValue` wrappers — so asserting `Send` /
// `Sync` would be **unsound**. Removing the blanket impls leaves
// `GpuContext` structurally `!Send + !Sync` on wasm (the raw-pointer
// variants are `!Send` too). That is correct for the web target: it is
// single-threaded, one interop instance per Worker, and such holders'
// `Option<Arc<GpuContext>>` fields become `!Send` there as well — no
// Rust thread handoff exists to break (cross-Worker handoff is
// `postMessage` / `Transferable`, not thread movement). Native builds
// keep `Send + Sync`.
#[cfg(not(target_family = "wasm"))]
unsafe impl Send for GpuContext {}
#[cfg(not(target_family = "wasm"))]
unsafe impl Sync for GpuContext {}

impl GpuContext {
    #[cfg(feature = "wgpu")]
    pub fn from_wgpu(
        device: std::sync::Arc<wgpu::Device>,
        queue: std::sync::Arc<wgpu::Queue>,
        adapter: std::sync::Arc<wgpu::Adapter>,
    ) -> Self {
        Self::Wgpu { device, queue, adapter }
    }

    pub fn backend(&self) -> BackendKind {
        match self {
            Self::Cpu => BackendKind::Cpu,
            #[cfg(feature = "wgpu")]
            Self::Wgpu { .. } => BackendKind::Wgpu,
            // The web variants front a `wgpu::Device` (WebGPU / WebGL2
            // backend); report `Wgpu` like the generic `Wgpu` context. `WebGl`
            // is gated on `webgl`, `WebGpu` on `web`.
            #[cfg(all(target_family = "wasm", feature = "wgpu", feature = "web"))]
            Self::WebGpu { .. } => BackendKind::Wgpu,
            #[cfg(all(target_family = "wasm", feature = "wgpu", feature = "webgl"))]
            Self::WebGl { .. } => BackendKind::Wgpu,
            #[cfg(feature = "opencl")]
            Self::OpenCl { .. } => BackendKind::OpenCl,
            #[cfg(not(feature = "opencl"))]
            Self::OpenCl { .. } => BackendKind::Cpu,
            #[cfg(feature = "cuda")]
            Self::Cuda { .. } => BackendKind::Cuda,
            #[cfg(not(feature = "cuda"))]
            Self::Cuda { .. } => BackendKind::Cpu,
            // Metal / D3D11 / D3D12 / Vulkan / OpenGL have no dedicated
            // BackendKind (the wgpu backend fronts them all).
            // Report Cpu when wgpu isn't compiled in; otherwise the host
            // would have provided `Wgpu` for GPU work.
            #[cfg(feature = "wgpu")]
            Self::Metal { .. }
            | Self::D3D11 { .. }
            | Self::D3D12 { .. }
            | Self::Vulkan { .. }
            | Self::OpenGL { .. } => BackendKind::Wgpu,
            #[cfg(not(feature = "wgpu"))]
            Self::Metal { .. }
            | Self::D3D11 { .. }
            | Self::D3D12 { .. }
            | Self::Vulkan { .. }
            | Self::OpenGL { .. } => BackendKind::Cpu,
        }
    }

    /// Returns `(cuCtx, cuStream)` for the `Cuda` variant; `Err`
    /// otherwise. SDK decoders use this to avoid pattern-matching the
    /// `GpuContext` enum at every call site.
    pub fn cuda_context_and_stream(&self) -> Result<(*mut c_void, *mut c_void), GpuContextError> {
        match self {
            Self::Cuda { context, stream, .. } => Ok((*context, *stream)),
            other => {
                Err(GpuContextError::LaneMismatch { actual: other.backend(), requested: "cuda_context_and_stream" })
            }
        }
    }

    /// Returns `(cl_context, cl_command_queue)` for the `OpenCl`
    /// variant; `Err` otherwise.
    pub fn opencl_context_and_queue(&self) -> Result<(*mut c_void, *mut c_void), GpuContextError> {
        match self {
            Self::OpenCl { context, queue, .. } => Ok((*context, *queue)),
            other => {
                Err(GpuContextError::LaneMismatch { actual: other.backend(), requested: "opencl_context_and_queue" })
            }
        }
    }

    /// Returns the `MTLCommandQueue` handle for the `Metal` variant;
    /// `Err` otherwise.
    pub fn metal_command_queue(&self) -> Result<*mut c_void, GpuContextError> {
        match self {
            Self::Metal { queue, .. } => Ok(*queue),
            other => Err(GpuContextError::LaneMismatch { actual: other.backend(), requested: "metal_command_queue" }),
        }
    }

    /// Returns the `MTLDevice` handle for the `Metal` variant; `Err`
    /// otherwise.
    pub fn metal_device(&self) -> Result<*mut c_void, GpuContextError> {
        match self {
            Self::Metal { device, .. } => Ok(*device),
            other => Err(GpuContextError::LaneMismatch { actual: other.backend(), requested: "metal_device" }),
        }
    }

    /// CUDA device UUID for the `Cuda` variant — populated when the
    /// caller filled the `uuid` field. Used to stamp
    /// `GpuResource::CudaPtr2D` for cross-API import identity matching.
    pub fn cuda_uuid(&self) -> Result<[u8; 16], GpuContextError> {
        match self {
            Self::Cuda { uuid: Some(u), .. } => Ok(*u),
            Self::Cuda { uuid: None, .. } => Err(GpuContextError::MissingField("Cuda::uuid")),
            other => Err(GpuContextError::LaneMismatch { actual: other.backend(), requested: "cuda_uuid" }),
        }
    }

    /// CUDA device id (ordinal) for the `Cuda` variant.
    pub fn cuda_device_id(&self) -> Result<i32, GpuContextError> {
        match self {
            Self::Cuda { device_id, .. } => Ok(*device_id),
            other => Err(GpuContextError::LaneMismatch { actual: other.backend(), requested: "cuda_device_id" }),
        }
    }

    /// Metal IORegistry id for the `Metal` variant — populated when
    /// the caller filled the `registry_id` field.
    pub fn metal_registry_id(&self) -> Result<u64, GpuContextError> {
        match self {
            Self::Metal { registry_id: Some(id), .. } => Ok(*id),
            Self::Metal { registry_id: None, .. } => Err(GpuContextError::MissingField("Metal::registry_id")),
            other => Err(GpuContextError::LaneMismatch { actual: other.backend(), requested: "metal_registry_id" }),
        }
    }

    /// OpenCL `cl_device_id` for the `OpenCl` variant.
    pub fn opencl_device(&self) -> Result<*mut c_void, GpuContextError> {
        match self {
            Self::OpenCl { device, .. } => Ok(*device),
            other => Err(GpuContextError::LaneMismatch { actual: other.backend(), requested: "opencl_device" }),
        }
    }

    /// GL-sharing handles for the `OpenCl` variant, if the caller set
    /// them at construction. `Ok(None)` means the variant matches but
    /// no sharing was declared; `Err(LaneMismatch)` means the caller
    /// asked for OpenCL on a non-OpenCL context.
    pub fn opencl_gl_sharing(&self) -> Result<Option<OpenClGlSharing>, GpuContextError> {
        match self {
            Self::OpenCl { gl_sharing, .. } => Ok(*gl_sharing),
            other => Err(GpuContextError::LaneMismatch { actual: other.backend(), requested: "opencl_gl_sharing" }),
        }
    }

    /// Stable identity of the underlying device, when the context carries
    /// enough information to construct one. `Wgpu` returns `None`
    /// here — wgpu-side device lookup belongs to the interop layer.
    pub fn device_id(&self) -> Option<DeviceId> {
        match self {
            Self::Cpu => None,
            Self::Cuda { uuid: Some(u), .. } => Some(DeviceId::CudaUuid(*u)),
            Self::Cuda { .. } => None,
            Self::D3D11 { luid: Some((h, l)), .. } => Some(DeviceId::DxgiLuid { high: *h, low: *l }),
            Self::D3D12 { luid: Some((h, l)), .. } => Some(DeviceId::DxgiLuid { high: *h, low: *l }),
            Self::D3D11 { .. } | Self::D3D12 { .. } => None,
            Self::Metal { registry_id: Some(id), .. } => Some(DeviceId::MetalRegistryId(*id)),
            Self::Metal { .. } => None,
            Self::Vulkan { uuid: Some(u), .. } => Some(DeviceId::VulkanUuid(*u)),
            Self::Vulkan { .. } => None,
            Self::OpenCl { .. } => None, // Interop layer: platform/device string probe.
            // OpenGL has no cross-driver stable device identity in core
            // — `GL_EXT_memory_object` exposes `GL_DEVICE_UUID_EXT` but
            // that's the underlying Vulkan/D3D UUID. Callers who need
            // device identity for a GL context should pair with
            // `GpuContext::Vulkan` / `::D3D12` on the same physical GPU.
            Self::OpenGL { .. } => None,
            #[cfg(feature = "wgpu")]
            Self::Wgpu { .. } => None,
            // Web device identity lives in the interop layer (raw
            // `GpuDevice` / `WebGl2RenderingContext` JS-value identity),
            // not in this stable `DeviceId` enum — same posture as `Wgpu`.
            // `WebGl` is gated on `webgl`, `WebGpu` on `web`.
            #[cfg(all(target_family = "wasm", feature = "wgpu", feature = "web"))]
            Self::WebGpu { .. } => None,
            #[cfg(all(target_family = "wasm", feature = "wgpu", feature = "webgl"))]
            Self::WebGl { .. } => None,
        }
    }
}

/// Failure modes for the typed `GpuContext` unpackers above.
///
/// Errors here are caller-fault (mismatched lane, missing optional
/// field) — they don't indicate device loss or runtime trouble.
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum GpuContextError {
    /// Caller asked for a per-lane accessor that doesn't match the
    /// `GpuContext` variant the caller supplied (e.g. asking for
    /// `cuda_context_and_stream` on a `Metal` context).
    #[error("GpuContext lane {actual:?} cannot satisfy a {requested} accessor")]
    LaneMismatch { actual: BackendKind, requested: &'static str },
    /// The variant matched but an optional field (`uuid`,
    /// `registry_id`, …) was left `None` by the caller.
    #[error("optional field {0} not populated on this GpuContext")]
    MissingField(&'static str),
}

impl core::fmt::Debug for GpuContext {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        // Do not print raw pointers.
        match self {
            Self::Cpu => f.write_str("GpuContext::Cpu"),
            Self::OpenCl { gl_sharing, .. } => {
                f.debug_struct("OpenCl").field("gl_sharing", gl_sharing).finish_non_exhaustive()
            }
            Self::Cuda { device_id, uuid, .. } => {
                f.debug_struct("Cuda").field("device_id", device_id).field("uuid", uuid).finish()
            }
            Self::Metal { registry_id, .. } => f.debug_struct("Metal").field("registry_id", registry_id).finish(),
            Self::D3D11 { luid, .. } => f.debug_struct("D3D11").field("luid", luid).finish(),
            Self::D3D12 { luid, .. } => f.debug_struct("D3D12").field("luid", luid).finish(),
            Self::Vulkan { queue_family_index, uuid, .. } => {
                f.debug_struct("Vulkan").field("queue_family_index", queue_family_index).field("uuid", uuid).finish()
            }
            Self::OpenGL { backend, share_group, .. } => {
                f.debug_struct("OpenGL").field("backend", backend).field("share_group", share_group).finish()
            }
            #[cfg(feature = "wgpu")]
            Self::Wgpu { .. } => f.write_str("GpuContext::Wgpu"),
            #[cfg(all(target_family = "wasm", feature = "wgpu", feature = "web"))]
            Self::WebGpu { raw_device, .. } => {
                f.debug_struct("WebGpu").field("raw_device", &raw_device.is_some()).finish_non_exhaustive()
            }
            #[cfg(all(target_family = "wasm", feature = "wgpu", feature = "webgl"))]
            Self::WebGl { raw_context, .. } => {
                f.debug_struct("WebGl").field("raw_context", &raw_context.is_some()).finish_non_exhaustive()
            }
        }
    }
}