Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ rust-version = "1.91"
flate2 = "1.1"
hang = { version = "0.20", path = "rs/hang" }
kio = { version = "0.5", path = "rs/kio" }
libc = "0.2"
linux-raw-sys = { version = "0.12.1", features = ["ioctl"] }
# Permutation testing for the kio primitives (and everything built on them).
# Only compiled under `--cfg loom`; see `just rs loom`.
loom = { version = "0.7.2", features = ["futures"] }
Expand Down
25 changes: 15 additions & 10 deletions doc/lib/rs/crate/moq-video.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Four role modules, symmetric on both ends of the wire:
| `capture` | Camera, display, window, or application frames | AVFoundation + ScreenCaptureKit (macOS), V4L2 + PipeWire (Linux), Media Foundation + DXGI (Windows) |
| `encode` | Raw frames to H.264/H.265, published through `moq-mux` | VideoToolbox, Media Foundation, NVENC, VAAPI, openh264 |
| `decode` | A subscribed track back to raw frames | VideoToolbox, Media Foundation/DXVA, NVDEC, openh264 |
| `render` | A frame drawn on the GPU, handed back as a `wgpu` texture | wgpu, with a zero-copy Metal import on macOS |
| `render` | A frame drawn on the GPU, handed back as a `wgpu` texture | wgpu, with zero-copy Metal and Vulkan imports |

A picture is a `Frame` wherever it crosses the API: a `moq_net::Timestamp` and a
`Surface` holding the pixels. Capture and decode produce them, encode and render
Expand Down Expand Up @@ -66,8 +66,8 @@ cargo add moq-video --features render,pipewire
| `capture` | yes | Native device capture (`v4l` and `zune-jpeg` on Linux) |
| `nvenc` / `nvdec` | yes | NVIDIA encode/decode on Linux (`cudarc`, `moq-nvenc`) |
| `vaapi` | no | Intel/AMD encode on Linux (`moq-vaapi`), unvalidated on hardware |
| `render` | no | `wgpu` and the GPU renderer |
| `pipewire` | no | Wayland/X11 screen capture via xdg-desktop-portal |
| `render` | no | `wgpu`, the GPU renderer, and Linux DMA-BUF support |
| `pipewire` | no | Wayland/X11 screen capture via xdg-desktop-portal and DMA-BUF |

