Skip to main content

gpu_handle_types/
gpu_context.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! `GpuContext` — host-provided GPU context. The same value can feed a
4//! decoder's, an encoder's and an interop layer's external-context
5//! constructor.
6//!
7//! `GpuContext` is **not `Clone`**. Share it with `Arc<GpuContext>` — the
8//! identity of the context is meaningful, and cloning a raw D3D11 device
9//! pointer without matching `AddRef` is a use-after-free waiting to
10//! happen. Wrapping in `Arc` gives reference semantics without a clone
11//! footgun.
12
13use std::ffi::c_void;
14
15use crate::{BackendKind, DeviceId};
16
17#[non_exhaustive]
18pub enum GpuContext {
19    Cpu,
20    OpenCl {
21        context: *mut c_void,
22        queue: *mut c_void,
23        device: *mut c_void,
24        /// Caller-asserted GL-sharing properties for `context`.
25        ///
26        /// When set, the caller asserts that `context` was created with
27        /// `CL_GL_CONTEXT_KHR` + the matching platform property
28        /// (`CL_WGL_HDC_KHR` on Windows desktop, `CL_GLX_DISPLAY_KHR`
29        /// on Linux X11, `CL_EGL_DISPLAY_KHR` on EGL/ANGLE,
30        /// `CL_CGL_SHAREGROUP_KHR` on macOS CGL) pointing at the GL
31        /// display + context recorded here. Required by the wgpu-GL
32        /// import path (`cl_khr_gl_sharing` route) — without it, the
33        /// downstream consumer would have to take the
34        /// `cl_khr_external_memory` cross-context fallback (with its
35        /// ~250–400 µs extra setup cost) or the explicit CPU-bounce
36        /// opt-in.
37        ///
38        /// `None` is correct when the caller's CL context is not
39        /// GL-shared (compute-only pipelines that never feed wgpu-GL).
40        /// SDK decoders that feed wgpu-GL can refuse to start with a
41        /// non-GL-shared CL context.
42        gl_sharing: Option<OpenClGlSharing>,
43    },
44    /// External CUDA context + stream owned by the caller.
45    ///
46    /// Lifetime contract: `context` and `stream` must remain valid for
47    /// the lifetime of every resource/handle minted through an interop
48    /// / decoder / encoder built on this context — including
49    /// `Drop`-time teardown, which may run on arbitrary threads long
50    /// after the mint. Downstream keep-alives capture the raw
51    /// `CUcontext` and re-bind it to destroy per-context tokens
52    /// (e.g. `CUsurfObject`); a context destroyed early cannot be
53    /// detected — the driver may recycle the address, resolving those
54    /// tokens against an unrelated context.
55    Cuda {
56        context: *mut c_void,
57        stream: *mut c_void,
58        device_id: i32,
59        uuid: Option<[u8; 16]>,
60    },
61    Metal {
62        device: *mut c_void,
63        queue: *mut c_void,
64        registry_id: Option<u64>,
65    },
66    D3D11 {
67        device: *mut c_void,
68        device_context: *mut c_void,
69        luid: Option<(i32, u32)>,
70    },
71    D3D12 {
72        device: *mut c_void,
73        queue: *mut c_void,
74        luid: Option<(i32, u32)>,
75    },
76    Vulkan {
77        instance: *mut c_void,
78        physical_device: *mut c_void,
79        device: *mut c_void,
80        queue: u64,
81        queue_family_index: u32,
82        uuid: Option<[u8; 16]>,
83    },
84    /// External OpenGL / OpenGL ES context owned by the caller.
85    ///
86    /// `display` / `context` are the caller's platform-native identifiers
87    /// — `EGLDisplay`+`EGLContext` on EGL, `HDC`+`HGLRC` on WGL,
88    /// `CGLContextObj` on CGL (with `display` mirroring `context`).
89    ///
90    /// `share_group` is an opaque identifier the caller uses to stamp
91    /// share-group identity. Two `OpenGL` contexts with the same non-
92    /// `None` `share_group` are assumed to share GL object names;
93    /// `None` means "solo context, no sharing guarantees". There is no
94    /// runtime query for this on EGL or WGL, so the value is purely
95    /// caller-supplied.
96    OpenGL {
97        display: *mut c_void,
98        context: *mut c_void,
99        share_group: Option<u64>,
100        backend: GlBackend,
101    },
102    /// Caller-supplied `wgpu` device, queue, and the adapter the device
103    /// was created from.
104    ///
105    /// `adapter` is load-bearing for per-format capability probes:
106    /// `wgpu::Adapter::get_texture_format_features(...)` is the only
107    /// reliable way to ask whether a given format supports
108    /// `STORAGE_BINDING`, `RENDER_ATTACHMENT`, etc. on the caller's
109    /// physical device. `wgpu::Device::features()` reports the
110    /// device-creation-time *requested* feature set, which is a strict
111    /// subset of what the adapter can actually do per format —
112    /// inadequate for the probe (e.g. on a desktop Vulkan adapter R8
113    /// storage is available without any feature flag, so `Device::features()`
114    /// can't tell you).
115    ///
116    /// External-context-mode factories run their per-format capability
117    /// detection against the borrowed `adapter`; the alternative —
118    /// enumerating fresh adapters from a new `wgpu::Instance` and
119    /// guessing which one matches the device — is incorrect on
120    /// multi-GPU systems, which is why this field exists.
121    #[cfg(feature = "wgpu")]
122    Wgpu {
123        device: std::sync::Arc<wgpu::Device>,
124        queue: std::sync::Arc<wgpu::Queue>,
125        adapter: std::sync::Arc<wgpu::Adapter>,
126    },
127
128    /// Caller-supplied `wgpu` device + queue running the **WebGPU**
129    /// backend (`wgpu::Backend::BrowserWebGpu`). wasm-only.
130    ///
131    /// `raw_device` is the caller's own `web_sys::GpuDevice`, retained so
132    /// same-device `GPUTexture` imports can be identity-compared
133    /// (`Object.is`) against it — the WebGPU spec exposes no
134    /// `GPUTexture.device` reflection. `None` on owned-device mode (we
135    /// created the device, the identity check is automatic); `Some` on
136    /// external-context mode (the host hands us their handle).
137    #[cfg(all(target_family = "wasm", feature = "wgpu", feature = "web"))]
138    WebGpu {
139        device: std::sync::Arc<wgpu::Device>,
140        queue: std::sync::Arc<wgpu::Queue>,
141        raw_device: Option<web_sys::GpuDevice>,
142    },
143
144    /// Caller-supplied `wgpu` device + queue running the **WebGL2**
145    /// backend (`wgpu::Backend::Gl` on wasm). wasm-only.
146    ///
147    /// `raw_context` is the caller's own `web_sys::WebGl2RenderingContext`,
148    /// retained so same-context `WebGLTexture` imports and exports can be
149    /// identity-compared against it. `None` on owned-device mode; `Some`
150    /// on external-context mode.
151    ///
152    /// Gated on `webgl` (not `web`): this variant exists to construct a
153    /// WebGL2-backed interop, which needs the wgpu WebGL accessors — merged
154    /// upstream, but in no wgpu release yet, so a git `[patch]` on wgpu trunk.
155    #[cfg(all(target_family = "wasm", feature = "wgpu", feature = "webgl"))]
156    WebGl {
157        device: std::sync::Arc<wgpu::Device>,
158        queue: std::sync::Arc<wgpu::Queue>,
159        raw_context: Option<web_sys::WebGl2RenderingContext>,
160    },
161}
162
163/// Caller-asserted GL-sharing handles for an `OpenCl` context.
164///
165/// Mirrors the (display, context, backend) triple from
166/// [`GpuContext::OpenGL`] — these are the same values the caller
167/// passed (or would pass) into `clCreateContext`'s
168/// `cl_context_properties[]` array as the values of
169/// `CL_GL_CONTEXT_KHR` and the matching platform property.
170///
171/// **No share-group**: `cl_khr_gl_sharing`'s property array binds the
172/// CL context to a specific (display, context) pair, not to a share
173/// group. Cross-share-group import is not standardised at the CL spec
174/// level — the right escape hatch is `cl_khr_external_memory` (an
175/// interop bridge layer's job), not a share-group field here.
176#[derive(Copy, Clone)]
177pub struct OpenClGlSharing {
178    pub display: *mut c_void,
179    pub context: *mut c_void,
180    pub backend: GlBackend,
181}
182
183impl core::fmt::Debug for OpenClGlSharing {
184    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
185        f.debug_struct("OpenClGlSharing").field("backend", &self.backend).finish_non_exhaustive()
186    }
187}
188
189// Same Send/Sync stance as the parent `GpuContext` — the handles are
190// opaque pointers + plain data, caller owns lifecycle.
191unsafe impl Send for OpenClGlSharing {}
192unsafe impl Sync for OpenClGlSharing {}
193
194/// Which GL context flavour [`GpuContext::OpenGL::context`] points at.
195///
196/// `Desktop` / `Egl` / `Angle(_)` distinguish which procedure-loader an
197/// interop layer uses (`wglGetProcAddress` + `opengl32.dll`, `eglGetProcAddress`,
198/// `dlsym` on the relevant `.framework`). `Web` is reserved for wasm /
199/// WebGL2 callers; cross-API interop isn't reachable there.
200#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
201#[non_exhaustive]
202pub enum GlBackend {
203    Desktop,
204    Egl,
205    Angle(AngleBackend),
206    Web,
207}
208
209/// Underlying implementation of an ANGLE "GL" context. Probed via
210/// `glGetString(GL_RENDERER)` + the matching `EGL_ANGLE_*` extension
211/// when `GlBackend::Angle(_)` is selected.
212#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
213#[non_exhaustive]
214pub enum AngleBackend {
215    Vulkan,
216    D3D11,
217    D3D12,
218    Metal,
219}
220
221/// Host-supplied scheduler that posts a closure onto the thread that
222/// owns a particular OpenGL context.
223///
224/// ## Why this exists
225///
226/// `GLuint` deletion (`glDeleteSemaphoresEXT`, `glDeleteTextures`, etc.)
227/// MUST run on a thread where the owning GL context is current — the GL
228/// namespace is thread-local and a delete dispatched against a foreign
229/// or null context is rejected as `GL_INVALID_OPERATION`. An interop
230/// layer's GL bridges (`wgpu-interop`'s, for instance) produce GL-side
231/// objects whose `Drop` may run on arbitrary threads in finalizer-driven
232/// hosts (JNI / .NET / GC-managed wrappers): producer-side
233/// `Arc<dyn SyncWaiter>` keep-alives, retained imports and process-wide
234/// producer-identity caches all hold strong references that are
235/// routinely released by threads with no GL context current.
236///
237/// Without an executor, those Drops can only queue the GL name onto a
238/// pending-delete bucket that is drained when:
239///
240/// - any thread that currently holds the matching GL context current
241///   reaches one of the GL-side bridge entry points (an opportunistic
242///   drain), or
243/// - the host explicitly asks the interop layer to prune its GL bridge
244///   objects from a thread that holds the matching context current.
245///
246/// Hosts whose pipeline cannot guarantee either path (e.g. a shutdown
247/// flow where the bridge layer has already stopped accepting work but
248/// the language-runtime GC keeps releasing the parked Arcs) should
249/// implement this trait and register an instance with the interop
250/// layer. Once registered, off-thread `Drop`s for that context are
251/// routed through the executor and run on the GL-owning thread directly.
252///
253/// ## Contract
254///
255/// `execute(target_ctx, task)` MUST eventually run `task` on a thread
256/// where the GL context identified by `target_ctx` is current. The
257/// implementation is allowed to:
258///
259/// - drop the closure unrun if the GL context is being torn down
260///   (the namespace dies with the context, so any leaked GL name is
261///   reclaimed); the only consequence is a small bump in the deferred-
262///   delete bucket until the next bridge call drains it.
263/// - run the closure synchronously when the calling thread already
264///   holds the right context current. The closure does not depend on
265///   any state outside its capture.
266///
267/// `target_ctx` is the raw `EGLContext` / `HGLRC` / `CGLContextObj`
268/// cast to `usize`. Hosts running a single GL thread can ignore the
269/// argument and post unconditionally; multi-context hosts use it to
270/// route to the correct thread/queue.
271pub trait GlContextExecutor: Send + Sync + 'static {
272    /// Schedule `task` to run on the GL-owning thread for the context
273    /// identified by `target_ctx`. See trait docs for the contract.
274    fn execute(&self, target_ctx: usize, task: Box<dyn FnOnce() + Send + 'static>);
275}
276
277// Caller asserts host platform permits cross-thread sharing.
278//
279// `Sync` is asserted on the same grounds as `Send`: once an external
280// caller hands us a `GpuContext`, the handles inside are immutable
281// pointers + plain data. Cross-thread access to those pointers is
282// safe — what is NOT safe is calling the *underlying device* from
283// multiple threads simultaneously, which is the caller's
284// responsibility regardless of our Sync stance. Without `Sync`,
285// `Arc<GpuContext>` would not be `Send`, breaking the `Send` invariant
286// of any decoder / encoder / resampler that holds an
287// `Option<Arc<GpuContext>>` field and must cross a thread handoff
288// (a prefetch thread, say).
289//
290// **wasm carve-out.** These blanket impls are `cfg(not(target_family =
291// "wasm"))`: on wasm the `WebGpu` / `WebGl` variants carry
292// `web_sys::GpuDevice` / `web_sys::WebGl2RenderingContext` (and
293// `Arc<wgpu::Device>`, which is itself `!Send` on wasm) — all
294// inherently thread-affine `JsValue` wrappers — so asserting `Send` /
295// `Sync` would be **unsound**. Removing the blanket impls leaves
296// `GpuContext` structurally `!Send + !Sync` on wasm (the raw-pointer
297// variants are `!Send` too). That is correct for the web target: it is
298// single-threaded, one interop instance per Worker, and such holders'
299// `Option<Arc<GpuContext>>` fields become `!Send` there as well — no
300// Rust thread handoff exists to break (cross-Worker handoff is
301// `postMessage` / `Transferable`, not thread movement). Native builds
302// keep `Send + Sync`.
303#[cfg(not(target_family = "wasm"))]
304unsafe impl Send for GpuContext {}
305#[cfg(not(target_family = "wasm"))]
306unsafe impl Sync for GpuContext {}
307
308impl GpuContext {
309    #[cfg(feature = "wgpu")]
310    pub fn from_wgpu(
311        device: std::sync::Arc<wgpu::Device>,
312        queue: std::sync::Arc<wgpu::Queue>,
313        adapter: std::sync::Arc<wgpu::Adapter>,
314    ) -> Self {
315        Self::Wgpu { device, queue, adapter }
316    }
317
318    pub fn backend(&self) -> BackendKind {
319        match self {
320            Self::Cpu => BackendKind::Cpu,
321            #[cfg(feature = "wgpu")]
322            Self::Wgpu { .. } => BackendKind::Wgpu,
323            // The web variants front a `wgpu::Device` (WebGPU / WebGL2
324            // backend); report `Wgpu` like the generic `Wgpu` context. `WebGl`
325            // is gated on `webgl`, `WebGpu` on `web`.
326            #[cfg(all(target_family = "wasm", feature = "wgpu", feature = "web"))]
327            Self::WebGpu { .. } => BackendKind::Wgpu,
328            #[cfg(all(target_family = "wasm", feature = "wgpu", feature = "webgl"))]
329            Self::WebGl { .. } => BackendKind::Wgpu,
330            #[cfg(feature = "opencl")]
331            Self::OpenCl { .. } => BackendKind::OpenCl,
332            #[cfg(not(feature = "opencl"))]
333            Self::OpenCl { .. } => BackendKind::Cpu,
334            #[cfg(feature = "cuda")]
335            Self::Cuda { .. } => BackendKind::Cuda,
336            #[cfg(not(feature = "cuda"))]
337            Self::Cuda { .. } => BackendKind::Cpu,
338            // Metal / D3D11 / D3D12 / Vulkan / OpenGL have no dedicated
339            // BackendKind (the wgpu backend fronts them all).
340            // Report Cpu when wgpu isn't compiled in; otherwise the host
341            // would have provided `Wgpu` for GPU work.
342            #[cfg(feature = "wgpu")]
343            Self::Metal { .. }
344            | Self::D3D11 { .. }
345            | Self::D3D12 { .. }
346            | Self::Vulkan { .. }
347            | Self::OpenGL { .. } => BackendKind::Wgpu,
348            #[cfg(not(feature = "wgpu"))]
349            Self::Metal { .. }
350            | Self::D3D11 { .. }
351            | Self::D3D12 { .. }
352            | Self::Vulkan { .. }
353            | Self::OpenGL { .. } => BackendKind::Cpu,
354        }
355    }
356
357    /// Returns `(cuCtx, cuStream)` for the `Cuda` variant; `Err`
358    /// otherwise. SDK decoders use this to avoid pattern-matching the
359    /// `GpuContext` enum at every call site.
360    pub fn cuda_context_and_stream(&self) -> Result<(*mut c_void, *mut c_void), GpuContextError> {
361        match self {
362            Self::Cuda { context, stream, .. } => Ok((*context, *stream)),
363            other => {
364                Err(GpuContextError::LaneMismatch { actual: other.backend(), requested: "cuda_context_and_stream" })
365            }
366        }
367    }
368
369    /// Returns `(cl_context, cl_command_queue)` for the `OpenCl`
370    /// variant; `Err` otherwise.
371    pub fn opencl_context_and_queue(&self) -> Result<(*mut c_void, *mut c_void), GpuContextError> {
372        match self {
373            Self::OpenCl { context, queue, .. } => Ok((*context, *queue)),
374            other => {
375                Err(GpuContextError::LaneMismatch { actual: other.backend(), requested: "opencl_context_and_queue" })
376            }
377        }
378    }
379
380    /// Returns the `MTLCommandQueue` handle for the `Metal` variant;
381    /// `Err` otherwise.
382    pub fn metal_command_queue(&self) -> Result<*mut c_void, GpuContextError> {
383        match self {
384            Self::Metal { queue, .. } => Ok(*queue),
385            other => Err(GpuContextError::LaneMismatch { actual: other.backend(), requested: "metal_command_queue" }),
386        }
387    }
388
389    /// Returns the `MTLDevice` handle for the `Metal` variant; `Err`
390    /// otherwise.
391    pub fn metal_device(&self) -> Result<*mut c_void, GpuContextError> {
392        match self {
393            Self::Metal { device, .. } => Ok(*device),
394            other => Err(GpuContextError::LaneMismatch { actual: other.backend(), requested: "metal_device" }),
395        }
396    }
397
398    /// CUDA device UUID for the `Cuda` variant — populated when the
399    /// caller filled the `uuid` field. Used to stamp
400    /// `GpuResource::CudaPtr2D` for cross-API import identity matching.
401    pub fn cuda_uuid(&self) -> Result<[u8; 16], GpuContextError> {
402        match self {
403            Self::Cuda { uuid: Some(u), .. } => Ok(*u),
404            Self::Cuda { uuid: None, .. } => Err(GpuContextError::MissingField("Cuda::uuid")),
405            other => Err(GpuContextError::LaneMismatch { actual: other.backend(), requested: "cuda_uuid" }),
406        }
407    }
408
409    /// CUDA device id (ordinal) for the `Cuda` variant.
410    pub fn cuda_device_id(&self) -> Result<i32, GpuContextError> {
411        match self {
412            Self::Cuda { device_id, .. } => Ok(*device_id),
413            other => Err(GpuContextError::LaneMismatch { actual: other.backend(), requested: "cuda_device_id" }),
414        }
415    }
416
417    /// Metal IORegistry id for the `Metal` variant — populated when
418    /// the caller filled the `registry_id` field.
419    pub fn metal_registry_id(&self) -> Result<u64, GpuContextError> {
420        match self {
421            Self::Metal { registry_id: Some(id), .. } => Ok(*id),
422            Self::Metal { registry_id: None, .. } => Err(GpuContextError::MissingField("Metal::registry_id")),
423            other => Err(GpuContextError::LaneMismatch { actual: other.backend(), requested: "metal_registry_id" }),
424        }
425    }
426
427    /// OpenCL `cl_device_id` for the `OpenCl` variant.
428    pub fn opencl_device(&self) -> Result<*mut c_void, GpuContextError> {
429        match self {
430            Self::OpenCl { device, .. } => Ok(*device),
431            other => Err(GpuContextError::LaneMismatch { actual: other.backend(), requested: "opencl_device" }),
432        }
433    }
434
435    /// GL-sharing handles for the `OpenCl` variant, if the caller set
436    /// them at construction. `Ok(None)` means the variant matches but
437    /// no sharing was declared; `Err(LaneMismatch)` means the caller
438    /// asked for OpenCL on a non-OpenCL context.
439    pub fn opencl_gl_sharing(&self) -> Result<Option<OpenClGlSharing>, GpuContextError> {
440        match self {
441            Self::OpenCl { gl_sharing, .. } => Ok(*gl_sharing),
442            other => Err(GpuContextError::LaneMismatch { actual: other.backend(), requested: "opencl_gl_sharing" }),
443        }
444    }
445
446    /// Stable identity of the underlying device, when the context carries
447    /// enough information to construct one. `Wgpu` returns `None`
448    /// here — wgpu-side device lookup belongs to the interop layer.
449    pub fn device_id(&self) -> Option<DeviceId> {
450        match self {
451            Self::Cpu => None,
452            Self::Cuda { uuid: Some(u), .. } => Some(DeviceId::CudaUuid(*u)),
453            Self::Cuda { .. } => None,
454            Self::D3D11 { luid: Some((h, l)), .. } => Some(DeviceId::DxgiLuid { high: *h, low: *l }),
455            Self::D3D12 { luid: Some((h, l)), .. } => Some(DeviceId::DxgiLuid { high: *h, low: *l }),
456            Self::D3D11 { .. } | Self::D3D12 { .. } => None,
457            Self::Metal { registry_id: Some(id), .. } => Some(DeviceId::MetalRegistryId(*id)),
458            Self::Metal { .. } => None,
459            Self::Vulkan { uuid: Some(u), .. } => Some(DeviceId::VulkanUuid(*u)),
460            Self::Vulkan { .. } => None,
461            Self::OpenCl { .. } => None, // Interop layer: platform/device string probe.
462            // OpenGL has no cross-driver stable device identity in core
463            // — `GL_EXT_memory_object` exposes `GL_DEVICE_UUID_EXT` but
464            // that's the underlying Vulkan/D3D UUID. Callers who need
465            // device identity for a GL context should pair with
466            // `GpuContext::Vulkan` / `::D3D12` on the same physical GPU.
467            Self::OpenGL { .. } => None,
468            #[cfg(feature = "wgpu")]
469            Self::Wgpu { .. } => None,
470            // Web device identity lives in the interop layer (raw
471            // `GpuDevice` / `WebGl2RenderingContext` JS-value identity),
472            // not in this stable `DeviceId` enum — same posture as `Wgpu`.
473            // `WebGl` is gated on `webgl`, `WebGpu` on `web`.
474            #[cfg(all(target_family = "wasm", feature = "wgpu", feature = "web"))]
475            Self::WebGpu { .. } => None,
476            #[cfg(all(target_family = "wasm", feature = "wgpu", feature = "webgl"))]
477            Self::WebGl { .. } => None,
478        }
479    }
480}
481
482/// Failure modes for the typed `GpuContext` unpackers above.
483///
484/// Errors here are caller-fault (mismatched lane, missing optional
485/// field) — they don't indicate device loss or runtime trouble.
486#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
487#[non_exhaustive]
488pub enum GpuContextError {
489    /// Caller asked for a per-lane accessor that doesn't match the
490    /// `GpuContext` variant the caller supplied (e.g. asking for
491    /// `cuda_context_and_stream` on a `Metal` context).
492    #[error("GpuContext lane {actual:?} cannot satisfy a {requested} accessor")]
493    LaneMismatch { actual: BackendKind, requested: &'static str },
494    /// The variant matched but an optional field (`uuid`,
495    /// `registry_id`, …) was left `None` by the caller.
496    #[error("optional field {0} not populated on this GpuContext")]
497    MissingField(&'static str),
498}
499
500impl core::fmt::Debug for GpuContext {
501    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
502        // Do not print raw pointers.
503        match self {
504            Self::Cpu => f.write_str("GpuContext::Cpu"),
505            Self::OpenCl { gl_sharing, .. } => {
506                f.debug_struct("OpenCl").field("gl_sharing", gl_sharing).finish_non_exhaustive()
507            }
508            Self::Cuda { device_id, uuid, .. } => {
509                f.debug_struct("Cuda").field("device_id", device_id).field("uuid", uuid).finish()
510            }
511            Self::Metal { registry_id, .. } => f.debug_struct("Metal").field("registry_id", registry_id).finish(),
512            Self::D3D11 { luid, .. } => f.debug_struct("D3D11").field("luid", luid).finish(),
513            Self::D3D12 { luid, .. } => f.debug_struct("D3D12").field("luid", luid).finish(),
514            Self::Vulkan { queue_family_index, uuid, .. } => {
515                f.debug_struct("Vulkan").field("queue_family_index", queue_family_index).field("uuid", uuid).finish()
516            }
517            Self::OpenGL { backend, share_group, .. } => {
518                f.debug_struct("OpenGL").field("backend", backend).field("share_group", share_group).finish()
519            }
520            #[cfg(feature = "wgpu")]
521            Self::Wgpu { .. } => f.write_str("GpuContext::Wgpu"),
522            #[cfg(all(target_family = "wasm", feature = "wgpu", feature = "web"))]
523            Self::WebGpu { raw_device, .. } => {
524                f.debug_struct("WebGpu").field("raw_device", &raw_device.is_some()).finish_non_exhaustive()
525            }
526            #[cfg(all(target_family = "wasm", feature = "wgpu", feature = "webgl"))]
527            Self::WebGl { raw_context, .. } => {
528                f.debug_struct("WebGl").field("raw_context", &raw_context.is_some()).finish_non_exhaustive()
529            }
530        }
531    }
532}