feat(core,host): CODEC_PYROWAVE negotiation — opt-in only, host dispatch wired
audit / bun-audit (push) Successful in 11s
windows-host / package (push) Failing after 48s
ci / docs-site (push) Successful in 50s
ci / web (push) Successful in 52s
android / android (push) Failing after 58s
decky / build-publish (push) Successful in 32s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 38s
audit / cargo-audit (push) Successful in 2m15s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m43s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m18s
ci / bench (push) Successful in 6m37s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
deb / build-publish (push) Failing after 8m23s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m59s
arch / build-publish (push) Failing after 12m6s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10m10s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12m54s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11m49s
flatpak / build-publish (push) Failing after 8m16s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m46s
ci / rust (push) Failing after 19m32s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 11m33s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 10m56s
docker / deploy-docs (push) Successful in 21s
apple / swift (push) Successful in 4m53s
release / apple (push) Successful in 25m50s
apple / screenshots (push) Successful in 20m55s
audit / bun-audit (push) Successful in 11s
windows-host / package (push) Failing after 48s
ci / docs-site (push) Successful in 50s
ci / web (push) Successful in 52s
android / android (push) Failing after 58s
decky / build-publish (push) Successful in 32s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 38s
audit / cargo-audit (push) Successful in 2m15s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m43s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m18s
ci / bench (push) Successful in 6m37s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
deb / build-publish (push) Failing after 8m23s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m59s
arch / build-publish (push) Failing after 12m6s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10m10s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12m54s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11m49s
flatpak / build-publish (push) Failing after 8m16s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m46s
ci / rust (push) Failing after 19m32s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 11m33s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 10m56s
docker / deploy-docs (push) Successful in 21s
apple / swift (push) Successful in 4m53s
release / apple (push) Successful in 25m50s
apple / screenshots (push) Successful in 20m55s
Phase 2a+2c of design/pyrowave-codec-plan.md. Core: CODEC_PYROWAVE = 0x08 on Hello::video_codecs/Welcome::codec. Deliberately absent from resolve_codec's precedence ladder (plan §3 — a 100-400 Mbps codec must never win a negotiation by mere mutual support): reachable exclusively through the client's explicit preferred_codec. Invariant tests cover never-auto-selected (even as the only shared codec), preferred-path selection, and graceful fallback. ABI mirror PUNKTFUNK_CODEC_PYROWAVE + lockstep assert for the Apple/Android embedders. Host: Codec::PyroWave variant threaded through the wire mappings; a negotiated PyroWave session routes straight to the backend ahead of the PUNKTFUNK_ENCODER pref dispatch (which stays a lab override). The advertisement bit rides host_wire_caps only when the capture side would actually deliver ingestible frames — linux_zero_copy_is_vaapi(), i.e. AMD/Intel auto or an explicit operator pref on NVIDIA; per-session raw-dmabuf OutputFormat plumbing is recorded as the Phase-3 item. The libavcodec name helpers are dispatch-guarded unreachable; the web console gains ApiCodec::PyroWave (api/openapi.json regenerated). Validated on .21: 308 host tests green with and without the feature, 145 core tests green with quic, clippy clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+3
-2
@@ -10,7 +10,7 @@
|
||||
"name": "MIT OR Apache-2.0",
|
||||
"identifier": "MIT OR Apache-2.0"
|
||||
},
|
||||
"version": "0.9.1"
|
||||
"version": "0.11.0"
|
||||
},
|
||||
"paths": {
|
||||
"/api/v1/clients": {
|
||||
@@ -2047,7 +2047,8 @@
|
||||
"enum": [
|
||||
"h264",
|
||||
"h265",
|
||||
"av1"
|
||||
"av1",
|
||||
"pyrowave"
|
||||
]
|
||||
},
|
||||
"ApiDisplayInfo": {
|
||||
|
||||
@@ -928,6 +928,10 @@ pub const PUNKTFUNK_CODEC_H264: u8 = 0x01;
|
||||
pub const PUNKTFUNK_CODEC_HEVC: u8 = 0x02;
|
||||
/// Codec bit: AV1. (Mirrors `quic::CODEC_AV1`.)
|
||||
pub const PUNKTFUNK_CODEC_AV1: u8 = 0x04;
|
||||
/// Codec bit: PyroWave — the opt-in wired-LAN intra-only wavelet codec. Never auto-selected:
|
||||
/// the host picks it ONLY when the client also passes it as `preferred_codec`
|
||||
/// (design/pyrowave-codec-plan.md §3). (Mirrors `quic::CODEC_PYROWAVE`.)
|
||||
pub const PUNKTFUNK_CODEC_PYROWAVE: u8 = 0x08;
|
||||
|
||||
// Keep the ABI cap bits in lockstep with the wire constants (compile-time guard against drift).
|
||||
#[cfg(feature = "quic")]
|
||||
@@ -938,6 +942,7 @@ const _: () = {
|
||||
assert!(PUNKTFUNK_CODEC_H264 == crate::quic::CODEC_H264);
|
||||
assert!(PUNKTFUNK_CODEC_HEVC == crate::quic::CODEC_HEVC);
|
||||
assert!(PUNKTFUNK_CODEC_AV1 == crate::quic::CODEC_AV1);
|
||||
assert!(PUNKTFUNK_CODEC_PYROWAVE == crate::quic::CODEC_PYROWAVE);
|
||||
};
|
||||
|
||||
// Keep the ABI gamepad constants in lockstep with the wire enum (compile-time guard against drift).
|
||||
|
||||
@@ -151,15 +151,28 @@ pub const CODEC_H264: u8 = 0x01;
|
||||
pub const CODEC_HEVC: u8 = 0x02;
|
||||
/// [`Hello::video_codecs`] bit: the client can decode AV1.
|
||||
pub const CODEC_AV1: u8 = 0x04;
|
||||
/// [`Hello::video_codecs`] bit: the client can decode **PyroWave** — the opt-in wired-LAN
|
||||
/// intra-only wavelet codec (design/pyrowave-codec-plan.md; 100–400 Mbps class, 8-bit SDR,
|
||||
/// every frame independently decodable). Deliberately **absent from [`resolve_codec`]'s
|
||||
/// precedence ladder**: it is selected only when the client also names it
|
||||
/// [`Hello::preferred_codec`] (or the host operator forces the advertisement mask) — a codec
|
||||
/// that needs a wired-LAN bitrate must never win a negotiation just because both ends support
|
||||
/// it. The bit means "PyroWave bitstream as of the punktfunk-vendored pin"
|
||||
/// (`crates/pyrowave-sys/vendor/pyrowave/PUNKTFUNK-VENDOR.txt`): upstream has no bitstream
|
||||
/// version field, so a vendored bump that changes the bitstream bumps the punktfunk protocol
|
||||
/// version instead (plan §4.2).
|
||||
pub const CODEC_PYROWAVE: u8 = 0x08;
|
||||
|
||||
/// Resolve which single codec the host will emit, from the client's advertised [`Hello::video_codecs`]
|
||||
/// bitfield (`0` = an older client, treated as HEVC-only) intersected with what the host's chosen
|
||||
/// encoder can produce (`host_capable`, also a bitfield). `preferred` is the client's soft preference
|
||||
/// ([`Hello::preferred_codec`], `0` = none): when it's in the shared set it wins; otherwise the tie is
|
||||
/// broken by **HEVC > AV1 > H.264** (HEVC is the established, best-tested path; H.264 is the
|
||||
/// compatibility / software floor). Returns the single-bit codec value, or `None` when client and host
|
||||
/// share nothing — the caller then refuses the session with a clear error rather than emitting a
|
||||
/// stream the client can't decode.
|
||||
/// compatibility / software floor). [`CODEC_PYROWAVE`] is intentionally NOT in that ladder — it can
|
||||
/// only be returned via the `preferred` path (plan §3: opt-in, pinned, honest). Returns the
|
||||
/// single-bit codec value, or `None` when client and host share nothing the ladder may pick — the
|
||||
/// caller then refuses the session with a clear error rather than emitting a stream the client
|
||||
/// can't decode.
|
||||
pub fn resolve_codec(client_codecs: u8, host_capable: u8, preferred: u8) -> Option<u8> {
|
||||
// An older client (no codec byte) decodes HEVC — the only codec every pre-negotiation build sent.
|
||||
let client = if client_codecs == 0 {
|
||||
|
||||
@@ -101,6 +101,30 @@ fn codec_negotiation_and_back_compat() {
|
||||
// A preference the host can't emit still can't rescue a no-shared-codec case.
|
||||
assert_eq!(resolve_codec(CODEC_HEVC, CODEC_H264, CODEC_HEVC), None);
|
||||
|
||||
// PyroWave is opt-in ONLY (plan §3): mutual support NEVER auto-selects it — the ladder
|
||||
// ignores it entirely...
|
||||
assert_eq!(
|
||||
resolve_codec(CODEC_HEVC | CODEC_PYROWAVE, CODEC_HEVC | CODEC_PYROWAVE, 0),
|
||||
Some(CODEC_HEVC)
|
||||
);
|
||||
// ...even when it is the ONLY shared codec (an all-intra 200 Mbps stream must never be a
|
||||
// silent fallback)...
|
||||
assert_eq!(resolve_codec(CODEC_PYROWAVE, CODEC_PYROWAVE, 0), None);
|
||||
// ...it is reachable exclusively through the client's explicit preference.
|
||||
assert_eq!(
|
||||
resolve_codec(
|
||||
CODEC_HEVC | CODEC_PYROWAVE,
|
||||
CODEC_HEVC | CODEC_PYROWAVE,
|
||||
CODEC_PYROWAVE
|
||||
),
|
||||
Some(CODEC_PYROWAVE)
|
||||
);
|
||||
// A pyrowave preference against a host without the backend falls back to the ladder.
|
||||
assert_eq!(
|
||||
resolve_codec(CODEC_HEVC | CODEC_PYROWAVE, CODEC_HEVC, CODEC_PYROWAVE),
|
||||
Some(CODEC_HEVC)
|
||||
);
|
||||
|
||||
// A Hello advertising codecs roundtrips, and the wire form of a codec-only Hello decodes on
|
||||
// a build that ignores the trailing byte (back-compat: extra bytes are skipped).
|
||||
let h = Hello {
|
||||
|
||||
@@ -37,6 +37,10 @@ pub enum Codec {
|
||||
H264,
|
||||
H265,
|
||||
Av1,
|
||||
/// PyroWave — the opt-in wired-LAN intra-only wavelet codec (design/pyrowave-codec-plan.md).
|
||||
/// Only ever negotiated via the client's explicit `preferred_codec` (never the precedence
|
||||
/// ladder) and only emitted by the `pyrowave`-feature backend; every AU is a keyframe.
|
||||
PyroWave,
|
||||
}
|
||||
|
||||
/// Chroma subsampling the encoder emits, negotiated with the client (the `PUNKTFUNK_444` gate + the
|
||||
@@ -73,6 +77,7 @@ impl Codec {
|
||||
match bit {
|
||||
punktfunk_core::quic::CODEC_H264 => Codec::H264,
|
||||
punktfunk_core::quic::CODEC_AV1 => Codec::Av1,
|
||||
punktfunk_core::quic::CODEC_PYROWAVE => Codec::PyroWave,
|
||||
_ => Codec::H265,
|
||||
}
|
||||
}
|
||||
@@ -83,6 +88,7 @@ impl Codec {
|
||||
Codec::H264 => punktfunk_core::quic::CODEC_H264,
|
||||
Codec::H265 => punktfunk_core::quic::CODEC_HEVC,
|
||||
Codec::Av1 => punktfunk_core::quic::CODEC_AV1,
|
||||
Codec::PyroWave => punktfunk_core::quic::CODEC_PYROWAVE,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +103,29 @@ impl Codec {
|
||||
/// still lands on HEVC for an auto client, exactly the pre-probe behaviour. Fed to
|
||||
/// [`punktfunk_core::quic::resolve_codec`] against the client's advertised codecs.
|
||||
pub fn host_wire_caps() -> u8 {
|
||||
// PyroWave rides ON TOP of whatever H.26x set resolves below: feature-gated, Linux-only
|
||||
// for now (the Windows host leg is blocked on the .173 D3D11 interop debt), and inert in
|
||||
// negotiation unless the client explicitly prefers it (resolve_codec ignores the bit in
|
||||
// its ladder). Advertised only when the capture side would actually deliver frames the
|
||||
// backend ingests (raw-dmabuf passthrough / CPU RGB): `linux_zero_copy_is_vaapi()` —
|
||||
// true on AMD/Intel auto and under an explicit PUNKTFUNK_ENCODER=pyrowave. On an NVIDIA
|
||||
// host with `auto`, capture resolves to the EGL→CUDA import the backend can't consume,
|
||||
// so the bit stays off until the OutputFormat plumbing carries a per-session
|
||||
// raw-dmabuf decision (Phase 3); the operator opts in with the env instead (plan §3).
|
||||
#[cfg(all(target_os = "linux", feature = "pyrowave"))]
|
||||
let pyro = if linux_zero_copy_is_vaapi()
|
||||
&& !matches!(
|
||||
crate::config::config().encoder_pref.as_str(),
|
||||
// A software pref usually means a GPU-less box — no Vulkan device to open.
|
||||
"software" | "sw" | "openh264"
|
||||
) {
|
||||
punktfunk_core::quic::CODEC_PYROWAVE
|
||||
} else {
|
||||
0u8
|
||||
};
|
||||
#[cfg(not(all(target_os = "linux", feature = "pyrowave")))]
|
||||
let pyro = 0u8;
|
||||
let base = (|| {
|
||||
/// The static GPU superset (H.264 | HEVC | AV1) — mirrors the GameStream
|
||||
/// `SERVER_CODEC_MODE_SUPPORT` advertisement for the unprobed backends.
|
||||
const GPU_SUPERSET: u8 = punktfunk_core::quic::CODEC_H264
|
||||
@@ -140,6 +169,8 @@ impl Codec {
|
||||
_ => punktfunk_core::quic::CODEC_HEVC,
|
||||
}
|
||||
}
|
||||
})();
|
||||
base | pyro
|
||||
}
|
||||
|
||||
/// Lowercase stats/console label (`"h264"` / `"hevc"` / `"av1"`) — the codec string seeded into
|
||||
@@ -149,6 +180,7 @@ impl Codec {
|
||||
Codec::H264 => "h264",
|
||||
Codec::H265 => "hevc",
|
||||
Codec::Av1 => "av1",
|
||||
Codec::PyroWave => "pyrowave",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +191,9 @@ impl Codec {
|
||||
Codec::H264 => "h264_nvenc",
|
||||
Codec::H265 => "hevc_nvenc",
|
||||
Codec::Av1 => "av1_nvenc",
|
||||
// Guarded by the open_video dispatch: a PyroWave session never reaches a
|
||||
// libavcodec backend.
|
||||
Codec::PyroWave => unreachable!("PyroWave has no FFmpeg encoder"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +207,9 @@ impl Codec {
|
||||
Codec::H264 => "h264_vaapi",
|
||||
Codec::H265 => "hevc_vaapi",
|
||||
Codec::Av1 => "av1_vaapi",
|
||||
// Guarded by the open_video dispatch: a PyroWave session never reaches a
|
||||
// libavcodec backend.
|
||||
Codec::PyroWave => unreachable!("PyroWave has no FFmpeg encoder"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +220,9 @@ impl Codec {
|
||||
Codec::H264 => "h264_amf",
|
||||
Codec::H265 => "hevc_amf",
|
||||
Codec::Av1 => "av1_amf",
|
||||
// Guarded by the open_video dispatch: a PyroWave session never reaches a
|
||||
// libavcodec backend.
|
||||
Codec::PyroWave => unreachable!("PyroWave has no FFmpeg encoder"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +233,9 @@ impl Codec {
|
||||
Codec::H264 => "h264_qsv",
|
||||
Codec::H265 => "hevc_qsv",
|
||||
Codec::Av1 => "av1_qsv",
|
||||
// Guarded by the open_video dispatch: a PyroWave session never reaches a
|
||||
// libavcodec backend.
|
||||
Codec::PyroWave => unreachable!("PyroWave has no FFmpeg encoder"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -324,7 +368,9 @@ impl Codec {
|
||||
pub fn max_dimension(self) -> u32 {
|
||||
match self {
|
||||
Codec::H264 => 4096,
|
||||
Codec::H265 | Codec::Av1 => 8192,
|
||||
// PyroWave has no codec-level dimension cap (arbitrary even sizes); 8192 matches the
|
||||
// buffer-math guard the other codecs get.
|
||||
Codec::H265 | Codec::Av1 | Codec::PyroWave => 8192,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,6 +385,9 @@ impl Codec {
|
||||
Codec::H264 => 480_000_000,
|
||||
Codec::H265 => 800_000_000,
|
||||
Codec::Av1 => 1_200_000_000,
|
||||
// No spec level/tier: the rate is a plain per-frame byte budget. Use the protocol's
|
||||
// own bitrate clamp so the step-down probe logic never binds below it.
|
||||
Codec::PyroWave => 8_000_000_000,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -523,6 +572,20 @@ fn open_video_backend(
|
||||
};
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// A NEGOTIATED PyroWave session (client advertised + preferred it, plan §3) routes
|
||||
// straight to that backend — the PUNKTFUNK_ENCODER pref below stays a lab override.
|
||||
if codec == Codec::PyroWave {
|
||||
#[cfg(feature = "pyrowave")]
|
||||
{
|
||||
return pyrowave::PyroWaveEncoder::open(width, height, fps, bitrate_bps)
|
||||
.map(|e| (Box::new(e) as Box<dyn Encoder>, "pyrowave"));
|
||||
}
|
||||
#[cfg(not(feature = "pyrowave"))]
|
||||
anyhow::bail!(
|
||||
"session negotiated PyroWave but this host was built without --features \
|
||||
punktfunk-host/pyrowave (the advertisement bit should not have been set)"
|
||||
);
|
||||
}
|
||||
// Pick the GPU encode backend. NVIDIA → NVENC/CUDA (the original path, unchanged);
|
||||
// AMD/Intel → VAAPI (one libavcodec backend for both). Auto-detect by default so a single
|
||||
// Linux binary serves any GPU; `PUNKTFUNK_ENCODER` forces a specific backend (and surfaces
|
||||
@@ -657,6 +720,11 @@ fn open_video_backend(
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// The Windows host leg is blocked on the .173 D3D11-interop debt (plan Phase 0 §3);
|
||||
// host_wire_caps never advertises the bit here, so this only guards a forged preference.
|
||||
if codec == Codec::PyroWave {
|
||||
anyhow::bail!("PyroWave host encode is not available on Windows yet");
|
||||
}
|
||||
let _ = cuda; // always false on Windows (no Cuda payload)
|
||||
// NVIDIA → NVENC (direct SDK), AMD → AMF, Intel → QSV (both libavcodec), else → software
|
||||
// H.264. `auto` (the default) resolves from the selected render adapter's vendor.
|
||||
|
||||
@@ -88,6 +88,8 @@ fn lp_idx(codec: Codec) -> usize {
|
||||
Codec::H264 => 0,
|
||||
Codec::H265 => 1,
|
||||
Codec::Av1 => 2,
|
||||
// Guarded by the open_video dispatch: PyroWave never opens the VAAPI backend.
|
||||
Codec::PyroWave => unreachable!("PyroWave has no VAAPI encoder"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -873,6 +873,8 @@ fn parse_spike(args: &[String]) -> Result<Options> {
|
||||
Codec::H264 => "h264",
|
||||
Codec::H265 => "h265",
|
||||
Codec::Av1 => "obu",
|
||||
// Raw concatenated PyroWave packets — a lab dump, not an FFmpeg-playable stream.
|
||||
Codec::PyroWave => "pyrowave",
|
||||
};
|
||||
PathBuf::from(format!("/tmp/punktfunk-spike.{ext}"))
|
||||
});
|
||||
|
||||
@@ -310,6 +310,8 @@ enum ApiCodec {
|
||||
H264,
|
||||
H265,
|
||||
Av1,
|
||||
/// PyroWave — the opt-in wired-LAN intra-only wavelet codec.
|
||||
PyroWave,
|
||||
}
|
||||
|
||||
impl From<Codec> for ApiCodec {
|
||||
@@ -318,6 +320,7 @@ impl From<Codec> for ApiCodec {
|
||||
Codec::H264 => ApiCodec::H264,
|
||||
Codec::H265 => ApiCodec::H265,
|
||||
Codec::Av1 => ApiCodec::Av1,
|
||||
Codec::PyroWave => ApiCodec::PyroWave,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user