A Steam Deck could lose HEVC entirely to a chroma switch nothing checked — and the tool you'd triage it with denied the queue it was decoding on #127

Merged
enricobuehler merged 1 commits from worktree-deck-hevc-shape-gates into main 2026-08-08 22:20:41 +00:00
Owner

Field report (2026-08-08, end user on a Steam Deck): "Sometimes I got an error saying that the decoder was not found and then it fell back to h264, but other times it worked."

Two separate defects, one of which explains the report and one of which would have sent the triage the wrong way.

1. The 4:4:4 advertisement was a promise nothing checked

VIDEO_CAP_444 rode the "Full chroma" setting alone, with a comment saying the software rung was the floor underneath it. M8 removed that floor — there is no permissively licensed software HEVC decoder, so software_decodable_codecs() is H264 | AV1. The host grants 4:4:4 on HEVC only, and answers the resolved chroma in the Welcome before the client builds a decoder.

So on a device with no 4:4:4 decode, the toggle did not cost crispness — it cost the entire codec:

Welcome resolves 4:4:4  →  Vulkan rung refuses the shape at construction
                        →  VAAPI refuses it too  →  no CPU rung for HEVC
                        →  NoSoftwareRung  →  reconnect advertising H.264 only
                        →  "HEVC decoding failed on this device — reconnecting"

AMD has no HEVC 4:4:4 decode on any silicon (verified two independent ways — see the vendor matrix work), the Deck included. H.264 never shows this symptom because it has a CPU rung underneath it.

Why it looked intermittent: enable_444 is per-profile and defaults off — "a 'Work' profile wants it; 'Game' usually doesn't". Same Deck, same host, same session: the Work profile loses HEVC where Game keeps it. A second source of variance is the host's own 4:4:4 encode probe, which only NVIDIA passes.

The fix

Gated on hevc_444_hardware_decodable, which asks the driver through the same code the rung uses at construction (VkH265Decoder::probe_stream_support) — so the advertisement and the rung that has to honour it cannot disagree. It costs a handful of capability queries, no session, no images, no submits, and && short-circuits so a box that never enabled Full chroma pays nothing.

Both depths are required, not either. With HDR on the host may resolve 4:4:4 10-bit, and a device offering YUV444_8 but not YUV444_10 would land in exactly the hole this closes.

Answering from the Vulkan rung alone is exact, not approximate — it is the only rung in this build that implements 4:4:4 at all: pf_vaadec::profile_for maps only chroma_format_idc == 1 and errors UnsupportedShape on 3; pf_dxvadec's config refuses "anything but 4:2:0" by construction; the CPU rung is 8-bit 4:2:0. That is also why an Intel box — whose hardware has done HEVC 4:4:4 since Ice Lake — is still false here: our DXVA/VAAPI rungs do not implement it, so advertising it would be a lie about us, not about the GPU.

The user is not left guessing: a warn names the setting, the reason, and PyroWave as the 4:4:4 path that works on any GPU.

One deliberate non-change: VIDEO_CAP_10BIT / VIDEO_CAP_HDR

These are advertised unprobed for the same reason 4:4:4 was, and I left them alone on purpose. The asymmetry is real: all three hardware rungs implement 10-bit 4:2:0 (profile_for maps (H265, 1, 10) and (Av1, 1, 10); pf-dxvadec carries P010), so a Vulkan-only probe there would answer false on boxes whose VAAPI/DXVA rung decodes 10-bit perfectly and would silently withdraw HDR from them — a visible regression bought against a case never observed. Gating 10-bit honestly needs a libva/D3D11 probe this path cannot afford, for the same reason av1_hardware_decodable does not consult VAAPI. Measured on a Deck: HEVC Main and Main 10 both offer the full format set, so 10-bit was never this bug.

2. --probe-decode described a different device from the one that streams

The RADV video-decode opt-in sat after the --list-adapters / --probe-decode / --list-audio / --pair early exits, so the triage tool never had it. Measured on a Deck (canary e22af40f), same binary, back to back:

