feat(client): M9's code half — native first, FFmpeg behind an off-by-default feature
`ffmpeg-fallback` on pf-client-core, default off on the crate. With it off the libavcodec rungs are not compiled, pf-ffvk leaves the dependency graph, and no ladder or demotion arm names them; with it on each sits exactly where it sits today, directly below its native twin. That is the switch which makes M10 a deletion rather than a redesign. The bake window and the regression criteria are the user's, per the plan, and nothing here claims the M9 gate is met. The hard part was not the feature, it was honesty. Two of the four native rungs have never decoded a frame on any hardware — native VAAPI at all, and native D3D11VA's AV1 leg — and making those the default would assert evidence that does not exist. So admission is per rung and per codec: a pair with hardware evidence joins `auto` always; a pair without it joins only when nothing proven is left below it (a build with no FFmpeg twin, where the alternative is not a proven rung but the CPU) or when the user asks with PUNKTFUNK_NATIVE_FIRST=1. Pins bypass it, so a lab run can still reach any rung. The shipping default therefore changes in exactly three ways, all evidence-backed: AV1 `auto` takes native Vulkan (250/250 bit-identical on an RTX 5070 Ti), Windows H.264/H.265 `auto` takes native D3D11VA above its FFmpeg twin (parity on two GPUs plus a 30-minute soak), and a failing Vulkan rung on Windows demotes to native D3D11VA first. Everything unproven is byte-for-byte as it was. The evidence state is written where it cannot rot: a table in video.rs's module docs, the same facts in code as `native_evidence()`, a test asserting them in both feature states, and a per-session log line carrying the rung, the codec, whether hardware has verified that pair and the evidence string — at WARN when it has not. A support engineer reading a log can now tell proven from assumed without asking anyone. Termination needed a new guarantee. With the FFmpeg twins gone, two native rungs in opposite per-vendor orders could hand a session back and forth forever, so a rung once entered is never re-entered and the walk is monotone to software. The never-delivered fall-through still works: with the feature on it is unchanged, and with it off it is redundant, because the next candidate already IS the rung below. ⚠ ffmpeg-next remains a hard dependency of pf-client-core, deliberately. What is left off-feature is three type-level residues — the codec-id vocabulary, the AVVkFrame guard that is pf-presenter's public import, and a pixel-format in one signature — every one of them an M10 §6 line item. Deleting them here would mean deleting the presenter's FFmpeg lane, 55 call sites, in a milestone whose gates cannot run a GPU. No libavcodec decoder is opened in a default build. ⚠ video_d3d11.rs was gated item by item rather than wholesale, and nothing in this tree compiles it — it needs a Windows check before anyone trusts it. Gates: both feature states, container clippy -D warnings and 158/159 tests, workspace check. The four decode crates are untouched, so the hardware rungs' 250/250 stands.
This commit is contained in:
@@ -13,7 +13,7 @@ name = "punktfunk-session"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = ["ui", "pyrowave"]
|
||||
default = ["ui", "pyrowave", "ffmpeg-fallback"]
|
||||
# PyroWave client decode (the wired-LAN wavelet codec) — enables the decode backend + the
|
||||
# planar present path. ON by default; each session still opts in explicitly (the Settings
|
||||
# codec pick, or PUNKTFUNK_PREFER_PYROWAVE=1). The Windows ARM64 leg builds
|
||||
@@ -24,6 +24,18 @@ pyrowave = ["pf-client-core/pyrowave", "pf-presenter/pyrowave"]
|
||||
# (`--no-default-features`) is the ~15 MB-smaller power-user build: same streaming,
|
||||
# stats on stdout only.
|
||||
ui = ["dep:pf-console-ui", "dep:serde_json"]
|
||||
# The FFmpeg-backed decode rungs, sitting directly below their native counterparts (M9's
|
||||
# `ffmpeg-fallback`). ON by default so what ships is unchanged while the M9 field bake is
|
||||
# the user's call to schedule; building WITHOUT it is the M10 preview — native rungs only,
|
||||
# no libavcodec decoder in the binary at all:
|
||||
#
|
||||
# cargo build -p punktfunk-client-session --no-default-features --features ui,pyrowave
|
||||
#
|
||||
# The other way to bake, and the recommended one, keeps this on and sets
|
||||
# `PUNKTFUNK_NATIVE_FIRST=1` in the session's environment: every native rung goes first
|
||||
# and the FFmpeg twin stays underneath as the fall-through. See `pf-client-core`'s
|
||||
# `video.rs` module docs.
|
||||
ffmpeg-fallback = ["pf-client-core/ffmpeg-fallback", "pf-presenter/ffmpeg-fallback"]
|
||||
|
||||
# Same Linux+Windows gating as the rest of the client stack; elsewhere this is a stub
|
||||
# binary.
|
||||
|
||||
@@ -49,10 +49,10 @@ path + per-stage latency equation); any tier but Off also emits the stdout mirro
|
||||
`--no-default-features` is the ~5 MB power-user build — same streaming, stats on stdout
|
||||
only, no Skia anywhere in the dependency tree.
|
||||
|
||||
Decode follows the Settings preference (auto is vendor-ordered: hardware Vulkan Video →
|
||||
VAAPI → software on Linux, hardware Vulkan Video → D3D11VA → software on Windows, with
|
||||
VAAPI/D3D11VA first on Intel; on H.264 and HEVC the native pf-vkdecode Vulkan decoder
|
||||
is tried immediately before FFmpeg-Vulkan): the Vulkan decoders run on the presenter's own
|
||||
Decode follows the Settings preference (auto is vendor-ordered: Vulkan Video → VAAPI →
|
||||
software on Linux, Vulkan Video → D3D11VA → software on Windows, with VAAPI/D3D11VA first
|
||||
on Intel — and since M9 each of those is a NATIVE rung with its libavcodec twin directly
|
||||
below it; see "Decode rungs" below): the Vulkan decoders run on the presenter's own
|
||||
device where the stack supports it (every vendor, zero copy); VAAPI dmabufs import
|
||||
per-plane elsewhere (D3D11VA textures on Windows); software is the universal fallback.
|
||||
10-bit Main10 and HDR10 are advertised (`VIDEO_CAP_10BIT|HDR`): P010 decodes through the
|
||||
@@ -62,9 +62,31 @@ tone-map in-shader to SDR when it doesn't (`PUNKTFUNK_TONEMAP_PEAK` tunes the ro
|
||||
default ≈1000 nits). The host still gates the upgrade behind its `PUNKTFUNK_10BIT`
|
||||
policy.
|
||||
|
||||
## Decode rungs (M9: native first)
|
||||
|
||||
`auto` walks native rungs first — pf-vkdecode over Vulkan Video, then the platform's own
|
||||
(pf-dxvadec on Windows, pf-vaadec on Linux), then the CPU rung (openh264/rav1d). The
|
||||
libavcodec rungs are still compiled in by default and sit DIRECTLY BELOW their native
|
||||
counterpart as the fall-through; `--no-default-features --features ui,pyrowave` builds
|
||||
without them entirely.
|
||||
|
||||
Two of the native rungs have never decoded a frame on real hardware (native VAAPI at all;
|
||||
native D3D11VA's AV1 leg), so `auto` skips those while a libavcodec rung is still below
|
||||
them. `PUNKTFUNK_NATIVE_FIRST=1` switches them in — that is the M9 field-bake switch, and
|
||||
it keeps the FFmpeg twin underneath as the safety net. Every session logs the rung it
|
||||
landed on with its evidence state:
|
||||
|
||||
decode rung active rung=native-vulkan codec=HEVC hardware_verified=true evidence=...
|
||||
|
||||
…and that line is a WARNING when nothing has ever decoded a frame through the rung/codec
|
||||
pair the session chose. `pf-client-core`'s `video.rs` module docs carry the full table.
|
||||
|
||||
Debug/bisect knobs: `PUNKTFUNK_DECODER=native-vulkan|native-vaapi|native-d3d11va|vulkan|vaapi|d3d11va|software`
|
||||
(the three `native-*` values pin this program's own decoders; `native-vaapi` also takes
|
||||
`PUNKTFUNK_VAAPI_DEVICE=/dev/dri/renderDNNN` to choose the GPU), `PUNKTFUNK_PRESENT_MODE=
|
||||
(the three `native-*` values pin this program's own decoders and bypass the evidence rule
|
||||
above, which is how a lab run reaches a rung `auto` will not pick; the three bare values
|
||||
name the libavcodec rungs specifically and refuse in a build without them; `native-vaapi`
|
||||
also takes `PUNKTFUNK_VAAPI_DEVICE=/dev/dri/renderDNNN` to choose the GPU),
|
||||
`PUNKTFUNK_NATIVE_FIRST=1` (above), `PUNKTFUNK_PRESENT_MODE=
|
||||
mailbox|fifo|immediate|fifo_relaxed` (default MAILBOX, FIFO where the surface offers no
|
||||
MAILBOX — AMD on Windows), `PUNKTFUNK_VK_DEVICE=<index>` (multi-GPU), and
|
||||
`PUNKTFUNK_HW_FAULT=import` (fault every VAAPI dmabuf import — proves the three-strike
|
||||
|
||||
@@ -15,11 +15,15 @@ repository.workspace = true
|
||||
# (same public surface — see lib.rs).
|
||||
[target.'cfg(any(target_os = "linux", windows))'.dependencies]
|
||||
punktfunk-core = { path = "../punktfunk-core", features = ["quic"] }
|
||||
# FFmpeg's Vulkan hwcontext surface (Vulkan Video decode on the presenter's device).
|
||||
pf-ffvk = { path = "../pf-ffvk" }
|
||||
# FFmpeg's Vulkan hwcontext surface (Vulkan Video decode on the presenter's device) —
|
||||
# used by `video_vulkan.rs` and NOTHING else here, so it rides the M9 `ffmpeg-fallback`
|
||||
# feature: with the FFmpeg rungs off, this dependency is not in the graph at all.
|
||||
# (pf-presenter has its OWN pf-ffvk for the AVVkFrame sync contract; that one dies with
|
||||
# the FFmpeg lane at M10, §6.)
|
||||
pf-ffvk = { path = "../pf-ffvk", optional = true }
|
||||
# Native Vulkan Video decode (WP-C of the native-decode program, HEVC added by M3
|
||||
# WP-2, AV1 by M7 — pin only): auto's rung immediately above FFmpeg-Vulkan (2026-08-05
|
||||
# ladder decision), also pinnable via `PUNKTFUNK_DECODER=native-vulkan` —
|
||||
# WP-2, AV1 by M7): auto's TOP rung on both desktop OSes since M9 — for every codec it
|
||||
# speaks, AV1 included — also pinnable via `PUNKTFUNK_DECODER=native-vulkan` —
|
||||
# video_vk_native.rs, running pf-vkdecode's VkH264Decoder/VkH265Decoder/VkAv1Decoder on
|
||||
# the presenter's shared device.
|
||||
pf-vkdecode = { path = "../pf-vkdecode" }
|
||||
@@ -85,7 +89,23 @@ rav1d = { version = "1", default-features = false, features = ["bitdepth_8"] }
|
||||
# otherwise name it is out of reach. Already in the tree (rav1d's own dependency).
|
||||
libc = "0.2"
|
||||
|
||||
# Video decode (same FFmpeg pin as the host) and Opus for the audio planes.
|
||||
# Opus for the audio planes.
|
||||
#
|
||||
# `ffmpeg-next` (same pin as the host) is the DECODE side, and since M9 it is no longer
|
||||
# the decode side of anything the ladder reaches by default: the three FFmpeg-backed
|
||||
# rungs live behind `ffmpeg-fallback` (below). It stays a hard dependency for exactly
|
||||
# three residues, all of them M10's §6 line item ("pf-client-core: drop ffmpeg-next")
|
||||
# and all of them type-level rather than decode-level:
|
||||
# * `ffmpeg::codec::Id` — the codec vocabulary `Decoder::new` still speaks (the wire
|
||||
# `quic::CODEC_*` bits are the other half; `wire_codec_of` is where they meet);
|
||||
# * `DrmFrameGuard::Av` + `VkVideoFrame` + `DecodedImage::VkFrame` — the FFmpeg-Vulkan
|
||||
# frame TYPE, which is `pf-presenter`'s public import (`vk/present.rs`'s AVVkFrame
|
||||
# lock/unlock contract). Deleting it here is deleting the presenter's FFmpeg lane,
|
||||
# which M10 does in one move together with `crates/pf-ffvk`;
|
||||
# * `AVPixelFormat` in `drm_fourcc_for`'s signature (its constants are locked by a
|
||||
# test that runs on every leg).
|
||||
# Nothing in that list opens a decoder, and with `ffmpeg-fallback` off nothing in this
|
||||
# crate calls libavcodec at all.
|
||||
ffmpeg-next = "8"
|
||||
opus = "0.3"
|
||||
|
||||
@@ -177,6 +197,23 @@ sha2 = "0.10"
|
||||
# still strictly per-session opt-in (Settings codec pick / PUNKTFUNK_PREFER_PYROWAVE=1).
|
||||
default = ["pyrowave"]
|
||||
pyrowave = ["dep:pyrowave-sys", "dep:ash"]
|
||||
# M9 — the three FFmpeg-backed decode rungs (`video_vulkan`, `video_vaapi`, the
|
||||
# libavcodec half of `video_d3d11`, plus the `video_libav` ownership helpers they share).
|
||||
#
|
||||
# **OFF here on purpose, and that is NOT the shipping default.** The crate default is the
|
||||
# M10 world — natives only — so that `cargo clippy -p pf-client-core` compiles, lints and
|
||||
# tests the ladder M10 will leave behind, and M10 itself becomes `rm` plus attribute
|
||||
# deletion instead of a redesign. The binaries that actually decode
|
||||
# (`clients/session`, and `pf-presenter` which owns the run loop) turn it back ON in their
|
||||
# own default feature sets, so what a user installs is unchanged until they say otherwise.
|
||||
#
|
||||
# With it ON the FFmpeg rungs sit exactly where they sit today: directly BELOW their
|
||||
# native counterparts, as the fall-through a native init failure or error streak lands on.
|
||||
# With it OFF the ladder is native → other native → software and nothing else.
|
||||
#
|
||||
# See `video.rs`'s module docs for the rung/evidence table and for the two ways to run the
|
||||
# M9 field bake.
|
||||
ffmpeg-fallback = ["dep:pf-ffvk"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -72,26 +72,30 @@ pub mod video;
|
||||
mod video_color;
|
||||
#[cfg(any(target_os = "linux", windows))]
|
||||
mod video_software;
|
||||
// libav ownership helpers shared by the hardware decoders below (`AvBuffer`).
|
||||
#[cfg(any(target_os = "linux", windows))]
|
||||
// libav ownership helpers shared by the FFmpeg-backed rungs below (`AvBuffer`) — and by
|
||||
// nothing else, so it rides `ffmpeg-fallback` with them (M9).
|
||||
#[cfg(all(any(target_os = "linux", windows), feature = "ffmpeg-fallback"))]
|
||||
mod video_libav;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(all(target_os = "linux", feature = "ffmpeg-fallback"))]
|
||||
mod video_vaapi;
|
||||
// Native VAAPI decode (M6 of the native-decode program): pf-vaadec's plans driven
|
||||
// straight into libva, dlopen'd at runtime, exporting DRM-PRIME dmabufs the
|
||||
// presenter imports — the FFmpeg-free replacement for `video_vaapi`. Pin-only for
|
||||
// now (`PUNKTFUNK_DECODER=native-vaapi`).
|
||||
// presenter imports — the FFmpeg-free replacement for `video_vaapi`. It has decoded
|
||||
// nothing on hardware, so `auto` reaches it only where nothing proven is left below it
|
||||
// (an `ffmpeg-fallback`-less build) or where the user asked — see `video`'s evidence
|
||||
// table; `PUNKTFUNK_DECODER=native-vaapi` reaches it by pin regardless.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod video_vaapi_native;
|
||||
// Native Vulkan Video decode (WP-C of the native-decode program, HEVC added by M3
|
||||
// WP-2, AV1 by M7): pf-vkdecode's H.264/H.265/AV1 decoders on the presenter's shared
|
||||
// device — auto's rung immediately above FFmpeg-Vulkan (2026-08-05 ladder decision;
|
||||
// the program is dropping FFmpeg from the client), also pinnable via
|
||||
// `PUNKTFUNK_DECODER=native-vulkan`. The AV1 leg is PIN ONLY until it has hardware
|
||||
// evidence, so an `auto` AV1 session still lands on the FFmpeg rungs.
|
||||
// device — auto's TOP rung on both desktop OSes since M9, for all three codecs (each
|
||||
// leg has hardware parity against libavcodec; see `video`'s evidence table), also
|
||||
// pinnable via `PUNKTFUNK_DECODER=native-vulkan`.
|
||||
#[cfg(any(target_os = "linux", windows))]
|
||||
mod video_vk_native;
|
||||
#[cfg(any(target_os = "linux", windows))]
|
||||
// FFmpeg's Vulkan Video rung — the fall-through directly BELOW `video_vk_native`, and
|
||||
// only when `ffmpeg-fallback` is compiled in (M9).
|
||||
#[cfg(all(any(target_os = "linux", windows), feature = "ffmpeg-fallback"))]
|
||||
mod video_vulkan;
|
||||
// The OS-clipboard bridge for the shared clipboard (design/clipboard-and-file-transfer.md §5).
|
||||
// Built everywhere the session client is; the platform seam inside is Windows-real,
|
||||
@@ -103,11 +107,18 @@ pub mod clipboard;
|
||||
// Linux's: the decoder is plain Vulkan compute on the presenter's device (no fds, no
|
||||
// dmabuf, no D3D11 interop), so the old "Windows present-path decision" that gated it
|
||||
// resolved itself — the present path is now literally the same code.
|
||||
// D3D11VA. Two halves with two lifetimes, which is why this module is NOT gated as a
|
||||
// whole: the hand-off ring, the decode-device creation and `display_hdr_volume` are
|
||||
// shared, field-proven, FFmpeg-free code that `video_d3d11_native` (and
|
||||
// `clients/session`) build on; the libavcodec DECODER inside it rides
|
||||
// `ffmpeg-fallback` and is gated item by item in the file (M9).
|
||||
#[cfg(windows)]
|
||||
pub mod video_d3d11;
|
||||
// Native D3D11VA (M5): `ID3D11VideoDecoder` driven from pf-bitstream plans, filling the SAME
|
||||
// hand-off ring `video_d3d11` owns. Pin-only (`PUNKTFUNK_DECODER=native-d3d11va`) until it has
|
||||
// hardware evidence.
|
||||
// hand-off ring `video_d3d11` owns. In `auto` since M9 for the codecs that have hardware
|
||||
// evidence (H.264/H.265); its AV1 leg has decoded nothing on hardware and stays out of
|
||||
// `auto` — see `video`'s evidence table — while `PUNKTFUNK_DECODER=native-d3d11va` reaches
|
||||
// every leg by pin.
|
||||
#[cfg(windows)]
|
||||
pub mod video_d3d11_native;
|
||||
#[cfg(all(any(target_os = "linux", windows), feature = "pyrowave"))]
|
||||
|
||||
+1016
-286
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,10 @@
|
||||
//! The stream's per-frame colour signalling (`ColorDesc`) + the Y′CbCr→RGB CSC matrix (`csc_rows`).
|
||||
#![allow(clippy::unnecessary_cast)]
|
||||
|
||||
// Only [`ColorDesc::from_raw`] — the FFmpeg rungs' per-frame CICP read — needs libav here;
|
||||
// every native rung fills `ColorDesc` from pf-bitstream instead. So the import rides
|
||||
// `ffmpeg-fallback` (M9) and this module is FFmpeg-free in a default build.
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
use ffmpeg_next as ffmpeg;
|
||||
|
||||
/// The stream's colour signaling, read PER-FRAME from the decoder (HEVC VUI → the
|
||||
@@ -18,12 +22,14 @@ pub struct ColorDesc {
|
||||
}
|
||||
|
||||
impl ColorDesc {
|
||||
/// Read the CICP fields off a raw decoded frame. Public: the Windows client's raw-FFI
|
||||
/// D3D11VA/software decoders build their per-frame `ColorDesc` with it too (same
|
||||
/// `ffmpeg-next` major, so the `AVFrame` type unifies across the workspace).
|
||||
/// Read the CICP fields off a raw decoded frame — the FFmpeg rungs' per-frame colour
|
||||
/// source, and theirs alone: every native rung reads the same signalling out of the
|
||||
/// SPS/sequence header through pf-bitstream, which is why this compiles out with
|
||||
/// `ffmpeg-fallback` (M9) and why nothing was lost when it did.
|
||||
///
|
||||
/// # Safety
|
||||
/// `frame` must point to a valid `AVFrame` (alive for the duration of the call).
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
pub unsafe fn from_raw(frame: *const ffmpeg::ffi::AVFrame) -> ColorDesc {
|
||||
// SAFETY: caller guarantees a live AVFrame; these are plain enum field reads.
|
||||
unsafe {
|
||||
|
||||
@@ -45,12 +45,21 @@
|
||||
//! writes the decode surface.
|
||||
|
||||
use crate::video::ColorDesc;
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
use crate::video_libav::AvBuffer;
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
// Every `bail!` in this file is in the libavcodec half (the DXVA profile probe and the
|
||||
// decoder itself); the shared hand-off half raises its errors through `.context()`.
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
use anyhow::bail;
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
use ffmpeg_next as ffmpeg;
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
use std::ffi::c_void;
|
||||
use std::ptr;
|
||||
use windows::core::{Interface, GUID};
|
||||
use windows::core::Interface;
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
use windows::core::GUID;
|
||||
use windows::Win32::d3d11::{
|
||||
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Multithread, ID3D11Texture2D,
|
||||
ID3D11VideoContext1, ID3D11VideoDevice, ID3D11VideoProcessor, ID3D11VideoProcessorEnumerator,
|
||||
@@ -71,10 +80,14 @@ use windows::Win32::dxgi::{
|
||||
DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P601, DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P709,
|
||||
DXGI_COLOR_SPACE_YCBCR_STUDIO_G2084_LEFT_P2020, DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P2020,
|
||||
DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P601, DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709,
|
||||
DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_NV12, DXGI_FORMAT_P010,
|
||||
DXGI_FORMAT_R10G10B10A2_UNORM, DXGI_RATIONAL, DXGI_SAMPLE_DESC, DXGI_SHARED_RESOURCE_READ,
|
||||
DXGI_SHARED_RESOURCE_WRITE,
|
||||
DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R10G10B10A2_UNORM, DXGI_RATIONAL, DXGI_SAMPLE_DESC,
|
||||
DXGI_SHARED_RESOURCE_READ, DXGI_SHARED_RESOURCE_WRITE,
|
||||
};
|
||||
// The decode-surface formats are named only by the libavcodec rung's adapter probe here —
|
||||
// the native rung declares its own pool formats in `video_d3d11_native` — so they ride
|
||||
// `ffmpeg-fallback` with it.
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
use windows::Win32::dxgi::{DXGI_FORMAT, DXGI_FORMAT_NV12, DXGI_FORMAT_P010};
|
||||
use windows::Win32::windef::RECT;
|
||||
use windows::Win32::winnt::HANDLE;
|
||||
|
||||
@@ -90,16 +103,27 @@ const RING_SLOTS: usize = 6;
|
||||
const ACQUIRE_TIMEOUT_MS: u32 = 2000;
|
||||
|
||||
/// Probe pool size — mirrors what libavcodec sizes for a worst-case DPB (legacy value).
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
const DECODE_POOL_SIZE: i32 = 12;
|
||||
|
||||
/// `D3D11_BIND_DECODER` — the decode pool's ONLY bind flag (see `get_format_d3d11`).
|
||||
///
|
||||
/// The NATIVE rung has its own copy of this fact ([`crate::video_d3d11_native`] builds its
|
||||
/// pool with `D3D11_BIND_DECODER` and nothing else, and pf-dxvadec's tests pin it), so this
|
||||
/// one belongs to the libavcodec probe alone and rides `ffmpeg-fallback` with it.
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
const BIND_DECODER: u32 = 0x200;
|
||||
|
||||
// DXVA decode-profile GUIDs (`dxva.h`), defined locally so no extra windows-rs feature or
|
||||
// metadata surface is pulled in for four constants.
|
||||
// metadata surface is pulled in for four constants. The native rung reads its profile GUIDs
|
||||
// from `pf_dxvadec` (unit-tested there), so these belong to the FFmpeg rung's adapter probe.
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
const PROFILE_H264_VLD_NOFGT: GUID = GUID::from_u128(0x1b81be68_a0c7_11d3_b984_00c04f2e73c5);
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
const PROFILE_HEVC_VLD_MAIN: GUID = GUID::from_u128(0x5b11d51b_2f4c_4452_bcc3_09f2a1160cc0);
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
const PROFILE_HEVC_VLD_MAIN10: GUID = GUID::from_u128(0x107af0e0_ef1a_4d19_aba8_67a163073d13);
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
const PROFILE_AV1_VLD_PROFILE0: GUID = GUID::from_u128(0xb8be4ccb_cf53_46ba_8d59_d6b8a6da5d2a);
|
||||
|
||||
/// One decoded frame, parked in a ring slot the presenter imports by NT handle. Plain POD —
|
||||
@@ -146,10 +170,17 @@ pub struct D3d11Frame {
|
||||
}
|
||||
|
||||
// --- FFmpeg hwcontext_d3d11va ABI (repr(C) mirrors, same as the legacy decoder) --------------
|
||||
//
|
||||
// Everything from here to `create_device` below is the libavcodec HALF of this module — the
|
||||
// `ffmpeg-fallback` rung (M9). It is gated item by item rather than moved to its own file
|
||||
// because the two halves share this module's docs, its constants and its hand-off ring, and
|
||||
// because nothing in this tree COMPILES `cfg(windows)` code: a file move here could not be
|
||||
// checked by any gate, an attribute can at least be read against the item it sits on.
|
||||
|
||||
/// `hwcontext_d3d11va.h` — `AVHWDeviceContext::hwctx` for D3D11VA. FFmpeg installs the
|
||||
/// `ID3D11Multithread` default lock + multithread protection during init, which is what lets
|
||||
/// the presenter-side device share textures with the decode thread safely.
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
#[repr(C)]
|
||||
struct AVD3D11VADeviceContext {
|
||||
device: *mut c_void, // ID3D11Device*
|
||||
@@ -164,6 +195,7 @@ struct AVD3D11VADeviceContext {
|
||||
/// `hwcontext_d3d11va.h` — `AVHWFramesContext::hwctx`. A user-built frames context gets NO
|
||||
/// default bind flags (BindFlags 0 → `CreateTexture2D` E_INVALIDARG); only the probe below
|
||||
/// builds one, and it sets `BIND_DECODER` exactly like libavcodec's own path.
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
#[repr(C)]
|
||||
struct AVD3D11VAFramesContext {
|
||||
texture: *mut c_void, // ID3D11Texture2D* (null → FFmpeg allocates the pool)
|
||||
@@ -179,6 +211,7 @@ struct AVD3D11VAFramesContext {
|
||||
// contexts (pf-encode's `ffmpeg_win.rs` and pf-client-core's `video_d3d11.rs`); they must agree
|
||||
// with libav AND with each other, and these assertions are what makes a drift in either a build
|
||||
// failure instead of a runtime mystery.
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
const _: () = {
|
||||
use std::mem::{offset_of, size_of};
|
||||
type P = *mut c_void;
|
||||
@@ -198,6 +231,7 @@ const _: () = {
|
||||
assert!(offset_of!(AVD3D11VAFramesContext, texture_infos) == 2 * size_of::<P>());
|
||||
};
|
||||
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
fn averr(what: &str, code: i32) -> anyhow::Error {
|
||||
anyhow!("{what}: {}", ffmpeg::Error::from(code))
|
||||
}
|
||||
@@ -209,6 +243,7 @@ fn averr(what: &str, code: i32) -> anyhow::Error {
|
||||
/// sizing, and the decoder-only `D3D11_BIND_DECODER` flags. A hand-built context validated on
|
||||
/// NVIDIA was rejected by Intel at the first `SubmitDecoderBuffers` (E_INVALIDARG) — the
|
||||
/// vendor-proof path is the one the ffmpeg CLI/mpv ship.
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
unsafe extern "C" fn get_format_d3d11(
|
||||
avctx: *mut ffmpeg::ffi::AVCodecContext,
|
||||
mut list: *const ffmpeg::ffi::AVPixelFormat,
|
||||
@@ -235,6 +270,7 @@ unsafe extern "C" fn get_format_d3d11(
|
||||
/// FFmpeg hwdevice because hwaccel selection (`get_format`) only runs on the FIRST access
|
||||
/// unit — an unsupported profile would otherwise burn the opening IDR and recover through the
|
||||
/// mid-stream demotion path instead of committing to software up front.
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
fn decode_profile_supported(device: &ID3D11Device, codec_id: ffmpeg::codec::Id) -> Result<()> {
|
||||
let video: ID3D11VideoDevice = device
|
||||
.cast()
|
||||
@@ -280,6 +316,7 @@ fn decode_profile_supported(device: &ID3D11Device, codec_id: ffmpeg::codec::Id)
|
||||
/// creates the real NV12 decode surface array. On a GPU/driver that can't create the pool this
|
||||
/// fails here, up front, so the session commits to software from the first frame (a clean,
|
||||
/// gap-free stream) instead of dying mid-stream on the opening IDR.
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
unsafe fn d3d11va_decode_supported(hw_device: *mut ffmpeg::ffi::AVBufferRef) -> bool {
|
||||
use ffmpeg::ffi::*;
|
||||
// SAFETY: `hw_device` is a valid `AVBufferRef` by this fn's contract; the frames context is
|
||||
@@ -819,6 +856,7 @@ impl HandoffRing {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
pub(crate) struct D3d11vaDecoder {
|
||||
ctx: *mut ffmpeg::ffi::AVCodecContext,
|
||||
/// The D3D11VA hwdevice, owned. Nothing reads this field after construction — the codec context
|
||||
@@ -841,6 +879,7 @@ pub(crate) struct D3d11vaDecoder {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
// SAFETY: the libav pointers are this decoder's own allocations (freed once in `Drop`) and the COM
|
||||
// interfaces it holds are reference-counted with interlocked counts, so moving the whole struct to
|
||||
// another thread and releasing it there is sound. D3D11's immediate context is not thread-SAFE but
|
||||
@@ -849,6 +888,7 @@ pub(crate) struct D3d11vaDecoder {
|
||||
// textures through their NT handles on its own device. Moved, never shared; deliberately NOT `Sync`.
|
||||
unsafe impl Send for D3d11vaDecoder {}
|
||||
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
impl D3d11vaDecoder {
|
||||
pub(crate) fn new(
|
||||
codec_id: ffmpeg::codec::Id,
|
||||
@@ -1007,6 +1047,7 @@ impl D3d11vaDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffmpeg-fallback")]
|
||||
impl Drop for D3d11vaDecoder {
|
||||
fn drop(&mut self) {
|
||||
use ffmpeg::ffi;
|
||||
|
||||
@@ -12,11 +12,20 @@
|
||||
//!
|
||||
//! # Admission
|
||||
//!
|
||||
//! Explicit pin only — `PUNKTFUNK_DECODER=native-d3d11va`. It is NOT in the automatic ladder
|
||||
//! and must not be until it has hardware evidence: M2's native Vulkan rung was admitted to
|
||||
//! `auto` only after WP-D closed bit-exact against libavcodec on three drivers plus a
|
||||
//! 92-minute soak, and this rung has decoded nothing yet. A refusal or an init failure logs
|
||||
//! and falls through to the standard ladder, so the pin can never cost a session its decoder.
|
||||
//! `PUNKTFUNK_DECODER=native-d3d11va` reaches every leg of this rung, in every build. `auto`
|
||||
//! is per-CODEC and per-evidence since M9 (`video::native_rung_admitted`, and the evidence
|
||||
//! table in `video`'s module docs):
|
||||
//!
|
||||
//! * **H.264 and H.265 are in `auto`** — frame-hash parity against libavcodec on an RTX 4090
|
||||
//! and an AMD iGPU plus a 30-minute soak (M5). They sit directly above the libavcodec
|
||||
//! D3D11VA rung wherever the ladder reaches DXVA.
|
||||
//! * **AV1 is not** — it was wired in M7 and has decoded nothing on any hardware, so `auto`
|
||||
//! skips it and an AV1 session lands on the FFmpeg rung exactly as it did before. It joins
|
||||
//! when the evidence exists, or in a build with no `ffmpeg-fallback` rung below it (there
|
||||
//! the alternative is the CPU, and the session log says so at `warn`).
|
||||
//!
|
||||
//! A refusal or an init failure logs and falls through to the standard ladder, so neither the
|
||||
//! pin nor the `auto` admission can cost a session its decoder.
|
||||
//!
|
||||
//! # The decode pool — the part that has already failed once
|
||||
//!
|
||||
|
||||
@@ -71,9 +71,13 @@ use crate::video_color::ColorDesc;
|
||||
|
||||
/// `PUNKTFUNK_DECODER=native-vaapi` — the pin that selects this rung.
|
||||
///
|
||||
/// Pin-only, like M5's native D3D11VA rung was at the same stage and for the same
|
||||
/// reason: the native Vulkan rung joined `auto` only after bit-exact parity on
|
||||
/// several drivers and a long soak, and this one has decoded nothing yet.
|
||||
/// The pin reaches this rung in EVERY build. `auto` is the conditional one: this rung
|
||||
/// has decoded nothing on any hardware, so `video::native_rung_admitted` lets it into
|
||||
/// `auto` only where no proven rung is left below it (a build without the
|
||||
/// `ffmpeg-fallback` FFmpeg rungs) or where the user asked
|
||||
/// (`PUNKTFUNK_NATIVE_FIRST=1`). The pin staying unconditional is what makes the
|
||||
/// missing evidence GENERATABLE — a rule that gated the pin too would be a rule no
|
||||
/// hardware run could ever satisfy.
|
||||
pub(crate) const DECODER_PIN: &str = "native-vaapi";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
//! HEVC by M3 WP-2 and to AV1 by M7): pf-vkdecode's
|
||||
//! [`VkH264Decoder`]/[`VkH265Decoder`]/[`VkAv1Decoder`] running on the PRESENTER's own
|
||||
//! VkDevice — the same zero-copy shape as the FFmpeg-Vulkan backend, with no FFmpeg in
|
||||
//! the path. Auto's rung immediately ABOVE FFmpeg-Vulkan since the 2026-08-05 ladder
|
||||
//! decision (WP-D closed bit-exact — the program is dropping FFmpeg from the client),
|
||||
//! also pinnable via `PUNKTFUNK_DECODER=native-vulkan`; `video::native_vulkan_gate` is
|
||||
//! the admission either way, and a failure falls through to the FFmpeg-Vulkan rung.
|
||||
//! **AV1 is reachable by the PIN only** — that rung has decoded nothing on hardware,
|
||||
//! so it is absent from every `auto` arm on the same rule M5's native D3D11VA and M6's
|
||||
//! native VAAPI rungs follow (`video::native_vulkan_gate` is where that lives).
|
||||
//! the path. Auto's TOP rung on both desktop OSes since M9, for ALL THREE codecs — each
|
||||
//! leg has bit-exact parity against libavcodec (H.264/H.265 on three drivers plus a
|
||||
//! 92-minute soak, M2/M3; AV1 250/250 on an RTX 5070 Ti, M7 — `video`'s evidence table
|
||||
//! holds the record) — also pinnable via `PUNKTFUNK_DECODER=native-vulkan`;
|
||||
//! `video::native_vulkan_gate` is the admission either way, and a failure falls through
|
||||
//! to the rung below (FFmpeg-Vulkan where `ffmpeg-fallback` compiled it, the platform's
|
||||
//! native rung otherwise).
|
||||
//!
|
||||
//! **Codec dispatch:** the negotiated codec picks the decoder ONCE, at construction
|
||||
//! ([`Codec`]) — H.264, H.265 or AV1, the three codecs pf-vkdecode speaks. The
|
||||
@@ -69,10 +69,9 @@
|
||||
//! read here as a CLEAN access unit, and a clean AU clears `video.rs`'s demotion
|
||||
//! streak. A rung whose every key frame fails would then never demote — one error
|
||||
//! per key frame, zeroed by the skipped frames between them — and the `!delivered`
|
||||
//! fall-through to FFmpeg-Vulkan, the documented backstop for a level above
|
||||
//! fall-through to the rung below, the documented backstop for a level above
|
||||
//! `maxLevelIdc`, a sequence header disagreeing with the Welcome and (AV1 only)
|
||||
//! film grain, would be unreachable. All three codecs demote identically here, and
|
||||
//! only the H.26x paths have hardware evidence.
|
||||
//! film grain, would be unreachable. All three codecs demote identically here.
|
||||
//!
|
||||
//! **Queue lock:** pf-vkdecode submits on queue 0 of the decode family
|
||||
//! ([`DECODE_QUEUE_INDEX`] — the presenter creates exactly one queue per family). When
|
||||
@@ -246,9 +245,10 @@ impl pf_vkdecode::QueueLock for NativeQueueLock {
|
||||
/// this module knowing about FFmpeg's codec ids (and `video::native_vulkan_gate`
|
||||
/// stays the single admission decision).
|
||||
///
|
||||
/// Being IN this enum is not the same as being in `auto`: `Av1` is pin-only until it
|
||||
/// has hardware evidence, and `video::native_vulkan_gate` — not this list — is where
|
||||
/// that decision lives.
|
||||
/// Being IN this enum is not the same as being in `auto`: this list says pf-vkdecode has
|
||||
/// a decoder, `video::native_vulkan_gate` (through `video::native_rung_admitted` and the
|
||||
/// evidence table) says whether the automatic ladder may pick it. All three legs are in
|
||||
/// `auto` since M9, and that is the gate's decision to change, not this list's.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum NativeCodec {
|
||||
H264,
|
||||
|
||||
@@ -53,6 +53,17 @@ windows-sys = { version = "0.61", features = [
|
||||
# backend in pf-client-core; ON by default, matching pf-client-core's default.
|
||||
default = ["pyrowave"]
|
||||
pyrowave = ["pf-client-core/pyrowave"]
|
||||
# Forwards M9's `ffmpeg-fallback` (the libavcodec decode rungs) to pf-client-core. OFF
|
||||
# here on purpose — deliberately NOT in `default` — so `cargo clippy -p pf-presenter`
|
||||
# compiles this crate against the native-only ladder M10 will leave behind. The binary
|
||||
# that ships turns it on: `clients/session` has it in ITS default set, because that is
|
||||
# the crate that actually builds a `Decoder`.
|
||||
#
|
||||
# This crate's own `pf-ffvk` dependency and its `DecodedImage::VkFrame` present lane are
|
||||
# NOT gated by it: they are the FFmpeg-Vulkan frame contract, and they die together with
|
||||
# `crates/pf-ffvk` at M10 (§6), in one move, rather than half-way through a milestone
|
||||
# whose gates cannot run a GPU.
|
||||
ffmpeg-fallback = ["pf-client-core/ffmpeg-fallback"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
Reference in New Issue
Block a user