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 for the
32        /// `cl_khr_gl_sharing` route into a wgpu-GL device — without it, a
33        /// consumer has to fall back to `cl_khr_external_memory`
34        /// cross-context sharing (more setup per import) or a CPU copy.
35        ///
36        /// `None` is correct when the caller's CL context is not
37        /// GL-shared (compute-only work that never feeds wgpu-GL). A
38        /// consumer that feeds wgpu-GL may refuse a non-GL-shared CL
39        /// context.
40        gl_sharing: Option<OpenClGlSharing>,
41    },
42    /// External CUDA context + stream owned by the caller.
43    ///
44    /// Lifetime contract: `context` and `stream` must remain valid for
45    /// the lifetime of every resource/handle minted through an interop
46    /// / decoder / encoder built on this context — including
47    /// `Drop`-time teardown, which may run on arbitrary threads long
48    /// after the mint. Keep-alives minted against this context capture the
49    /// raw `CUcontext` and re-bind it to destroy per-context tokens
50    /// (e.g. `CUsurfObject`); a context destroyed early cannot be
51    /// detected — the driver may recycle the address, resolving those
52    /// tokens against an unrelated context.
53    Cuda {
54        context: *mut c_void,
55        stream: *mut c_void,
56        device_id: i32,
57        uuid: Option<[u8; 16]>,
58    },
59    Metal {
60        device: *mut c_void,
61        queue: *mut c_void,
62        registry_id: Option<u64>,
63    },
64    D3D11 {
65        device: *mut c_void,
66        device_context: *mut c_void,
67        luid: Option<(i32, u32)>,
68    },
69    D3D12 {
70        device: *mut c_void,
71        queue: *mut c_void,
72        luid: Option<(i32, u32)>,
73    },
74    Vulkan {
75        instance: *mut c_void,
76        physical_device: *mut c_void,
77        device: *mut c_void,
78        queue: u64,
79        queue_family_index: u32,
80        uuid: Option<[u8; 16]>,
81    },
82    /// External OpenGL / OpenGL ES context owned by the caller.
83    ///
84    /// `display` / `context` are the caller's platform-native identifiers
85    /// — `EGLDisplay`+`EGLContext` on EGL, `HDC`+`HGLRC` on WGL,
86    /// `CGLContextObj` on CGL (with `display` mirroring `context`).
87    ///
88    /// `share_group` is an opaque identifier the caller uses to stamp
89    /// share-group identity. Two `OpenGL` contexts with the same non-
90    /// `None` `share_group` are assumed to share GL object names;
91    /// `None` means "solo context, no sharing guarantees". There is no
92    /// runtime query for this on EGL or WGL, so the value is purely
93    /// caller-supplied.
94    OpenGL {
95        display: *mut c_void,
96        context: *mut c_void,
97        share_group: Option<u64>,
98        backend: GlBackend,
99    },
100    /// Caller-supplied `wgpu` device, queue, and the adapter the device
101    /// was created from.
102    ///
103    /// `adapter` is load-bearing for per-format capability probes:
104    /// `wgpu::Adapter::get_texture_format_features(...)` is the only
105    /// reliable way to ask whether a given format supports
106    /// `STORAGE_BINDING`, `RENDER_ATTACHMENT`, etc. on the caller's
107    /// physical device. `wgpu::Device::features()` reports the
108    /// device-creation-time *requested* feature set, which is a strict
109    /// subset of what the adapter can actually do per format —
110    /// inadequate for the probe (e.g. on a desktop Vulkan adapter R8
111    /// storage is available without any feature flag, so `Device::features()`
112    /// can't tell you).
113    ///
114    /// External-context-mode factories run their per-format capability
115    /// detection against the borrowed `adapter`; the alternative —
116    /// enumerating fresh adapters from a new `wgpu::Instance` and
117    /// guessing which one matches the device — is incorrect on
118    /// multi-GPU systems, which is why this field exists.
119    ///
120    /// The three wgpu handles are internally reference-counted, so the
121    /// caller passes clones of its own and keeps using them.
122    #[cfg(feature = "wgpu")]
123    Wgpu {
124        device: wgpu::Device,
125        queue: wgpu::Queue,
126        adapter: wgpu::Adapter,
127    },
128
129    /// Caller-supplied `wgpu` device + queue running the **WebGPU**
130    /// backend (`wgpu::Backend::BrowserWebGpu`). wasm-only.
131    ///
132    /// `raw_device` is the caller's own `web_sys::GpuDevice`, retained so
133    /// same-device `GPUTexture` imports can be identity-compared
134    /// (`Object.is`) against it — the WebGPU spec exposes no
135    /// `GPUTexture.device` reflection. `None` when the device was created
136    /// by the library this context is handed to (it already knows the
137    /// identity); `Some` when the host supplies a device of its own.
138    #[cfg(all(target_family = "wasm", feature = "wgpu", feature = "web"))]
139    WebGpu {
140        device: wgpu::Device,
141        queue: wgpu::Queue,
142        raw_device: Option<web_sys::GpuDevice>,
143    },
144
145    /// Caller-supplied `wgpu` device + queue running the **WebGL2**
146    /// backend (`wgpu::Backend::Gl` on wasm). wasm-only.
147    ///
148    /// `raw_context` is the caller's own `web_sys::WebGl2RenderingContext`,
149    /// retained so same-context `WebGLTexture` imports and exports can be
150    /// identity-compared against it. `None` when the device was created by
151    /// the library this context is handed to; `Some` when the host supplies
152    /// a context of its own.
153    ///
154    /// Gated on `webgl` rather than `web`, so a WebGPU-only build carries no
155    /// WebGL2 context variant.
156    #[cfg(all(target_family = "wasm", feature = "wgpu", feature = "webgl"))]
157    WebGl {
158        device: wgpu::Device,
159        queue: wgpu::Queue,
160        raw_context: Option<web_sys::WebGl2RenderingContext>,
161    },
162}
163
164/// Caller-asserted GL-sharing handles for an `OpenCl` context.
165///
166/// Mirrors the (display, context, backend) triple from
167/// [`GpuContext::OpenGL`] — these are the same values the caller
168/// passed (or would pass) into `clCreateContext`'s
169/// `cl_context_properties[]` array as the values of
170/// `CL_GL_CONTEXT_KHR` and the matching platform property.
171///
172/// **No share-group**: `cl_khr_gl_sharing`'s property array binds the
173/// CL context to a specific (display, context) pair, not to a share
174/// group. Cross-share-group import is not standardised at the CL spec
175/// level — the right escape hatch is `cl_khr_external_memory` (an
176/// interop bridge layer's job), not a share-group field here.
177#[derive(Copy, Clone)]
178pub struct OpenClGlSharing {
179    pub display: *mut c_void,
180    pub context: *mut c_void,
181    pub backend: GlBackend,
182}
183
184impl core::fmt::Debug for OpenClGlSharing {
185    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
186        f.debug_struct("OpenClGlSharing").field("backend", &self.backend).finish_non_exhaustive()
187    }
188}
189
190// Same Send/Sync stance as the parent `GpuContext` — the handles are
191// opaque pointers + plain data, caller owns lifecycle.
192unsafe impl Send for OpenClGlSharing {}
193unsafe impl Sync for OpenClGlSharing {}
194
195/// Which GL context flavour [`GpuContext::OpenGL::context`] points at.
196///
197/// `Desktop` / `Egl` / `Angle(_)` distinguish which procedure-loader an
198/// interop layer uses (`wglGetProcAddress` + `opengl32.dll`, `eglGetProcAddress`,
199/// `dlsym` on the relevant `.framework`). `Web` is reserved for wasm /
200/// WebGL2 callers; cross-API interop isn't reachable there.
201#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
202#[non_exhaustive]
203pub enum GlBackend {
204    Desktop,
205    Egl,
206    Angle(AngleBackend),
207    Web,
208}
209
210/// Underlying implementation of an ANGLE "GL" context. Probed via
211/// `glGetString(GL_RENDERER)` + the matching `EGL_ANGLE_*` extension
212/// when `GlBackend::Angle(_)` is selected.
213#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
214#[non_exhaustive]
215pub enum AngleBackend {
216    Vulkan,
217    D3D11,
218    D3D12,
219    Metal,
220}
221
222/// Host-supplied scheduler that posts a closure onto the thread that
223/// owns a particular OpenGL context.
224///
225/// ## Why this exists
226///
227/// `GLuint` deletion (`glDeleteSemaphoresEXT`, `glDeleteTextures`, etc.)
228/// MUST run on a thread where the owning GL context is current — the GL
229/// namespace is thread-local and a delete dispatched against a foreign
230/// or null context is rejected as `GL_INVALID_OPERATION`. An interop
231/// layer's GL bridges produce GL-side
232/// objects whose `Drop` may run on arbitrary threads in finalizer-driven
233/// hosts (JNI / .NET / GC-managed wrappers): producer-side
234/// `Arc<dyn SyncWaiter>` keep-alives, retained imports and process-wide
235/// producer-identity caches all hold strong references that are
236/// routinely released by threads with no GL context current.
237///
238/// Without an executor, those Drops can only queue the GL name onto a
239/// pending-delete bucket that is drained when:
240///
241/// - any thread that currently holds the matching GL context current
242///   reaches one of the GL-side bridge entry points (an opportunistic
243///   drain), or
244/// - the host explicitly asks the interop layer to prune its GL bridge
245///   objects from a thread that holds the matching context current.
246///
247/// Hosts whose pipeline cannot guarantee either path (e.g. a shutdown
248/// flow where the bridge layer has already stopped accepting work but
249/// the language-runtime GC keeps releasing the parked Arcs) should
250/// implement this trait and register an instance with the interop
251/// layer. Once registered, off-thread `Drop`s for that context are
252/// routed through the executor and run on the GL-owning thread directly.
253///
254/// ## Contract
255///
256/// `execute(target_ctx, task)` MUST eventually run `task` on a thread
257/// where the GL context identified by `target_ctx` is current. The
258/// implementation is allowed to:
259///
260/// - drop the closure unrun if the GL context is being torn down
261///   (the namespace dies with the context, so any leaked GL name is
262///   reclaimed); the only consequence is a small bump in the deferred-
263///   delete bucket until the next bridge call drains it.
264/// - run the closure synchronously when the calling thread already
265///   holds the right context current. The closure does not depend on
266///   any state outside its capture.
267///
268/// `target_ctx` is the raw `EGLContext` / `HGLRC` / `CGLContextObj`
269/// cast to `usize`. Hosts running a single GL thread can ignore the
270/// argument and post unconditionally; multi-context hosts use it to
271/// route to the correct thread/queue.
272pub trait GlContextExecutor: Send + Sync + 'static {
273    /// Schedule `task` to run on the GL-owning thread for the context
274    /// identified by `target_ctx`. See trait docs for the contract.
275    fn execute(&self, target_ctx: usize, task: Box<dyn FnOnce() + Send + 'static>);
276}
277
278// Caller asserts host platform permits cross-thread sharing.
279//
280// `Sync` is asserted on the same grounds as `Send`: once a `GpuContext`
281// is constructed, the handles inside are immutable
282// pointers + plain data. Cross-thread access to those pointers is
283// safe — what is NOT safe is calling the *underlying device* from
284// multiple threads simultaneously, which is the caller's
285// responsibility regardless of this type's `Sync` stance. Without `Sync`,
286// `Arc<GpuContext>` would not be `Send`, breaking the `Send` invariant
287// of any decoder / encoder / resampler that holds an
288// `Option<Arc<GpuContext>>` field and must cross a thread handoff
289// (a prefetch thread, say).
290//
291// **wasm carve-out.** These blanket impls are `cfg(not(target_family =
292// "wasm"))`: on wasm the `WebGpu` / `WebGl` variants carry
293// `web_sys::GpuDevice` / `web_sys::WebGl2RenderingContext` (and
294// `wgpu::Device`, which is itself `!Send` on wasm) — all
295// inherently thread-affine `JsValue` wrappers — so asserting `Send` /
296// `Sync` would be **unsound**. Removing the blanket impls leaves
297// `GpuContext` structurally `!Send + !Sync` on wasm (the raw-pointer
298// variants are `!Send` too). That is correct for the web target: it is
299// single-threaded, one interop instance per Worker, and such holders'
300// `Option<Arc<GpuContext>>` fields become `!Send` there as well — no
301// Rust thread handoff exists to break (cross-Worker handoff is
302// `postMessage` / `Transferable`, not thread movement). Native builds
303// keep `Send + Sync`.
304#[cfg(not(target_family = "wasm"))]
305unsafe impl Send for GpuContext {}
306#[cfg(not(target_family = "wasm"))]
307unsafe impl Sync for GpuContext {}
308
309impl GpuContext {
310    /// A [`GpuContext::Wgpu`] over the caller's device, queue and the
311    /// adapter the device was created from. The wgpu handles are
312    /// internally reference-counted; pass clones.
313    #[cfg(feature = "wgpu")]
314    pub fn from_wgpu(device: wgpu::Device, queue: wgpu::Queue, adapter: wgpu::Adapter) -> 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. Lets a CUDA consumer read the pair without
359    /// pattern-matching the `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}