gpu_handle_types/gpu_resource.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! `GpuResource` — the single source of truth for GPU resource handles
4//! crossing API boundaries. Used by decoders, encoders, interop layers and
5//! renderers.
6//!
7//! The variant payloads are **typed newtypes**
8//! (see [`handles`]). Each newtype owns its raw payload privately and
9//! exposes a single `try_from_raw` constructor that asserts invariants
10//! once. The enum here is a thin sum type over those newtypes.
11//!
12//! # Lifetime model
13//!
14//! Each newtype variant carries either:
15//! - `Option<Arc<dyn ResourceKeepAlive>>` — borrowed raw handle, lifetime
16//! anchored by the producer's `Arc` if attached.
17//! - An `Arc<...>` refcount wrapper — for OS-handle / refcounted-foreign
18//! payloads (`OwnedFd`, `OwnedHandle`, `IoSurfaceRetain`,
19//! `CvBufferRetain`, `AHbInner`). Clone bumps the Arc; last drop runs
20//! the wrapped destructor.
21//!
22//! Cloning a `GpuResource` is at most one atomic per attached Arc — no
23//! driver round-trip, no `CFRetain` / `AHardwareBuffer_acquire` per
24//! clone.
25//!
26//! # Thread safety
27//!
28//! `Send + Sync` is **auto-derived** on the enum (not a blanket
29//! `unsafe impl`). Each newtype carries its own `unsafe impl Send + Sync`
30//! documented on the type — see [`handles`]. If a future variant lands
31//! a non-`Send` payload, the compiler refuses to derive on the enum,
32//! forcing the author to either fix the payload or refactor consumers
33//! explicitly.
34//!
35//! **wasm carve-out.** On `target_family = "wasm"` `GpuResource` is
36//! `!Send + !Sync` on BOTH the non-atomics and threaded `atomics` targets.
37//! The pin that holds on both is the `wgpu` `WgpuTexture` variant's
38//! `Option<Arc<dyn ResourceKeepAlive>>` keep-alive: `ResourceKeepAlive`'s
39//! [`crate::MaybeSendSync`] supertrait is the *empty* marker on wasm, so
40//! that `Arc` trait object is `!Send + !Sync`, and the variant — unlike the
41//! native newtypes — carries no `unsafe impl` to override it. This is
42//! independent of the threading model and of whether a wgpu web backend is
43//! even compiled: with no backend selected, `wgpu::Texture` is an
44//! uninhabited, vacuously-`Send` dispatch enum, so it is the keep-alive
45//! field, not `wgpu::Texture`'s own auto-traits, that pins the variant.
46//!
47//! The `atomics` target adds a second, independent pin: the
48//! [`GpuResource::Web`] variant's `web_sys` payload is `!Send` there,
49//! because wasm-bindgen gates `unsafe impl Send/Sync for JsValue` on
50//! `cfg(not(target_feature = "atomics"))`. On the non-atomics target that
51//! same payload *is* `Send + Sync` (wasm-bindgen's fragile impl) — a
52//! property this crate deliberately does **not** rely on, which is exactly why the
53//! keep-alive pin above is written to stand on its own. The `Web` variant
54//! carries **no** `unsafe impl`, so it stays `!Send` on the atomics target;
55//! do not add one "for symmetry".
56//!
57//! This carve-out is intentional and non-restrictive: one interop instance
58//! per Worker, cross-Worker handoff via `postMessage` / `Transferable`s,
59//! never Rust thread movement.
60
61mod handles;
62
63pub use handles::*;
64
65/// Zero-copy GPU resource handle.
66///
67/// `#[non_exhaustive]`, so adding a variant is not a breaking change.
68#[derive(Clone, Debug)]
69#[non_exhaustive]
70pub enum GpuResource {
71 // ── D3D11 / D3D12 ─────────────────────────────────────────────────
72 D3D11Texture(D3D11Texture),
73 D3D12Resource(D3D12Resource),
74 D3D12BufferHandle(D3D12BufferHandle),
75
76 // ── Vulkan ────────────────────────────────────────────────────────
77 VkImage(VkImage),
78 VkBufferHandle(VkBufferHandle),
79 /// `VK_KHR_external_memory_fd` opaque-fd export (Linux / Android).
80 #[cfg(any(unix, target_os = "wasi", target_os = "hermit"))]
81 VkOpaqueFd(VkOpaqueFd),
82 /// `VK_KHR_external_memory_win32` opaque-NT-handle export (Windows).
83 #[cfg(windows)]
84 VkOpaqueWin32(VkOpaqueWin32),
85
86 // ── CUDA ──────────────────────────────────────────────────────────
87 CudaPtr2D(CudaPtr2D),
88 CudaBufferHandle(CudaBufferHandle),
89 /// Opaque CUDA `CUsurfObject` bound to a `CUarray` — a `surf2Dwrite`
90 /// kernel write target (typically a foreign-API texture imported as
91 /// a CUDA array). Gated on the `wgpu` feature because the handle
92 /// carries a `wgpu::TextureFormat`; see [`CudaSurface`].
93 #[cfg(feature = "wgpu")]
94 CudaSurface(CudaSurface),
95
96 // ── OpenCL ────────────────────────────────────────────────────────
97 OpenClMem(OpenClMem),
98
99 // ── Metal / Apple ─────────────────────────────────────────────────
100 MetalTextureHandle(MetalTextureHandle),
101 MetalBufferHandle(MetalBufferHandle),
102 #[cfg(target_vendor = "apple")]
103 IoSurface(IoSurface),
104 #[cfg(target_vendor = "apple")]
105 VideoToolboxFrame(VideoToolboxFrame),
106
107 // ── OpenGL ────────────────────────────────────────────────────────
108 GlTextureHandle(GlTextureHandle),
109 GlBufferHandle(GlBufferHandle),
110
111 // ── Linux ─────────────────────────────────────────────────────────
112 /// Bare Linux DMA-BUF descriptor.
113 #[cfg(any(target_os = "linux", target_os = "android"))]
114 DmaBufHandle(DmaBufHandle),
115 /// VA-API surface (`VASurfaceID` + its `VADisplay`).
116 #[cfg(target_os = "linux")]
117 VaapiSurface(VaapiSurface),
118
119 // ── Android ───────────────────────────────────────────────────────
120 #[cfg(target_os = "android")]
121 AHardwareBufferHandle(AHardwareBufferHandle),
122 #[cfg(target_os = "android")]
123 MediaCodecFrame(MediaCodecFrame),
124 #[cfg(target_os = "android")]
125 AImageFrame(AImageFrame),
126 #[cfg(target_os = "android")]
127 AndroidNativeWindowHandle(AndroidNativeWindowHandle),
128 #[cfg(target_os = "android")]
129 AndroidSurfaceControlHandle(AndroidSurfaceControlHandle),
130
131 // ── Windows NT handles ────────────────────────────────────────────
132 #[cfg(windows)]
133 NtHandle(NtHandle),
134 #[cfg(windows)]
135 KmtToken(KmtToken),
136
137 // ── CPU ───────────────────────────────────────────────────────────
138 CpuBytes(CpuBytes),
139 CpuPlanes(CpuPlaneSet),
140 CpuSharedSlot(CpuSharedSlot),
141
142 // ── Native wgpu (same-device passthrough) ─────────────────────────
143 /// A `wgpu::Texture` already on the consumer's own wgpu device — the
144 /// degenerate "nothing to import" case. The interop import path
145 /// returns the texture verbatim (no foreign-handle import, no copy).
146 /// Gated on the `wgpu` feature; see [`WgpuTexture`].
147 #[cfg(feature = "wgpu")]
148 WgpuTexture(WgpuTexture),
149
150 // ── Web (wasm) ────────────────────────────────────────────────────
151 /// Browser GPU / media handle (`GPUTexture`, `WebGLTexture`,
152 /// `<video>`, `VideoFrame`, …). wasm-only. See the module
153 /// `# Thread safety` note and [`crate::web::WebGpuResource`].
154 #[cfg(all(target_family = "wasm", feature = "web"))]
155 Web(crate::web::WebGpuResource),
156}
157
158// Compile-time enforcement of the wasm thread-affinity carve-out.
159// On wasm `GpuResource` is `!Send + !Sync`:
160// - with the `wgpu` feature (a default feature) the
161// `WgpuTexture` variant's `Option<Arc<dyn ResourceKeepAlive>>`
162// keep-alive is `!Send + !Sync` on BOTH wasm targets — its
163// `MaybeSendSync` bound is the empty marker on wasm — and the variant
164// has no `unsafe impl` to force it `Send`. This is the pin on the
165// *non-atomics* target (where the `Web` payload below is still `Send`),
166// which is why the assertion is gated on `wgpu`.
167// - on the threaded (`atomics`) target the `Web(WebGpuResource)`
168// variant's `web_sys` payload is additionally `!Send` (wasm-bindgen
169// gates `Send for JsValue` on non-atomics).
170// The guarantee therefore holds independent of the wasm threading model,
171// which is why nothing here relies on wgpu's
172// `fragile-send-sync-non-atomic-wasm`.
173#[cfg(all(target_family = "wasm", feature = "web", feature = "wgpu"))]
174static_assertions::assert_not_impl_any!(GpuResource: Send, Sync);
175
176/// Up-to-four-plane CPU buffer set. `count` ∈ `1..=4`; unused slots are
177/// `None`. Zero heap allocation — the `[Option<CpuPlane>; 4]` is inline.
178#[derive(Debug, Copy, Clone)]
179pub struct CpuPlaneSet {
180 pub format: crate::PixelFormat,
181 pub count: u8,
182 pub planes: [Option<CpuPlane>; 4],
183}
184
185#[derive(Debug, Copy, Clone)]
186pub struct CpuPlane {
187 pub data: *mut u8,
188 pub size: usize,
189 /// Row pitch in bytes.
190 pub row_pitch: u32,
191}
192
193// SAFETY: caller-asserted contract on the byte ranges. Same shape as
194// the typed-newtype payloads in `handles`.
195unsafe impl Send for CpuPlaneSet {}
196unsafe impl Sync for CpuPlaneSet {}
197unsafe impl Send for CpuPlane {}
198unsafe impl Sync for CpuPlane {}
199
200// ─────────────────────────────────────────────────────────────────────────────
201// GL bind targets / formats
202// ─────────────────────────────────────────────────────────────────────────────
203
204/// GL bind target the texture was allocated with. Maps 1:1 onto the
205/// matching `GL_TEXTURE_*` enum value.
206///
207/// `GL_TEXTURE_EXTERNAL_OES` is intentionally absent — those are
208/// surfaced through the Android `MediaCodec` / `AImage` paths or the
209/// Linux EGL_EXT_image_external bridge, not directly as a
210/// `GpuResource::GlTextureHandle`. Importers take the OES target through
211/// their own import description instead.
212#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
213#[non_exhaustive]
214pub enum GlTextureTarget {
215 /// `GL_TEXTURE_2D`.
216 Texture2D,
217 /// `GL_TEXTURE_2D_ARRAY`.
218 Texture2DArray,
219 /// `GL_TEXTURE_3D`.
220 Texture3D,
221 /// `GL_TEXTURE_CUBE_MAP`.
222 CubeMap,
223 /// `GL_TEXTURE_RECTANGLE` (desktop GL only).
224 Rectangle,
225 /// `GL_RENDERBUFFER`.
226 Renderbuffer,
227}
228
229impl GlTextureTarget {
230 /// The matching `GL_TEXTURE_*` / `GL_RENDERBUFFER` enum value.
231 pub const fn to_gl_enum(self) -> u32 {
232 match self {
233 Self::Texture2D => 0x0DE1,
234 Self::Texture2DArray => 0x8C1A,
235 Self::Texture3D => 0x806F,
236 Self::CubeMap => 0x8513,
237 Self::Rectangle => 0x84F5,
238 Self::Renderbuffer => 0x8D41,
239 }
240 }
241}
242
243/// GL buffer bind target. Non-exhaustive — covers the targets a video /
244/// VFX caller realistically allocates exportable buffers under.
245#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
246#[non_exhaustive]
247pub enum GlBufferTarget {
248 /// `GL_ARRAY_BUFFER` — vertex attribute storage.
249 ArrayBuffer,
250 /// `GL_ELEMENT_ARRAY_BUFFER` — index storage.
251 ElementArrayBuffer,
252 /// `GL_PIXEL_UNPACK_BUFFER` — async upload PBO.
253 PixelUnpackBuffer,
254 /// `GL_PIXEL_PACK_BUFFER` — async download PBO.
255 PixelPackBuffer,
256 /// `GL_SHADER_STORAGE_BUFFER` — SSBO (read-write storage).
257 ShaderStorageBuffer,
258 /// `GL_UNIFORM_BUFFER` — UBO.
259 UniformBuffer,
260 /// `GL_TRANSFORM_FEEDBACK_BUFFER` — transform-feedback output.
261 TransformFeedbackBuffer,
262 /// `GL_COPY_READ_BUFFER` / `GL_COPY_WRITE_BUFFER` — generic
263 /// copy-only binding. Useful when the caller doesn't know the final
264 /// use up-front.
265 CopyReadBuffer,
266 CopyWriteBuffer,
267}
268
269impl GlBufferTarget {
270 /// The matching `GL_*_BUFFER` enum value.
271 pub const fn to_gl_enum(self) -> u32 {
272 match self {
273 Self::ArrayBuffer => 0x8892,
274 Self::ElementArrayBuffer => 0x8893,
275 Self::PixelUnpackBuffer => 0x88EC,
276 Self::PixelPackBuffer => 0x88EB,
277 Self::ShaderStorageBuffer => 0x90D2,
278 Self::UniformBuffer => 0x8A11,
279 Self::TransformFeedbackBuffer => 0x8C8E,
280 Self::CopyReadBuffer => 0x8F36,
281 Self::CopyWriteBuffer => 0x8F37,
282 }
283 }
284}
285
286/// GL sized internal-format enum the texture was created with — only the
287/// subset that a video pipeline ever surfaces.
288#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
289#[non_exhaustive]
290pub enum GlInternalFormat {
291 Unspecified,
292 R8,
293 R8Snorm,
294 R8Ui,
295 R8I,
296 R16,
297 R16F,
298 R16Ui,
299 R16I,
300 R32F,
301 Rg8,
302 Rg8Snorm,
303 Rg16,
304 Rg16F,
305 Rgba8,
306 Srgb8Alpha8,
307 Rgba8Snorm,
308 Rgb10A2,
309 Rgba16,
310 Rgba16F,
311 Rgba32F,
312 R11fG11fB10f,
313 Bgra8,
314 Depth16,
315 Depth24Stencil8,
316}
317
318impl GlInternalFormat {
319 /// The matching `GL_*` enum value, or `0` for `Unspecified`.
320 pub const fn to_gl_enum(self) -> u32 {
321 match self {
322 Self::Unspecified => 0,
323 Self::R8 => 0x8229,
324 Self::R8Snorm => 0x8F94,
325 Self::R8Ui => 0x8232,
326 Self::R8I => 0x8231,
327 Self::R16 => 0x822A,
328 Self::R16F => 0x822D,
329 Self::R16Ui => 0x8234,
330 Self::R16I => 0x8233,
331 Self::R32F => 0x822E,
332 Self::Rg8 => 0x822B,
333 Self::Rg8Snorm => 0x8F95,
334 Self::Rg16 => 0x822C,
335 Self::Rg16F => 0x822F,
336 Self::Rgba8 => 0x8058,
337 Self::Srgb8Alpha8 => 0x8C43,
338 Self::Rgba8Snorm => 0x8F97,
339 Self::Rgb10A2 => 0x8059,
340 Self::Rgba16 => 0x805B,
341 Self::Rgba16F => 0x881A,
342 Self::Rgba32F => 0x8814,
343 Self::R11fG11fB10f => 0x8C3A,
344 Self::Bgra8 => 0x93A1, // GL_BGRA8_EXT
345 Self::Depth16 => 0x81A5,
346 Self::Depth24Stencil8 => 0x88F0,
347 }
348 }
349}
350
351// ─────────────────────────────────────────────────────────────────────────────
352// Convenience constructors
353// ─────────────────────────────────────────────────────────────────────────────
354
355impl GpuResource {
356 /// Convenience constructor for the common case — single GL context,
357 /// `GL_TEXTURE_2D`, format left `Unspecified` for the importer to
358 /// take from its own import description. No keep-alive (caller owns
359 /// the texture name).
360 pub fn opengl_texture_simple(name: u32) -> Self {
361 // SAFETY: caller-supplied name in current context; the `0`
362 // sentinel is the only failure mode `try_from_raw` rejects.
363 let h = unsafe {
364 GlTextureHandle::try_from_raw(
365 name,
366 GlTextureTarget::Texture2D,
367 GlInternalFormat::Unspecified,
368 core::ptr::null_mut(),
369 None,
370 None,
371 )
372 }
373 .expect("opengl_texture_simple: name must be non-zero");
374 Self::GlTextureHandle(h)
375 }
376
377 /// Convenience constructor for the common case — single GL context,
378 /// `GL_ARRAY_BUFFER`. No keep-alive (caller owns the buffer name).
379 pub fn opengl_buffer_simple(name: u32, size: u64) -> Self {
380 // SAFETY: caller-supplied name in current context; the `0`
381 // sentinel is the only failure mode `try_from_raw` rejects.
382 let h = unsafe {
383 GlBufferHandle::try_from_raw(name, GlBufferTarget::ArrayBuffer, size, core::ptr::null_mut(), None, None)
384 }
385 .expect("opengl_buffer_simple: name and size must be non-zero");
386 Self::GlBufferHandle(h)
387 }
388}