invocation verdict
--probe-decode vulkan video decode: no · driver decode ops: none (0x0) · no queue family advertises VIDEO_DECODE
RADV_PERFTEST=video_decode … --probe-decode vulkan video decode: YES · H.264, H.265, AV1, VP9 (0xF)

The tool exists to be believed. Any Deck triage that consulted it reached the opposite of the truth — including, briefly, this one. Hoisted to the top of run, ahead of every early exit; nothing touches Vulkan before it (main calls run directly).

Shape of the change

The bit arithmetic moves into video::video_caps_for — a pure function — so the part that was actually wrong is testable without a GPU, a host or a Hello, in the spirit of last_rung_verdict. The device question stays with the caller, which owns the expensive probe and logs its own refusal with the user's setting in hand.

Verification

Run in the pf-lxcheck2 Linux container (pf-client-core does not build on macOS at all):

  • cargo fmt --all -- --check — clean
  • cargo build (plain, not only --all-targets — that hides "dead outside tests") — clean
  • cargo clippy --all-targets -- -D warnings — clean
  • cargo test -p pf-client-core185 passed, 0 failed

The new test is verified non-vacuous: planting the original defect (if want_444if true) fails it with exactly the intended assertion — a 4:4:4 promise this device cannot keep costs HEVC entirely, left: 4, right: 0.

Re-run green after rebasing onto e22082ac; no overlap with anything that landed meanwhile.

What is NOT verified

  • The fixed binary has never run on a Deck. Fix 2's outcome is bracketed by the two real Deck measurements above plus std::env::set_varsetenv(3) and verified ordering, so it is determined rather than guessed — but the on-glass confirmation is owed. It needs new packages in a dev container, so it was left rather than done half-way.
  • Fix 1's Some(vk) arm needs a GPU and has no test; only the None arm and the bit arithmetic are covered here.
  • The reporting user's log and settings were never obtained, so which mechanism fired for them is unconfirmed. The confirming evidence would be: whether their profile had Full chroma on, and which host they dialled.
