Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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