`--no-default-features` gives a codec-only build that still encodes and decodes
H.264 with openh264 but omits native capture and the Linux GPU dependencies. A
Expand Down Expand Up @@ -133,25 +133,26 @@ while let Some(frame) = video.read().await? {
## Zero-copy

`Surface` is a `#[non_exhaustive]` enum naming what actually holds a frame's
pixels: a `CVPixelBuffer` on macOS, a Direct3D 11 texture on Windows, CUDA memory
on Linux, or plain I420 anywhere. Keeping a decoded frame in the first three
avoids a round trip through system memory on every frame.
pixels: a `CVPixelBuffer` on macOS, a Direct3D 11 texture on Windows, CUDA or a
DMA-BUF on Linux, or plain I420 anywhere. Keeping a frame in one of the native
representations avoids a round trip through system memory on every frame.

How far that gets today depends on the platform, so here is the honest matrix
rather than a blanket promise:

| Platform | Decode output | Zero-copy transcode | Zero-copy render |
| --- | --- | --- | --- |
| macOS | `PixelBuffer` (VideoToolbox) | yes | yes, via `CVMetalTextureCache` |
| Linux | `Cuda` (NVDEC) | yes, straight into NVENC | no, downloaded to I420 first |
| Linux | `Cuda` (NVDEC) | yes, straight into NVENC | decoded CUDA frames: no; packed PipeWire DMA-BUF capture: yes, via Vulkan |
| Windows | `Texture` (Media Foundation / DXVA) | yes, through the Direct3D11 video processor | no, downloaded to I420 first |

`Frame::resize` stays on the GPU through a `VTPixelTransferSession`, CUDA kernel,
or Direct3D11 video processor. Call `Frame::resize_with` with
`resize::Acceleration::Cpu` to force a download and CPU resize. A driver that
rejects GPU resizing returns to CPU scaling and warns once. Rendering is
zero-copy on macOS only; the Vulkan and EGL importers that would extend it are
tracked in [#2481](https://github.com/moq-dev/moq/issues/2481).
rejects GPU resizing returns to CPU scaling and warns once. Linux Vulkan can
import packed RGB DMA-BUF screen frames. Multi-plane NV12 import and retiling a
modifier that Vulkan rejects remain tracked in
[#2819](https://github.com/moq-dev/moq/issues/2819).

Matching on `Surface` stays portable because every variant has a universal
fallback in `Surface::into_i420()`: take the fast path you recognize and let the
Expand Down Expand Up @@ -182,6 +183,10 @@ The `wgpu` version this was built against is re-exported as
`moq_video::render::wgpu`, so you name the exact version rather than guessing at
a compatible one.

On Linux, request `wgpu::Features::VULKAN_EXTERNAL_MEMORY_DMA_BUF` when creating
the device to activate the PipeWire DMA-BUF fast path. The renderer still works
without it and falls back to a CPU upload for linear allocations.

`Color` names the matrix and range (BT.601 or BT.709, limited or full), and the
shader converts per frame rather than assuming one space. A capture labels what it
produced, so a locally captured frame renders correctly with no help from you.
Expand Down
2 changes: 1 addition & 1 deletion rs/moq-native/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ webpki-roots = "1"
[target.'cfg(unix)'.dependencies]
# Errno constants for classifying an `accept(2)` failure (`accept::Failure`).
# Their numeric values differ across unix platforms, so they can't be spelled out.
libc = "0.2"
libc = { workspace = true }

[dev-dependencies]
anyhow = "1"
Expand Down
17 changes: 14 additions & 3 deletions rs/moq-video/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,18 @@ nvdec = ["dep:cudarc", "dep:moq-nvenc", "dep:libloading"]
# build: moq-vaapi dlopen's libva, so an enabled-vaapi build needs no libva at
# build time and still starts on a libva-less host, where automatic backend
# selection falls back to the next encoder like the NVENC backend does.
vaapi = ["dep:moq-vaapi"]
vaapi = ["dmabuf", "dep:moq-vaapi"]
# Linux DMA-BUF surface vocabulary and CPU fallback support. Enabled by every
# backend that can produce or consume one. It pulls no graphics API by itself.
dmabuf = ["dep:libc", "dep:linux-raw-sys"]
# GPU rendering of decoded frames (the `render` module): a wgpu pipeline that
# converts a frame to RGBA and hands back a texture the caller presents. Off by
# default because wgpu pulls a graphics stack (and its backend drivers) that a
# relay or a headless publisher has no use for; only an application that actually
# draws video turns it on.
# draws video turns it on. `dmabuf` adds the Linux surface vocabulary; Vulkan
# itself still comes from wgpu.
render = [
"dmabuf",
"dep:bytemuck",
"dep:wgpu",
"dep:objc2-metal",
Expand All @@ -55,7 +60,7 @@ render = [
# Off by default because the `pipewire` crate links libpipewire-0.3 via pkg-config
# at build time (and its bindgen needs libclang), so it only belongs in builds
# that actually capture displays. Camera capture (V4L2) needs nothing extra.
pipewire = ["capture", "dep:pipewire", "dep:ashpd"]
pipewire = ["capture", "dmabuf", "dep:pipewire", "dep:ashpd"]

[dependencies]
anyhow = "1"
Expand Down Expand Up @@ -106,10 +111,16 @@ ashpd = { version = "0.13", optional = true, default-features = false, features
# extra dependencies); libnvrtc itself is never loaded since we ship pre-built PTX
# (see frame/nv12_resize.ptx) that the driver JIT-compiles.
cudarc = { version = "0.19", optional = true, default-features = false, features = ["driver", "fallback-dynamic-loading", "cuda-12020", "nvrtc"] }
# Mapping a linear DMA-BUF for the universal CPU I420 fallback. Non-linear
# modifiers stay GPU-only and return an honest error instead of treating tiled
# memory as rows.
libc = { workspace = true, optional = true }
# Probe for the NVIDIA driver libraries before calling cudarc / the NVENC SDK,
# which panic (process-abort under release `panic = "abort"`) if their library is
# absent. Lets a GPU-less host fall back to the next encoder instead of crashing.
libloading = { version = "0.9", optional = true }
# Architecture-correct DMA_BUF_IOCTL_SYNC request values for CPU access.
linux-raw-sys = { workspace = true, optional = true }
moq-nvenc = { workspace = true, optional = true }
# Intel/AMD VAAPI hardware encoder, behind the opt-in `vaapi` feature. As of
# moq-vaapi 0.0.3 libva is dlopen'd at runtime, so the binary carries no NEEDED
Expand Down
Loading
Loading