Field report (2026-08-08, end user on a Steam Deck): *"Sometimes I got an error saying that the decoder was not found and then it fell back to h264, but other times it worked."* Two separate defects, one of which explains the report and one of which would have sent the triage the wrong way. ## 1. The 4:4:4 advertisement was a promise nothing checked `VIDEO_CAP_444` rode the "Full chroma" setting alone, with a comment saying the software rung was the floor underneath it. **M8 removed that floor** — there is no permissively licensed software HEVC decoder, so `software_decodable_codecs()` is `H264 | AV1`. The host grants 4:4:4 on **HEVC only**, and answers the resolved chroma in the Welcome *before* the client builds a decoder. So on a device with no 4:4:4 decode, the toggle did not cost crispness — it cost the entire codec: ``` Welcome resolves 4:4:4 → Vulkan rung refuses the shape at construction → VAAPI refuses it too → no CPU rung for HEVC → NoSoftwareRung → reconnect advertising H.264 only → "HEVC decoding failed on this device — reconnecting" ``` AMD has **no HEVC 4:4:4 decode on any silicon** (verified two independent ways — see the vendor matrix work), the Deck included. H.264 never shows this symptom because it has a CPU rung underneath it. **Why it looked intermittent:** `enable_444` is **per-profile** and defaults off — *"a 'Work' profile wants it; 'Game' usually doesn't"*. Same Deck, same host, same session: the Work profile loses HEVC where Game keeps it. A second source of variance is the host's own 4:4:4 encode probe, which only NVIDIA passes. ### The fix Gated on `hevc_444_hardware_decodable`, which asks the driver through the **same code the rung uses at construction** (`VkH265Decoder::probe_stream_support`) — so the advertisement and the rung that has to honour it cannot disagree. It costs a handful of capability queries, no session, no images, no submits, and `&&` short-circuits so a box that never enabled Full chroma pays nothing. **Both depths are required, not either.** With HDR on the host may resolve 4:4:4 **10-bit**, and a device offering `YUV444_8` but not `YUV444_10` would land in exactly the hole this closes. **Answering from the Vulkan rung alone is exact, not approximate** — it is the only rung in this build that implements 4:4:4 at all: `pf_vaadec::profile_for` maps only `chroma_format_idc == 1` and errors `UnsupportedShape` on 3; `pf_dxvadec`'s config refuses "anything but 4:2:0" by construction; the CPU rung is 8-bit 4:2:0. That is also why an Intel box — whose *hardware* has done HEVC 4:4:4 since Ice Lake — is still `false` here: our DXVA/VAAPI rungs do not implement it, so advertising it would be a lie about **us**, not about the GPU. The user is not left guessing: a `warn` names the setting, the reason, and PyroWave as the 4:4:4 path that works on any GPU. ### One deliberate non-change: `VIDEO_CAP_10BIT` / `VIDEO_CAP_HDR` These are advertised unprobed for the same reason 4:4:4 was, and I left them alone **on purpose**. The asymmetry is real: all three hardware rungs implement 10-bit 4:2:0 (`profile_for` maps `(H265, 1, 10)` and `(Av1, 1, 10)`; pf-dxvadec carries P010), so a Vulkan-only probe there would answer `false` on boxes whose VAAPI/DXVA rung decodes 10-bit perfectly and would **silently withdraw HDR from them** — a visible regression bought against a case never observed. Gating 10-bit honestly needs a libva/D3D11 probe this path cannot afford, for the same reason `av1_hardware_decodable` does not consult VAAPI. Measured on a Deck: HEVC Main **and** Main 10 both offer the full format set, so 10-bit was never this bug. ## 2. `--probe-decode` described a different device from the one that streams The RADV video-decode opt-in sat *after* the `--list-adapters` / `--probe-decode` / `--list-audio` / `--pair` early exits, so the triage tool never had it. Measured on a Deck (canary `e22af40f`), **same binary, back to back**: | invocation | verdict | |---|---| | `--probe-decode` | `vulkan video decode: no` · `driver decode ops: none (0x0)` · `no queue family advertises VIDEO_DECODE` | | `RADV_PERFTEST=video_decode … --probe-decode` | `vulkan video decode: YES` · `H.264, H.265, AV1, VP9 (0xF)` | The tool exists to be believed. Any Deck triage that consulted it reached the opposite of the truth — including, briefly, this one. Hoisted to the top of `run`, ahead of every early exit; nothing touches Vulkan before it (`main` calls `run` directly). ## Shape of the change The bit arithmetic moves into `video::video_caps_for` — a pure function — so the part that was actually wrong is testable without a GPU, a host or a Hello, in the spirit of `last_rung_verdict`. The device question stays with the caller, which owns the expensive probe and logs its own refusal with the user's setting in hand. ## Verification Run in the `pf-lxcheck2` Linux container (`pf-client-core` does not build on macOS at all): - `cargo fmt --all -- --check` — clean - `cargo build` (plain, **not** only `--all-targets` — that hides "dead outside tests") — clean - `cargo clippy --all-targets -- -D warnings` — clean - `cargo test -p pf-client-core` — **185 passed, 0 failed** The new test is **verified non-vacuous**: planting the original defect (`if want_444` → `if true`) fails it with exactly the intended assertion — `a 4:4:4 promise this device cannot keep costs HEVC entirely`, `left: 4, right: 0`. Re-run green after rebasing onto `e22082ac`; no overlap with anything that landed meanwhile. ## What is NOT verified - ⏳ **The fixed binary has never run on a Deck.** Fix 2's outcome is bracketed by the two real Deck measurements above plus `std::env::set_var` → `setenv(3)` and verified ordering, so it is determined rather than guessed — but the on-glass confirmation is owed. It needs new packages in a dev container, so it was left rather than done half-way. - ⏳ Fix 1's `Some(vk)` arm needs a GPU and has no test; only the `None` arm and the bit arithmetic are covered here. - ⏳ The reporting user's log and settings were never obtained, so *which* mechanism fired for **them** is unconfirmed. The confirming evidence would be: whether their profile had Full chroma on, and which host they dialled.
enricobuehler added 1 commit 2026-08-08 22:16:13 +00:00
fix(client): the 4:4:4 switch could cost a Deck its whole codec, and --probe-decode denied the queue it was decoding on
ci / bun-nix (pull_request) Successful in 41s
ci / web (pull_request) Successful in 1m17s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m21s
apple / swift (pull_request) Successful in 1m35s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m37s
ci / rust-arm64 (pull_request) Successful in 3m39s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m22s
android / android (pull_request) Successful in 3m56s
ci / rust (pull_request) Successful in 13m6s
9e598f8595
Two Steam Deck findings from a field report of "the decoder was not found, it
fell back to H.264 — but sometimes HEVC worked".

