feat(host): native AMF SDK encoder for Windows AMD — drop libavcodec
apple / swift (push) Successful in 1m26s
ci / rust (push) Failing after 38s
ci / web (push) Successful in 52s
windows-host / package (push) Successful in 7m26s
apple / screenshots (push) Successful in 6m8s
decky / build-publish (push) Successful in 13s
android / android (push) Successful in 4m58s
arch / build-publish (push) Successful in 5m31s
ci / docs-site (push) Successful in 1m1s
deb / build-publish (push) Successful in 3m20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
ci / bench (push) Successful in 4m52s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 10m11s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 10m11s
apple / swift (push) Successful in 1m26s
ci / rust (push) Failing after 38s
ci / web (push) Successful in 52s
windows-host / package (push) Successful in 7m26s
apple / screenshots (push) Successful in 6m8s
decky / build-publish (push) Successful in 13s
android / android (push) Successful in 4m58s
arch / build-publish (push) Successful in 5m31s
ci / docs-site (push) Successful in 1m1s
deb / build-publish (push) Successful in 3m20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
ci / bench (push) Successful in 4m52s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 10m11s
docker / deploy-docs (push) Successful in 18s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 10m11s
Direct-SDK AMF encoder (encode/windows/amf.rs), the AMD analogue of the direct-NVENC path, replacing the libavcodec *_amf dispatch. C-vtable FFI pinned to AMF headers v1.4.36, runtime-loaded from the driver's amfrt64.dll (no build feature, no new dependency) exactly as NVENC loads its DLL. - AVC/HEVC (SDR NV12 + 10-bit HDR P010) and AV1 (RDNA3+, probed); a bounded poll retires the libavcodec ~2-frame output hold; native in-place reset(). - Intra-refresh wave (PUNKTFUNK_INTRA_REFRESH), in-band HDR mastering/CLL metadata (*InHDRMetadata -> HEVC SEI / AV1 OBU), and a native codec probe feeding the GameStream advertisement (windows_backend_is_ffmpeg -> windows_backend_is_probed). - AMD dispatch / advertisement / 4:4:4 are native-only; the libavcodec AMF fallback and the PUNKTFUNK_AMF_FFMPEG hatch are removed. FFmpeg serves QSV only (its AMF path retained solely as the latency A/B comparator). - Overload back-pressure: submit bounds in-flight surfaces below the input ring, draining finished AUs (buffered for poll, FIFO-preserved) to free a slot and retry on AMF_INPUT_FULL instead of tearing the encoder down and forcing an IDR; this also closes a latent ring-overwrite corruption seen under load on-glass. Validated on the lab Ryzen iGPU (AMF runtime 1.4.37): HEVC/AVC across a native reset, HEVC Main10 mastering+CLL SEIs byte-verified, intra-refresh accepted, a backpressure burst FIFO-clean, and end-to-end via the macOS client. Measured §5.2 latency A/B: native encode_us p50 ~5 ms (0.31 frame periods) vs libavcodec ~17 ms (1.01). 4:4:4 stays unsupported (VCN hardware limit). Live-gated tests skip cleanly on non-AMD boxes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,28 @@
|
||||
# Native AMF encoder — handoff design
|
||||
|
||||
> **Status: PROPOSED (handoff, 2026-07-06).** Design + decision record for replacing the
|
||||
> libavcodec `*_amf` path with a direct AMF SDK encoder on Windows AMD
|
||||
> (`encode/windows/amf.rs`, the AMD analogue of the direct-SDK `encode/windows/nvenc.rs`).
|
||||
> Written for an implementer without prior context; every in-repo seam is named. Companion
|
||||
> context: the encode-stall watchdog + `Encoder::reset()` (punktfunk1.rs / encode.rs, shipped
|
||||
> 2026-07-06) and the AMF/QSV backend module docs in `encode/windows/ffmpeg_win.rs`.
|
||||
> **Status: PHASES 1 + 2 + 3 IMPLEMENTED (2026-07-06).** `encode/windows/amf.rs` ships the
|
||||
> direct-SDK encoder per §3 — FFI pinned to AMF headers v1.4.36, bounded poll, native `reset()`.
|
||||
> Phase 2: **AV1** (open-time probe gate; per-codec enum divergences honored — AV1 swaps the
|
||||
> ULL/LL usage values and uses GOP=0 + FORCE_FRAME_TYPE_KEY=1), **intra-refresh**
|
||||
> (`PUNKTFUNK_INTRA_REFRESH` opt-in mirroring Linux NVENC; `caps().intra_refresh` reflects the
|
||||
> driver's actual acceptance), **in-band HDR mastering/CLL metadata** (`*InHDRMetadata` host
|
||||
> buffer; HEVC + AV1), and the **native codec probe**. Phase 3: **the ffmpeg-AMF dispatch
|
||||
> fallback + `PUNKTFUNK_AMF_FFMPEG` hatch are deleted** — AMD dispatch / codec advertisement /
|
||||
> 4:4:4 answer are native-only; FFmpeg serves QSV only (`ffmpeg_win.rs`'s AMF machinery is kept
|
||||
> solely as the A/B comparator). `windows_backend_is_ffmpeg` → `windows_backend_is_probed`. **The
|
||||
> §7 field-silence gate on Phase 3 was pre-empted on explicit direction** — see the §7 gate note
|
||||
> for what that costs (VP-format-fallback now fails the session; AMFVideoConverter is the owed
|
||||
> native fix).
|
||||
> Live-validated on the lab Ryzen iGPU (VCN 3): AVC + HEVC batches across a native in-place reset
|
||||
> (Annex-B IDR contract, FIFO pairing); HEVC Main10 P010 with the mastering + CLL prefix SEIs
|
||||
> **confirmed present in the encoded IDR**; intra-refresh property accepted on both codecs; probe
|
||||
> honestly answers h264/h265=true, av1=false on this RDNA2 part. The **§5.2 latency A/B is
|
||||
> measured** (`amf_latency_ab_bench`, 1080p60 HEVC): native `encode_us` p50 **5.18 ms (0.31 frame
|
||||
> periods)** vs libavcodec-AMF **16.9 ms (1.01)** — 3.3× lower, the frame-hold gone. Owed: §5.3
|
||||
> on-glass session behaviors + soak (macOS-client on-glass test in progress), and field
|
||||
> validation on ≥2 VCN generations (AV1/RDNA3 has no lab hardware).
|
||||
> Companion context: the encode-stall watchdog + `Encoder::reset()` (punktfunk1.rs / encode.rs,
|
||||
> shipped 2026-07-06) and the QSV backend module docs in `encode/windows/ffmpeg_win.rs`.
|
||||
|
||||
## 1. Why (measured, not speculative)
|
||||
|
||||
@@ -142,8 +159,21 @@ log-and-continue, not fatal — availability varies by VCN generation/driver.
|
||||
|
||||
### 3.5 Error + stall semantics (interplay with the 2026-07-06 watchdog)
|
||||
|
||||
- `SubmitInput` → `AMF_INPUT_FULL`: return `Err` from `submit` — the session loop's
|
||||
submit-failure path runs the bounded in-place reset. Any other non-OK result: `Err`.
|
||||
- `SubmitInput` → `AMF_INPUT_FULL`: **back-pressure, NOT a wedge — drain and retry, do not
|
||||
reset.** (Original prescription "return `Err` → in-place reset" was **wrong**, disproven
|
||||
on-glass 2026-07-06: at throughput-ceiling loads — 5120x1440@240 P010 on the lab iGPU —
|
||||
`INPUT_FULL` → reset → forced IDR → a bigger keyframe → worse overload → a ~320 ms
|
||||
reset/IDR cascade, strictly worse than the libavcodec path's 16-deep input queue riding it
|
||||
out as latency. The log showed dozens of `submit failed … AMF_INPUT_FULL … rebuilt in place`
|
||||
and **zero** watchdog stalls.) The shipped handling: `submit` bounds in-flight surfaces below
|
||||
the input ring depth (`pending.len() < RING`) by draining finished AUs (buffered in a `ready`
|
||||
deque for `poll`, FIFO-preserved) to free a slot *before* reusing it, and treats a stray
|
||||
`INPUT_FULL` from `SubmitInput` the same way (drain + retry the surface). Only a drain that
|
||||
makes NO progress for a bounded budget (`INPUT_DRAIN_BUDGET`, 200 ms — well under the
|
||||
session watchdog's ~2 s) is a genuine wedge that escalates to `Err` → the in-place reset. This
|
||||
also closed a **latent corruption**: the old path let in-flight grow to AMF's internal input
|
||||
queue limit (16) against a ring of 4, so surfaces referenced ring slots already overwritten —
|
||||
the reset masked it. Any other non-OK `SubmitInput` result: `Err`.
|
||||
- `QueryOutput` → `AMF_REPEAT`: keep spinning within the poll budget, then `Ok(None)`.
|
||||
`AMF_EOF`: `Ok(None)` after flush. Anything else: `Err` (the loop's poll-error path
|
||||
resets).
|
||||
@@ -189,13 +219,27 @@ Baseline first, on the ffmpeg path (already deployed 2026-07-06 with the watchdo
|
||||
long session on the iGPU with `PUNKTFUNK_PERF=1`, record `wait_us_p50/p99`,
|
||||
`encode_us`, client-measured latency, and whether the watchdog ever fires. Then per phase:
|
||||
|
||||
1. Open/probe smoke per codec (AVC, HEVC, HEVC-10) on the iGPU.
|
||||
1. Open/probe smoke per codec (AVC, HEVC, HEVC-10) on the iGPU. **DONE** — the gated live
|
||||
tests in `amf.rs` (`amf_encode_live_smoke` AVC+HEVC+AV1-probe, `amf_hdr_encode_live_smoke`,
|
||||
`amf_native_probe_live`, `amf_intra_refresh_property_live`) pass on the lab Ryzen iGPU
|
||||
(VCN3/RDNA2): both codecs across a native `reset()`, HEVC Main10 IDR carrying the
|
||||
mastering(137)+CLL(144) SEIs byte-verified, intra-refresh property accepted, probe honestly
|
||||
`h264/h265=true, av1=false`.
|
||||
2. A/B the encode latency: expect `encode_us` p50 ~2 frame periods → ≤ 1 frame period
|
||||
(see §3.3 for why `wait_us` is the wrong metric on the ffmpeg side); client
|
||||
capture→glass p50 drops by ~30 ms at 60 Hz / ~17 ms at 120 Hz. Zero-copy baseline for
|
||||
the input side already measured 2026-07-06 on the lab iGPU (1080p120 HDR P010):
|
||||
`submit_us` p50 2.7–2.9 ms (system readback) → **0.26 ms** (zero-copy D3D11), p99
|
||||
6.6 ms → 0.5 ms.
|
||||
(see §3.3 for why `wait_us` is the wrong metric on the ffmpeg side). **MEASURED**
|
||||
2026-07-06 by the gated `amf_latency_ab_bench` (`PUNKTFUNK_AMF_BENCH=1`, 1080p60 HEVC,
|
||||
180 paced frames, same D3D11 NV12 input to both encoders, lab iGPU, debug build):
|
||||
native `encode_us` p50 **5.18 ms (0.31 frame periods)** / p99 5.81 ms vs libavcodec-AMF
|
||||
p50 **16.9 ms (1.01 frame periods)** / p99 17.5 ms — **3.3× lower, ~11.7 ms/frame saved**,
|
||||
and the native path is decisively sub-frame (the ~2-frame hold that used to live in
|
||||
`encode_us` is gone). Note the ffmpeg baseline came in at ~1 frame period, not the ~2 this
|
||||
plan projected: the shipping ffmpeg config already sets AMF `latency=true` (a ~1-frame
|
||||
hold), so the realized win is 3.3× / ~12 ms rather than the ~30 ms projected against an
|
||||
un-tuned 2-frame baseline; direction and sub-frame collapse are exactly as §3.3 described.
|
||||
Release builds should show a lower native number still (debug charges host-side
|
||||
surface-create + copy-submit into the 5.18 ms). Zero-copy baseline for the input side
|
||||
already measured 2026-07-06 on the lab iGPU (1080p120 HDR P010): `submit_us` p50 2.7–2.9 ms
|
||||
(system readback) → **0.26 ms** (zero-copy D3D11), p99 6.6 ms → 0.5 ms.
|
||||
3. Behavior: IDR on connect; mode switch mid-stream; HDR session (PQ VUI + 0xCE
|
||||
convergence); client keyframe-request recovery; encoder `reset()` under an injected
|
||||
failure; ≥30 min soak for the freeze class (watchdog log line
|
||||
@@ -221,8 +265,23 @@ long session on the iGPU with `PUNKTFUNK_PERF=1`, record `wait_us_p50/p99`,
|
||||
|
||||
## 7. Phasing
|
||||
|
||||
| Phase | Scope | Exit criterion |
|
||||
| --- | --- | --- |
|
||||
| 1 | FFI layer + AVC/HEVC (SDR + 10-bit HDR), bounded poll, native `reset()`, dispatch with ffmpeg fallback + `PUNKTFUNK_AMF_FFMPEG` hatch | §5.2–5.3 pass on the lab iGPU |
|
||||
| 2 | Intra-refresh (`caps().intra_refresh`), in-band HDR SEI (`supports_hdr_metadata`), AV1, native codec probe | field-validated on ≥2 VCN generations |
|
||||
| 3 | Delete the ffmpeg-AMF dispatch arm + hatch; FFmpeg remains QSV-only | one release of field silence on the fallback label |
|
||||
| Phase | Scope | Exit criterion | Status |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | FFI layer + AVC/HEVC (SDR + 10-bit HDR), bounded poll, native `reset()`, dispatch with ffmpeg fallback + `PUNKTFUNK_AMF_FFMPEG` hatch | §5.2–5.3 pass on the lab iGPU | **DONE** 2026-07-06 (§5.2 measured; §5.3 on-glass in progress) |
|
||||
| 2 | Intra-refresh (`caps().intra_refresh`), in-band HDR SEI (`supports_hdr_metadata`), AV1, native codec probe | field-validated on ≥2 VCN generations | **CODE DONE** 2026-07-06 (lab VCN3 only; AV1/RDNA3 + 2nd VCN gen still owed) |
|
||||
| 3 | Delete the ffmpeg-AMF dispatch arm + hatch; FFmpeg remains QSV-only | one release of field silence on the fallback label | **DONE** 2026-07-06 — see the gate note below |
|
||||
|
||||
**Phase 3 gate note (honesty):** the stated exit criterion (one release of field silence on the
|
||||
fallback label) was **NOT met** — Phase 3 was cut the same day the native path was written, on
|
||||
explicit direction, alongside a live macOS-client on-glass test. What Phase 3 removed: the
|
||||
`WindowsBackend::Amf` libavcodec fallback arm, the `PUNKTFUNK_AMF_FFMPEG` hatch, and the
|
||||
AMF→ffmpeg routes in `windows_codec_support` / `can_encode_444`. AMD dispatch, codec
|
||||
advertisement, and the 4:4:4 answer are all native-only now; FFmpeg (`ffmpeg_win.rs`) is reached
|
||||
only for QSV in production (its `WinVendor::Amf` machinery is retained solely as the
|
||||
`amf_latency_ab_bench` comparator, not deleted — excising it would churn the Intel-unvalidated
|
||||
QSV code for no gain). **Residual risk this pre-emption carries:** with the ffmpeg readback path
|
||||
gone, an AMD box whose capturer can't produce video-processor NV12/P010 (falls back to
|
||||
Bgra/Rgb10a2, or hands DDA CPU frames) now **fails the session** instead of degrading — the
|
||||
design's answer is the native AMFVideoConverter front-end (§3.2), owed if that fallback is ever
|
||||
seen in the field. Not observed on lab hardware (the VP yields NV12/P010). Reverting Phase 3 is a
|
||||
small, localized diff if field data turns up trouble.
|
||||
|
||||
Reference in New Issue
Block a user