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 we deliberately do **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]` — future versions may add variants; existing matchers stay
68/// valid.
69#[derive(Clone, Debug)]
70#[non_exhaustive]
71pub enum GpuResource {
72 // ── D3D11 / D3D12 ─────────────────────────────────────────────────
73 D3D11Texture(D3D11Texture),
74 D3D12Resource(D3D12Resource),
75 D3D12BufferHandle(D3D12BufferHandle),
76
77 // ── Vulkan ────────────────────────────────────────────────────────
78 VkImage(VkImage),
79 VkBufferHandle(VkBufferHandle),
80 /// `VK_KHR_external_memory_fd` opaque-fd export (Linux / Android).
81 #[cfg(any(unix, target_os = "wasi", target_os = "hermit"))]
82 VkOpaqueFd(VkOpaqueFd),
83 /// `VK_KHR_external_memory_win32` opaque-NT-handle export (Windows).
84 #[cfg(windows)]
85 VkOpaqueWin32(VkOpaqueWin32),
86
87 // ── CUDA ──────────────────────────────────────────────────────────
88 CudaPtr2D(CudaPtr2D),
89 CudaBufferHandle(CudaBufferHandle),
90 /// Opaque CUDA `CUsurfObject` bound to a `CUarray` — a `surf2Dwrite`
91 /// kernel write target (typically a foreign-API texture imported as
92 /// a CUDA array). Gated on the `wgpu` feature because the handle
93 /// carries a `wgpu::TextureFormat`; see [`CudaSurface`].
94 #[cfg(feature = "wgpu")]
95 CudaSurface(CudaSurface),
96
97 // ── OpenCL ────────────────────────────────────────────────────────
98 OpenClMem(OpenClMem),
99
100 // ── Metal / Apple ─────────────────────────────────────────────────
101 MetalTextureHandle(MetalTextureHandle),
102 MetalBufferHandle(MetalBufferHandle),
103 #[cfg(target_vendor = "apple")]
104 IoSurface(IoSurface),
105 #[cfg(target_vendor = "apple")]
106 VideoToolboxFrame(VideoToolboxFrame),
107
108 // ── OpenGL ────────────────────────────────────────────────────────
109 GlTextureHandle(GlTextureHandle),
110 GlBufferHandle(GlBufferHandle),
111
112 // ── Linux ─────────────────────────────────────────────────────────
113 /// Bare Linux DMA-BUF descriptor.
114 #[cfg(any(target_os = "linux", target_os = "android"))]
115 DmaBufHandle(DmaBufHandle),
116 /// VAAPI surface — kept as an importer path.
117 #[cfg(target_os = "linux")]
118 VaapiSurface(VaapiSurface),
119
120 // ── Android ───────────────────────────────────────────────────────
121 #[cfg(target_os = "android")]
122 AHardwareBufferHandle(AHardwareBufferHandle),
123 #[cfg(target_os = "android")]
124 MediaCodecFrame(MediaCodecFrame),
125 #[cfg(target_os = "android")]
126 AImageFrame(AImageFrame),
127 #[cfg(target_os = "android")]
128 AndroidNativeWindowHandle(AndroidNativeWindowHandle),
129 #[cfg(target_os = "android")]
130 AndroidSurfaceControlHandle(AndroidSurfaceControlHandle),
131
132 // ── Windows NT handles ────────────────────────────────────────────
133 #[cfg(windows)]
134 NtHandle(NtHandle),
135 #[cfg(windows)]
136 KmtToken(KmtToken),
137
138 // ── CPU ───────────────────────────────────────────────────────────
139 CpuBytes(CpuBytes),
140 CpuPlanes(CpuPlaneSet),
141 CpuSharedSlot(CpuSharedSlot),
142
143 // ── Native wgpu (same-device passthrough) ─────────────────────────
144 /// A `wgpu::Texture` already on the consumer's own wgpu device — the
145 /// degenerate "nothing to import" case. The interop import path
146 /// returns the texture verbatim (no foreign-handle import, no copy).
147 /// Gated on the `wgpu` feature; see [`WgpuTexture`].
148 #[cfg(feature = "wgpu")]
149 WgpuTexture(WgpuTexture),
150
151 // ── Web (wasm) ────────────────────────────────────────────────────
152 /// Browser GPU / media handle (`GPUTexture`, `WebGLTexture`,
153 /// `<video>`, `VideoFrame`, …). wasm-only. See the module
154 /// `# Thread safety` note and [`crate::web::WebGpuResource`].
155 #[cfg(all(target_family = "wasm", feature = "web"))]
156 Web(crate::web::WebGpuResource),
157}
158
159// Compile-time enforcement of the wasm thread-affinity carve-out.
160// On wasm `GpuResource` is `!Send + !Sync`:
161// - with the `wgpu` feature (always on for the interop stack) the
162// `WgpuTexture` variant's `Option<Arc<dyn ResourceKeepAlive>>`
163// keep-alive is `!Send + !Sync` on BOTH wasm targets — its
164// `MaybeSendSync` bound is the empty marker on wasm — and the variant
165// has no `unsafe impl` to force it `Send`. This is the pin on the
166// *non-atomics* target (where the `Web` payload below is still `Send`),
167// which is why the assertion is gated on `wgpu`.
168// - on the threaded (`atomics`) target the `Web(WebGpuResource)`
169// variant's `web_sys` payload is additionally `!Send` (wasm-bindgen
170// gates `Send for JsValue` on non-atomics).
171// The guarantee therefore holds independent of the wasm threading model,
172// which is why nothing here relies on wgpu's
173// `fragile-send-sync-non-atomic-wasm`.
174#[cfg(all(target_family = "wasm", feature = "web", feature = "wgpu"))]
175static_assertions::assert_not_impl_any!(GpuResource: Send, Sync);
176
177/// Up-to-four-plane CPU buffer set. `count` ∈ `1..=4`; unused slots are
178/// `None`. Zero heap allocation — the `[Option<CpuPlane>; 4]` is inline.
179#[derive(Debug, Copy, Clone)]
180pub struct CpuPlaneSet {
181 pub format: crate::PixelFormat,
182 pub count: u8,
183 pub planes: [Option<CpuPlane>; 4],
184}
185
186#[derive(Debug, Copy, Clone)]
187pub struct CpuPlane {
188 pub data: *mut u8,
189 pub size: usize,
190 /// Row pitch in bytes.
191 pub row_pitch: u32,
192}
193
194// SAFETY: caller-asserted contract on the byte ranges. Same shape as
195// the typed-newtype payloads in `handles`.
196unsafe impl Send for CpuPlaneSet {}
197unsafe impl Sync for CpuPlaneSet {}
198unsafe impl Send for CpuPlane {}
199unsafe impl Sync for CpuPlane {}
200
201// ─────────────────────────────────────────────────────────────────────────────
202// GL bind targets / formats
203// ─────────────────────────────────────────────────────────────────────────────
204
205/// GL bind target the texture was allocated with. Maps 1:1 onto the
206/// matching `GL_TEXTURE_*` enum value.
207///
208/// `GL_TEXTURE_EXTERNAL_OES` is intentionally absent — those are
209/// surfaced through the Android `MediaCodec` / `AImage` paths or the
210/// Linux EGL_EXT_image_external bridge, not directly as a
211/// `GpuResource::GlTextureHandle`. Importers take the OES target through
212/// their own import description instead.
213#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
214#[non_exhaustive]
215pub enum GlTextureTarget {
216 /// `GL_TEXTURE_2D`.
217 Texture2D,
218 /// `GL_TEXTURE_2D_ARRAY`.
219 Texture2DArray,
220 /// `GL_TEXTURE_3D`.
221 Texture3D,
222 /// `GL_TEXTURE_CUBE_MAP`.
223 CubeMap,
224 /// `GL_TEXTURE_RECTANGLE` (desktop GL only).
225 Rectangle,
226 /// `GL_RENDERBUFFER`.
227 Renderbuffer,
228}
229
230impl GlTextureTarget {
231 /// The matching `GL_TEXTURE_*` / `GL_RENDERBUFFER` enum value.
232 pub const fn to_gl_enum(self) -> u32 {
233 match self {
234 Self::Texture2D => 0x0DE1,
235 Self::Texture2DArray => 0x8C1A,
236 Self::Texture3D => 0x806F,
237 Self::CubeMap => 0x8513,
238 Self::Rectangle => 0x84F5,
239 Self::Renderbuffer => 0x8D41,
240 }
241 }
242}
243
244/// GL buffer bind target. Non-exhaustive — covers the targets a video /
245/// VFX caller realistically allocates exportable buffers under.
246#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
247#[non_exhaustive]
248pub enum GlBufferTarget {
249 /// `GL_ARRAY_BUFFER` — vertex attribute storage.
250 ArrayBuffer,
251 /// `GL_ELEMENT_ARRAY_BUFFER` — index storage.
252 ElementArrayBuffer,
253 /// `GL_PIXEL_UNPACK_BUFFER` — async upload PBO.
254 PixelUnpackBuffer,
255 /// `GL_PIXEL_PACK_BUFFER` — async download PBO.
256 PixelPackBuffer,
257 /// `GL_SHADER_STORAGE_BUFFER` — SSBO (read-write storage).
258 ShaderStorageBuffer,
259 /// `GL_UNIFORM_BUFFER` — UBO.
260 UniformBuffer,
261 /// `GL_TRANSFORM_FEEDBACK_BUFFER` — transform-feedback output.
262 TransformFeedbackBuffer,
263 /// `GL_COPY_READ_BUFFER` / `GL_COPY_WRITE_BUFFER` — generic
264 /// copy-only binding. Useful when the caller doesn't know the final
265 /// use up-front.
266 CopyReadBuffer,
267 CopyWriteBuffer,
268}
269
270impl GlBufferTarget {
271 /// The matching `GL_*_BUFFER` enum value.
272 pub const fn to_gl_enum(self) -> u32 {
273 match self {
274 Self::ArrayBuffer => 0x8892,
275 Self::ElementArrayBuffer => 0x8893,
276 Self::PixelUnpackBuffer => 0x88EC,
277 Self::PixelPackBuffer => 0x88EB,
278 Self::ShaderStorageBuffer => 0x90D2,
279 Self::UniformBuffer => 0x8A11,
280 Self::TransformFeedbackBuffer => 0x8C8E,
281 Self::CopyReadBuffer => 0x8F36,
282 Self::CopyWriteBuffer => 0x8F37,
283 }
284 }
285}
286
287/// GL sized internal-format enum the texture was created with — only the
288/// subset that a video pipeline ever surfaces.
289#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
290#[non_exhaustive]
291pub enum GlInternalFormat {
292 Unspecified,
293 R8,
294 R8Snorm,
295 R8Ui,
296 R8I,
297 R16,
298 R16F,
299 R16Ui,
300 R16I,
301 R32F,
302 Rg8,
303 Rg8Snorm,
304 Rg16,
305 Rg16F,
306 Rgba8,
307 Srgb8Alpha8,
308 Rgba8Snorm,
309 Rgb10A2,
310 Rgba16,
311 Rgba16F,
312 Rgba32F,
313 R11fG11fB10f,
314 Bgra8,
315 Depth16,
316 Depth24Stencil8,
317}
318
319impl GlInternalFormat {
320 /// The matching `GL_*` enum value, or `0` for `Unspecified`.
321 pub const fn to_gl_enum(self) -> u32 {
322 match self {
323 Self::Unspecified => 0,
324 Self::R8 => 0x8229,
325 Self::R8Snorm => 0x8F94,
326 Self::R8Ui => 0x8232,
327 Self::R8I => 0x8231,
328 Self::R16 => 0x822A,
329 Self::R16F => 0x822D,
330 Self::R16Ui => 0x8234,
331 Self::R16I => 0x8233,
332 Self::R32F => 0x822E,
333 Self::Rg8 => 0x822B,
334 Self::Rg8Snorm => 0x8F95,
335 Self::Rg16 => 0x822C,
336 Self::Rg16F => 0x822F,
337 Self::Rgba8 => 0x8058,
338 Self::Srgb8Alpha8 => 0x8C43,
339 Self::Rgba8Snorm => 0x8F97,
340 Self::Rgb10A2 => 0x8059,
341 Self::Rgba16 => 0x805B,
342 Self::Rgba16F => 0x881A,
343 Self::Rgba32F => 0x8814,
344 Self::R11fG11fB10f => 0x8C3A,
345 Self::Bgra8 => 0x93A1, // GL_BGRA8_EXT
346 Self::Depth16 => 0x81A5,
347 Self::Depth24Stencil8 => 0x88F0,
348 }
349 }
350}
351
352// ─────────────────────────────────────────────────────────────────────────────
353// Convenience constructors
354// ─────────────────────────────────────────────────────────────────────────────
355
356impl GpuResource {
357 /// Convenience constructor for the common case — single GL context,
358 /// `GL_TEXTURE_2D`, format left `Unspecified` for the importer to
359 /// take from its own import description. No keep-alive (caller owns
360 /// the texture name).
361 pub fn opengl_texture_simple(name: u32) -> Self {
362 // SAFETY: caller-supplied name in current context; the `0`
363 // sentinel is the only failure mode `try_from_raw` rejects.
364 let h = unsafe {
365 GlTextureHandle::try_from_raw(
366 name,
367 GlTextureTarget::Texture2D,
368 GlInternalFormat::Unspecified,
369 core::ptr::null_mut(),
370 None,
371 None,
372 )
373 }
374 .expect("opengl_texture_simple: name must be non-zero");
375 Self::GlTextureHandle(h)
376 }
377
378 /// Convenience constructor for the common case — single GL context,
379 /// `GL_ARRAY_BUFFER`. No keep-alive (caller owns the buffer name).
380 pub fn opengl_buffer_simple(name: u32, size: u64) -> Self {
381 // SAFETY: caller-supplied name in current context; the `0`
382 // sentinel is the only failure mode `try_from_raw` rejects.
383 let h = unsafe {
384 GlBufferHandle::try_from_raw(name, GlBufferTarget::ArrayBuffer, size, core::ptr::null_mut(), None, None)
385 }
386 .expect("opengl_buffer_simple: name and size must be non-zero");
387 Self::GlBufferHandle(h)
388 }
389}