From ca667cb79ae8e2f03b36279f9d86f16973397e46 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 7 Aug 2026 15:07:48 +0200 Subject: [PATCH] fix(vkdecode): the pNext order decided which struct got the decode caps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- crates/pf-vkdecode/src/caps.rs | 7 +++++-- crates/pf-vkdecode/src/caps_av1.rs | 7 +++++-- crates/pf-vkdecode/src/caps_h265.rs | 17 +++++++++++++++-- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/crates/pf-vkdecode/src/caps.rs b/crates/pf-vkdecode/src/caps.rs index e42c89ac..722b1772 100644 --- a/crates/pf-vkdecode/src/caps.rs +++ b/crates/pf-vkdecode/src/caps.rs @@ -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. diff --git a/crates/pf-vkdecode/src/caps_av1.rs b/crates/pf-vkdecode/src/caps_av1.rs index 40ae6d7a..509ae1b9 100644 --- a/crates/pf-vkdecode/src/caps_av1.rs +++ b/crates/pf-vkdecode/src/caps_av1.rs @@ -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. diff --git a/crates/pf-vkdecode/src/caps_h265.rs b/crates/pf-vkdecode/src/caps_h265.rs index d19852e7..a25c92ff 100644 --- a/crates/pf-vkdecode/src/caps_h265.rs +++ b/crates/pf-vkdecode/src/caps_h265.rs @@ -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.