Intel Arc never used Vulkan Video because our pNext order fed the decode caps to the wrong struct #92

Merged
enricobuehler merged 8 commits from worktree-probe-codec-names into main 2026-08-07 14:13:16 +00:00
Owner

Started as three small fixes from the first on-glass runs of --probe-decode (#89). Then the user disbelieved my "Intel driver bug" conclusion and turned out to be right — the headline fix is ours.

The real bug

Intel Arc has never used Vulkan Video decode on Windows. Every session refused with "driver advertises neither DPB_AND_OUTPUT_COINCIDE nor DISTINCT" and fell back to D3D11VA. That refusal was ours.

vkGetPhysicalDeviceVideoCapabilitiesKHR was called with the codec capability struct chained before VkVideoDecodeCapabilitiesKHRpush_next prepends, so the chain was caps → h265_caps → decode_caps. On Arc/Windows 101.8724 the driver fills those two by position, not by sType, and returned them swapped. Measured on glass, both ways:

decode_flags_raw max_level_idc
before 12 1
after 1 12

12 is STD_VIDEO_H265_LEVEL_IDC_6_2; 1 is DPB_AND_OUTPUT_COINCIDE. We were reading an H.265 level as a decode-capability bitmask, and 12 contains neither 0x1 nor 0x2 — so the check concluded the device had no DPB mode. It had one all along.

What gave it away: the base struct was fully populated the whole time — 15 DPB slots, 8×8 to 8192×8192 extents, SEPARATE_REFERENCE_IMAGES. A driver answering in that much detail is not declining.

NVIDIA and RADV dispatch by sType and don't care about order, which is exactly why the fleet stayed green and this reached the field. All three codec paths now chain decode caps first, the order every Vulkan sample uses.

This does not yet give the Arc Vulkan Video. It moves the refusal one step down the same function: the device advertises only COINCIDE (no DISTINCT), and its NV12 coincide entry does not advertise SAMPLED, which the zero-copy presenter path needs. Whether that is a second bug of ours or a real Intel constraint is not established, and nothing here claims it either way.

Why it took a driver-level A/B to find

caps.rs and caps_h265.rs logged nothing, so a zeroed flag was indistinguishable between "the driver said no" and "our chain never reached the struct". Two commits here add the driver's answer verbatim — base capabilities beside the decode ones, which is the discriminator — and that is what exposed the swap.

I had concluded "Intel driver bug" from our own error message. That is the same circular reasoning this logging now exists to break.

The three smaller fixes

  1. 43e713ec — the probe printed three codec names beside a four-bit mask. Bit 3 is VP9, a real operation with no punktfunk rung. Now named, and unknown bits print as unrecognised bits 0x… rather than vanishing.
  2. c0f8f051 — the probe printed its display position and called it the PUNKTFUNK_VK_DEVICE value. That variable indexes the raw enumeration order, before ranking. On hybrids — the exact hardware the flag is for — it could select the other GPU.
  3. c34e1412PUNKTFUNK_DECODER was read untrimmed, so one trailing space made a pin fall through to auto in silence. Now one pure resolve_decoder_pref shared with decode_pinned_to_software, which had the identical expression.

Still outstanding

--probe-decode remains over-optimistic: it reports YES for the Arc while the real path refuses, because it checks the five device-level conditions but never runs the per-profile capability query that actually gates the rung. Unchanged by this PR.

Gates

fmt --check clean; clippy -D warnings over pf-vkdecode, pf-client-core, punktfunk-client-session, pf-presenter in the Linux container; 187 pf-vkdecode + 164 pf-client-core tests.

The GPU parity legs covering this code need a GPU the build host doesn't have. The evidence for the headline fix is the on-glass A/B above, on .221, against host .21.

Started as three small fixes from the first on-glass runs of `--probe-decode` (#89). Then the user disbelieved my "Intel driver bug" conclusion and turned out to be right — the headline fix is ours. ## The real bug Intel Arc has never used Vulkan Video decode on Windows. Every session refused with *"driver advertises neither DPB_AND_OUTPUT_COINCIDE nor DISTINCT"* and fell back to D3D11VA. **That refusal was ours.** `vkGetPhysicalDeviceVideoCapabilitiesKHR` was called with the codec capability struct chained *before* `VkVideoDecodeCapabilitiesKHR` — `push_next` prepends, so the chain was `caps → h265_caps → decode_caps`. On Arc/Windows 101.8724 the driver fills those two **by position, not by sType**, and returned them swapped. Measured on glass, both ways: | | `decode_flags_raw` | `max_level_idc` | |---|---|---| | before | **12** | 1 | | after | **1** | 12 | `12` is `STD_VIDEO_H265_LEVEL_IDC_6_2`; `1` is `DPB_AND_OUTPUT_COINCIDE`. We were reading an H.265 *level* as a decode-capability *bitmask*, and 12 contains neither `0x1` nor `0x2` — so the check concluded the device had no DPB mode. It had one all along. What gave it away: the base struct was fully populated the whole time — 15 DPB slots, 8×8 to 8192×8192 extents, `SEPARATE_REFERENCE_IMAGES`. A driver answering in that much detail is not declining. NVIDIA and RADV dispatch by sType and don't care about order, which is exactly why the fleet stayed green and this reached the field. All three codec paths now chain decode caps first, the order every Vulkan sample uses. **This does not yet give the Arc Vulkan Video.** It moves the refusal one step down the same function: the device advertises only COINCIDE (no DISTINCT), and its NV12 coincide entry does not advertise `SAMPLED`, which the zero-copy presenter path needs. Whether that is a second bug of ours or a real Intel constraint is **not established**, and nothing here claims it either way. ## Why it took a driver-level A/B to find `caps.rs` and `caps_h265.rs` logged nothing, so a zeroed flag was indistinguishable between "the driver said no" and "our chain never reached the struct". Two commits here add the driver's answer verbatim — base capabilities beside the decode ones, which is the discriminator — and that is what exposed the swap. I had concluded "Intel driver bug" from our own error message. That is the same circular reasoning this logging now exists to break. ## The three smaller fixes 1. **`43e713ec`** — the probe printed three codec names beside a four-bit mask. Bit 3 is VP9, a real operation with no punktfunk rung. Now named, and unknown bits print as `unrecognised bits 0x…` rather than vanishing. 2. **`c0f8f051`** — the probe printed its *display* position and called it the `PUNKTFUNK_VK_DEVICE` value. That variable indexes the **raw** enumeration order, before ranking. On hybrids — the exact hardware the flag is for — it could select the other GPU. 3. **`c34e1412`** — `PUNKTFUNK_DECODER` was read untrimmed, so one trailing space made a pin fall through to `auto` in silence. Now one pure `resolve_decoder_pref` shared with `decode_pinned_to_software`, which had the identical expression. ## Still outstanding `--probe-decode` remains over-optimistic: it reports `YES` for the Arc while the real path refuses, because it checks the five device-level conditions but never runs the per-profile capability query that actually gates the rung. Unchanged by this PR. ## Gates `fmt --check` clean; `clippy -D warnings` over `pf-vkdecode`, `pf-client-core`, `punktfunk-client-session`, `pf-presenter` in the Linux container; 187 `pf-vkdecode` + 164 `pf-client-core` tests. The GPU parity legs covering this code need a GPU the build host doesn't have. The evidence for the headline fix is the on-glass A/B above, on .221, against host .21.
enricobuehler added 3 commits 2026-08-07 12:35:42 +00:00
First hardware run of --probe-decode, on the RTX 5070 Ti:

    driver decode ops:   H.264, H.265, AV1 (0xF)

Three names, four bits. 0xF is H.264|H.265|AV1|VP9 — bit 3 is
VK_VIDEO_CODEC_OPERATION_DECODE_VP9_BIT_KHR, a real decode operation this
client has no rung for, so the name table stopped short of it and the line
looked complete while silently dropping a codec the driver had advertised.

That is the exact failure this flag exists to prevent. The whole point of
--probe-decode is that a reader can trust the words to cover the number; a mask
with an unexplained bit asks them to trust it instead. VP9 is now named (marked
as having no punktfunk rung, because advertising it as decodable would be its
own lie), and any bit beyond the four we know prints as "unrecognised bits
0x…" rather than vanishing — so the next codec Khronos adds shows up as an
unknown rather than as nothing at all.

Gates: fmt clean; clippy -D warnings on punktfunk-client-session.
--probe-decode printed its DISPLAY position and called it the
PUNKTFUNK_VK_DEVICE value. It is not. pick_device resolves that variable against
the RAW vkEnumeratePhysicalDevices order (setup.rs, `devices.get(i)`) BEFORE any
ranking runs, while the probe sorts discrete-first for readability.

Those two orders disagree precisely on the hardware this flag exists to
diagnose. pick_device's own comment records why the ranking is there: "enumeration
order puts the iGPU FIRST on some hybrids (observed: Ryzen iGPU ahead of an RTX
dGPU)". So on a hybrid laptop the number the probe printed for the iGPU could
well be the number for the dGPU — a diagnostic handing out an actionable value
that selects the other GPU, which is worse than printing none.

Measured on the Arc + RTX 3500 Ada laptop, which is also where the first output
went out with the wrong claim in it: three adapters, and the same Arc iGPU
enumerated TWICE. So AdapterDecode now carries the raw enumeration index,
captured before the sort, and the printer uses it; the "default presenter"
marker stays on the first LISTED entry, because sorted-first is what pick_device
lands on when nothing overrides.

The duplicate is why the trailing hint names PUNKTFUNK_VK_ADAPTER as the safer
knob and admits its limit: two adapters sharing a marketing name cannot be told
apart by it, and a name match resolves to whichever enumerates first. The hint
also states the thing this whole output invites a reader to get wrong — that a
capable GPU in the list does not mean the decoder will use it, because Vulkan
Video decodes on the presenter's device and PUNKTFUNK_DECODER does not move the
presenter.

Gates: fmt clean; clippy -D warnings on punktfunk-client-session and pf-presenter.
fix(client): a decoder pin with a stray space was silently ignored
ci / bun-nix (pull_request) Successful in 20s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m7s
apple / swift (pull_request) Successful in 1m26s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 1m32s
ci / docs-site (pull_request) Successful in 1m44s
ci / rust-arm64 (pull_request) Successful in 2m23s
android / android (pull_request) Successful in 3m9s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m7s
ci / rust (pull_request) Successful in 6m0s
c34e1412fb
Found on glass, and it cost a whole session to find. PUNKTFUNK_DECODER was read
untrimmed, so "native-vulkan " — ONE trailing space — matched no arm of
native_vulkan_gate, fell through to `auto`, and on an Intel box `auto` takes
d3d11va first. The operator's pin never ran and NOTHING said so. Read against a
log, that is indistinguishable from the rung being refused for a hardware
reason, which is precisely the ambiguity the rest of this module's logging was
just rewritten to remove.

The space is not exotic. A Windows .cmd produces it for free: `echo x>> file`
keeps the space before the redirect, so every line written that way carries one.
PUNKTFUNK_VK_ADAPTER already trimmed; this did not, and the inconsistency is what
made it invisible — the GPU override obeyed while the decoder override did not.

The rule now lives in one pure function, resolve_decoder_pref, called by BOTH
readers. decode_pinned_to_software had the identical untrimmed expression, and
its own doc comment says a second reading of the same two inputs is a second
place for them to drift — fixing one and not the other would have proved it
right. Whitespace-only counts as ABSENT rather than as a pin to "", because an
exported-but-empty variable means "no override" and "" is a value the gate
happens to accept.

Tested as a pure rule (no process environment), including the end-to-end leg
that matters: the trimmed pin reaches native_vulkan_gate and is admitted. Like
the create-array tests in dee97e89 its before-state is a compile error rather
than a failing assertion, because the function is new — what it guards going
forward is real, and an editor who drops the trim fails it.

Gates: fmt clean; clippy -D warnings over pf-client-core,
punktfunk-client-session and pf-presenter in the Linux container; 164
pf-client-core tests.
enricobuehler added 1 commit 2026-08-07 12:50:26 +00:00
diag(vkdecode): log the driver's video capabilities verbatim
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m9s
ci / bun-nix (pull_request) Successful in 30s
ci / web (pull_request) Successful in 1m20s
apple / swift (pull_request) Successful in 1m28s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m51s
ci / rust-arm64 (pull_request) Successful in 2m44s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m46s
android / android (pull_request) Successful in 4m16s
ci / rust (pull_request) Successful in 4m52s
fb1a0a61e9
Nothing in the caps module logged anything, so when a device refused with
"advertises neither DPB_AND_OUTPUT_COINCIDE nor DISTINCT" there was no way to
separate two very different situations that present identically as a zero: the
driver filling the chain and genuinely declaring no DPB mode, versus our own
pNext chain never reaching VkVideoDecodeCapabilitiesKHR at all.

Printing the BASE VkVideoCapabilitiesKHR beside the decode flags is the
discriminator. A populated max_dpb_slots next to decode_flags: 0 means the
driver traversed the chain and answered; zeros across both mean the query never
landed and the refusal is ours, not the driver's.

Raised by the Intel Arc result on .221, where I concluded "driver bug" on the
strength of our own code's report — which is precisely the circular reasoning
this line exists to break.
enricobuehler added 1 commit 2026-08-07 12:55:47 +00:00
diag(vkdecode): log maxLevelIdc beside the decode flags
ci / bun-nix (pull_request) Successful in 44s
ci / docs-site (pull_request) Successful in 1m21s
apple / swift (pull_request) Successful in 1m33s
ci / web (pull_request) Successful in 1m28s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 2m15s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m33s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m11s
ci / rust (pull_request) Successful in 5m23s
android / android (pull_request) Canceled after 6m14s
a183cac8aa
The Arc returned decode_flags=0b1100 = 12 with a fully populated base struct (15
DPB slots, 8192x8192 max extent). Neither COINCIDE (0x1) nor DISTINCT (0x2) is
set, and 0x4|0x8 are not defined for that field at all — but 12 IS
STD_VIDEO_H265_LEVEL_IDC_6_2, and VkVideoDecodeCapabilitiesKHR and
VkVideoDecodeH265CapabilitiesKHR have identical layouts (sType, pNext, one u32).

So the suspicion is that we are reading H.265's maxLevelIdc where the decode
flags belong. Logging both settles it: if max_level_idc comes back as 1 or 2 the
two structs are crossed, and the refusal is ours rather than the driver's.
enricobuehler added 1 commit 2026-08-07 13:08:07 +00:00
fix(vkdecode): the pNext order decided which struct got the decode caps
ci / web (pull_request) Successful in 1m6s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m4s
ci / docs-site (pull_request) Successful in 1m14s
apple / swift (pull_request) Successful in 1m26s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 1m40s
ci / bun-nix (pull_request) Successful in 2m5s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m10s
android / android (pull_request) Successful in 5m17s
ci / rust (pull_request) Successful in 6m44s
ca667cb79a
Intel Arc never used Vulkan Video decode on Windows. The rung refused every
session with "driver advertises neither DPB_AND_OUTPUT_COINCIDE nor DISTINCT"
and fell back to D3D11VA — and that refusal was ours.

vkGetPhysicalDeviceVideoCapabilitiesKHR was called with the codec capability
struct chained BEFORE VkVideoDecodeCapabilitiesKHR (push_next prepends, so the
chain was caps -> h265_caps -> decode_caps). On Arc/Windows 101.8724 the driver
fills those two by POSITION, not by sType, and returned them SWAPPED. Measured,
on glass, both ways:

  before:  decode_flags_raw=12   max_level_idc=1
  after:   decode_flags_raw=1    max_level_idc=12

12 is STD_VIDEO_H265_LEVEL_IDC_6_2 and 1 is DPB_AND_OUTPUT_COINCIDE. We were
reading an H.265 level as a decode-capability bitmask; 12 contains neither 0x1
nor 0x2, so the check concluded the device had no DPB mode. It had one all along.
The base struct was fully populated throughout — 15 DPB slots, 8192x8192 max
extent — which is what gave the lie away: a driver that answers in that much
detail is not declining.

NVIDIA and RADV dispatch by sType and do not care about the order, which is
exactly why the fleet stayed green and this reached the field. Both orders are
spec-legal for us to write; only one survives a driver that assumes the
conventional one, and the conventional one — decode caps first, as every Vulkan
sample writes it — is now what all three codecs use.

⚠ This does NOT yet give the Arc Vulkan Video. It moves the refusal one step
down the same function: the device advertises only COINCIDE (no DISTINCT), and
its NV12 coincide entry does not advertise SAMPLED usage, which the zero-copy
presenter path needs. Whether that is a second bug of ours or a real Intel
constraint is not yet established, and this commit does not claim it either way.

Found because the user disbelieved my "Intel driver bug" conclusion. He was
right: I had reasoned from our own error message, which is the same circularity
the caps logging added in fb1a0a61/a183cac8 now exists to break.

Gates: fmt clean; clippy -D warnings; 187 pf-vkdecode tests. The GPU parity legs
that cover this code cannot run here (no GPU on the build host) — the evidence
is the on-glass A/B above.
enricobuehler force-pushed worktree-probe-codec-names from 2fa40f16fa to ca667cb79a 2026-08-07 13:08:07 +00:00 Compare
enricobuehler changed title from Three defects the first on-glass run of --probe-decode found — two in the probe itself to Intel Arc never used Vulkan Video because our pNext order fed the decode caps to the wrong struct 2026-08-07 13:08:32 +00:00
enricobuehler added 1 commit 2026-08-07 13:34:30 +00:00
diag(vkdecode): --probe-decode reports what the driver says about video images
ci / bun-nix (pull_request) Successful in 38s
apple / swift (pull_request) Successful in 1m31s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 2m11s
ci / docs-site (pull_request) Successful in 2m21s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m44s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m10s
ci / rust-arm64 (pull_request) Successful in 6m29s
android / android (pull_request) Successful in 7m44s
ci / rust (pull_request) Canceled after 9m40s
nix / flake (pull_request) Canceled after 9m38s
c06ee55b61
The Intel Arc refusal moved one step down the caps query and stopped again: the
coincide NV12 entry does not advertise SAMPLED. That sentence is punktfunk's, not
the driver's, and the last two times a conclusion was drawn from a sentence of ours
the conclusion was wrong.

So --probe-decode now prints the driver's own answers instead. For every profile the
client can negotiate (H.264 High, H.265 Main and Main 10, AV1 Main 8- and 10-bit) it
asks vkGetPhysicalDeviceVideoFormatPropertiesKHR in six usage combinations — the
three the image pools really create with, plus DPB|DST without sampling, SAMPLED
alone and DST alone, which are what localise a refusal to a half. Each answer is
printed as the driver gave it: format, usage and create flags named AND in hex with
unrecognised bits called out, image type, tiling. A failed query prints its VkResult
rather than vanishing into an empty list.

It goes through pf-vkdecode's own query rather than a copy of it, which meant
splitting query_formats into a physical-device form — the call never needed the
VkDevice the old signature demanded. VideoFormat gains imageType and imageTiling to
carry the whole record; VUID-VkImageCreateInfo-pNext-06811 compares both for
equality, so they were being assumed rather than read.

And because a driver that under-reports usage would be indistinguishable from one
that genuinely lacks it, the probe asks a second, independent question —
vkGetPhysicalDeviceImageFormatProperties2 over the same profile list — and prints it
only where the two disagree. A disagreement is the finding.

No behaviour change to any decode path: derivation reads the same fields it did.
enricobuehler added 1 commit 2026-08-07 13:44:17 +00:00
fix(vkdecode): a refused device says what the refusal costs, and about which format
ci / web (pull_request) Successful in 1m8s
apple / swift (pull_request) Successful in 1m33s
apple / screenshots (pull_request) Skipped
ci / bun-nix (pull_request) Successful in 1m33s
ci / docs-site (pull_request) Successful in 2m2s
ci / rust-arm64 (pull_request) Successful in 2m25s
android / android (pull_request) Successful in 3m1s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m13s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m3s
ci / rust (pull_request) Successful in 11m15s
nix / flake (pull_request) Successful in 12m49s
b166c53cc2
Three things the Intel Arc measurement showed were wrong or unhelpful in the refusal
path.

The message named NV12 whatever the stream was. A Main 10 session refused over P010
was told about NV12, which sends the reader to look up the wrong format's support.
Both variants now carry the format the driver's own entry reported.

A missing SAMPLED now says what it costs. "does not advertise usage SAMPLED" is
accurate and tells a field reporter nothing: the consequence is that no shader can
read this device's decoded pictures, so the zero-copy path cannot exist on it at all
— which is a different conversation from a device that is merely slower. The line
points at --probe-decode for the driver's own words.

And the probe's second opinion no longer claims to be one. Measured on both vendors,
vkGetPhysicalDeviceImageFormatProperties2 answers "creatable" for combinations the
video-format query rejects — on NVIDIA too, for SAMPLED alone, which is not a legal
video image usage at all. So it does not honour the chained profile list and must not
be read as permission; it is still printed, because otherwise everyone who reads a
refusal asks the question again, but it is labelled as not authority.

Also names the three video ENCODE usage bits, which NVIDIA advertises on decode
pictures and the probe was printing as "unrecognised 0xC000".
enricobuehler merged commit 428377eaa1 into main 2026-08-07 14:13:16 +00:00
enricobuehler deleted branch worktree-probe-codec-names 2026-08-07 14:13:18 +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#92