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

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.
This commit is contained in:
2026-08-07 15:07:48 +02:00
parent a183cac8aa
commit ca667cb79a
3 changed files with 25 additions and 6 deletions
+5 -2
View File
@@ -605,9 +605,12 @@ pub(crate) unsafe fn query_h264_caps(
let mut h264_caps = vk::VideoDecodeH264CapabilitiesKHR::default();
let mut decode_caps = vk::VideoDecodeCapabilitiesKHR::default();
// ⚠ ORDER IS LOAD-BEARING — see the measured Intel Arc swap in
// [`crate::caps_h265::query_h265_caps`]. `push_next` prepends, so pushing the codec
// struct FIRST leaves VkVideoDecodeCapabilitiesKHR directly after the base struct.
let mut caps = vk::VideoCapabilitiesKHR::default()
.push_next(&mut decode_caps)
.push_next(&mut h264_caps);
.push_next(&mut h264_caps)
.push_next(&mut decode_caps);
// SAFETY: physical device is live (DeviceHandles contract); `profile` roots a
// fully wired, immovable chain; `caps` chains driver-fillable structs that all
// outlive the call.
+5 -2
View File
@@ -278,9 +278,12 @@ pub(crate) unsafe fn query_av1_caps(
let mut av1_caps = vk::VideoDecodeAV1CapabilitiesKHR::default();
let mut decode_caps = vk::VideoDecodeCapabilitiesKHR::default();
// ⚠ ORDER IS LOAD-BEARING — see the measured Intel Arc swap in
// [`crate::caps_h265::query_h265_caps`]. `push_next` prepends, so pushing the codec
// struct FIRST leaves VkVideoDecodeCapabilitiesKHR directly after the base struct.
let mut caps = vk::VideoCapabilitiesKHR::default()
.push_next(&mut decode_caps)
.push_next(&mut av1_caps);
.push_next(&mut av1_caps)
.push_next(&mut decode_caps);
// SAFETY: physical device is live (DeviceHandles contract); `profile` roots a
// fully wired, immovable chain; `caps` chains driver-fillable structs that all
// outlive the call.
+15 -2
View File
@@ -279,9 +279,22 @@ pub(crate) unsafe fn query_h265_caps(
let mut h265_caps = vk::VideoDecodeH265CapabilitiesKHR::default();
let mut decode_caps = vk::VideoDecodeCapabilitiesKHR::default();
// ⚠ ORDER IS LOAD-BEARING on at least one shipping driver. `push_next` PREPENDS, so
// the chain is the reverse of the call order: pushing the codec struct last puts
// VkVideoDecodeCapabilitiesKHR FIRST after the base struct, which is the order every
// Vulkan sample writes it in.
//
// Measured on Intel Arc (Windows 101.8724) with the previous order — codec struct
// first — the driver filled the two by POSITION rather than by sType and returned
// them SWAPPED: `decode_caps.flags` came back 12 (= STD_VIDEO_H265_LEVEL_IDC_6_2)
// and `h265_caps.maxLevelIdc` came back 1 (= DPB_AND_OUTPUT_COINCIDE). Reading a
// level as a flag bitmask means neither COINCIDE nor DISTINCT appeared set, so the
// rung refused a device that in fact supports it, and every Arc fell back to D3D11VA.
// NVIDIA and RADV dispatch by sType and are indifferent to the order, which is why
// the fleet was green and this survived to the field.
let mut caps = vk::VideoCapabilitiesKHR::default()
.push_next(&mut decode_caps)
.push_next(&mut h265_caps);
.push_next(&mut h265_caps)
.push_next(&mut decode_caps);
// SAFETY: physical device is live (DeviceHandles contract); `profile` roots a
// fully wired, immovable chain; `caps` chains driver-fillable structs that all
// outlive the call.