**The 4:4:4 advertisement was a promise nothing checked.** `VIDEO_CAP_444` rode
the "Full chroma" setting alone. That was safe while a software HEVC decoder
existed underneath it; M8 removed one (there is no permissively licensed HEVC
CPU decoder, so `software_decodable_codecs()` is H.264|AV1). The host grants
4:4:4 on HEVC ONLY, and answers the resolved chroma in the Welcome before the
client builds a decoder — so on a device with no 4:4:4 decode the toggle did not
cost crispness, it cost the entire codec: the Vulkan rung refuses the shape at
construction, VAAPI refuses it too, there is no CPU rung, and the session
reconnects on H.264. AMD has no HEVC 4:4:4 decode on any silicon, so every Deck
with that switch on lost HEVC. It is per-profile and default-off, which is
exactly why it looked intermittent — a "Work" profile lost HEVC where "Game"
kept it, same box, same host.

Gated on `hevc_444_hardware_decodable`, which asks the driver through the SAME
code the rung uses at construction (`VkH265Decoder::probe_stream_support`), so
the advertisement and the rung that must honour it cannot disagree. Both depths
are required, not either: with HDR on the host may resolve 4:4:4 10-bit, and a
device offering YUV444_8 but not YUV444_10 would land in the same hole.

Answering from the Vulkan rung alone is exact rather than approximate — it is
the only rung in this build that implements 4:4:4 at all (`pf_vaadec::profile_for`
errors on chroma_format_idc 3, pf-dxvadec refuses anything but 4:2:0, the CPU
rung is 8-bit 4:2:0). Deliberately NOT extended to VIDEO_CAP_10BIT/HDR: all
three rungs implement 10-bit 4:2:0, so a Vulkan-only probe there would withdraw
HDR from boxes whose VAAPI/DXVA rung decodes it perfectly — a real regression
against a case never observed.

The bit arithmetic moves into `video::video_caps_for` so the part that was
wrong is testable without a GPU, a host or a Hello; the test is verified
non-vacuous against the planted original defect.

**`--probe-decode` described a different device from the one that streams.** The
RADV video-decode opt-in sat AFTER the --list-adapters/--probe-decode/--list-audio
/--pair early exits, so the triage tool never had it. Measured on a Deck
(canary e22af40f), same binary back to back: bare `--probe-decode` printed
"vulkan video decode: no", "driver decode ops: none (0x0)", "no queue family
advertises VIDEO_DECODE"; with RADV_PERFTEST=video_decode in the environment,
"YES" and "H.264, H.265, AV1, VP9". Any Deck triage that consulted it reached
the opposite of the truth. Hoisted to the top of `run`, ahead of every early
exit — nothing touches Vulkan before it (`main` calls `run` directly).

Gates, in the Linux container: fmt, plain `cargo build` (not only
--all-targets), `clippy --all-targets -D warnings`, and 185 tests.
enricobuehler merged commit 5c70a90358 into main 2026-08-08 22:20:41 +00:00
enricobuehler deleted branch worktree-deck-hevc-shape-gates 2026-08-08 22:20:43 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#127