From 1d587a259eb686cdb4fea5704801a7c9c2c8d90e Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sun, 19 Jul 2026 14:44:05 +0200 Subject: [PATCH] =?UTF-8?q?fix(client/d3d11va):=20green=20bar=20at=20the?= =?UTF-8?q?=20bottom=20=E2=80=94=20clamp=20the=20blit=20to=20the=20frame?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The decode surface is DXVA-aligned (height rounded up to the profile's alignment, 128 for HEVC/AV1), so it is taller than the frame: a 2400-line stream decodes into a 2432-line texture. VideoProcessorBlt was called with no stream source rect, so it blitted the WHOLE surface — the uninitialized padding rows (NV12 Y=0,U=V=0, which converts to vivid green) landed at the bottom of the output and the picture was squashed to fit. Set the source rect to the real frame. Pre-existing on this backend, but 40030e90 made D3D11VA auto's first choice on Intel, so it went from a corner case to what every Intel user sees — a ~32 px green bar at 3840x2400, reported on glass within the hour. Also dedupe the Settings GPU picker by description: that string IS the identity downstream (persisted as Settings::adapter, matched by name as PUNKTFUNK_VK_ADAPTER), so repeated enumerations of one adapter offered the user the same choice twice — live on an Intel Arc laptop, whose Vulkan ICD also enumerates the single physical iGPU twice. Both paths now log their raw enumeration (decode texture dims; DXGI name/LUID/ids) for the next report. Co-Authored-By: Claude Fable 5 --- clients/windows/src/gpu.rs | 45 +++++++++++++++++------ crates/pf-client-core/src/video_d3d11.rs | 46 +++++++++++++++++++++--- 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/clients/windows/src/gpu.rs b/clients/windows/src/gpu.rs index d65e3991..2e9e306f 100644 --- a/clients/windows/src/gpu.rs +++ b/clients/windows/src/gpu.rs @@ -41,16 +41,41 @@ fn all_adapters() -> Vec { /// Descriptions of the real (hardware, non-WARP) GPUs — the Settings GPU picker's option list. /// The picker only shows when this has more than one entry. +/// +/// **Deduplicated by description**, because the description IS the identity everywhere +/// downstream: the pick is persisted as that string (`Settings::adapter`) and matched by +/// name in the session binary (`PUNKTFUNK_VK_ADAPTER`). So two entries with the same name +/// are one selectable choice however many times DXGI enumerates them — listing it twice +/// only offers the user a meaningless coin flip. Seen live on an Intel Arc laptop +/// (2026-07-19), whose Vulkan ICD likewise enumerates the one physical iGPU twice. pub fn adapter_names() -> Vec { const DXGI_ADAPTER_FLAG_SOFTWARE: u32 = 2; // dxgi.h; not in this windows-rs feature set - all_adapters() - .iter() - .filter(|a| { - a.cast::() - .and_then(|a1| unsafe { a1.GetDesc1() }) - .map(|d| d.Flags & DXGI_ADAPTER_FLAG_SOFTWARE == 0) - .unwrap_or(true) - }) - .map(adapter_name) - .collect() + let mut names: Vec = Vec::new(); + for a in all_adapters() { + let desc1 = a + .cast::() + .and_then(|a1| unsafe { a1.GetDesc1() }) + .ok(); + let name = adapter_name(&a); + // Forensics for the next duplicate/oddity report — which adapters DXGI actually + // returned, and whether the repeats share a LUID (one adapter enumerated twice) + // or are distinct devices that merely present the same description. + if let Some(d) = &desc1 { + tracing::debug!( + name = %name, + luid = format!("{:08x}-{:08x}", d.AdapterLuid.HighPart, d.AdapterLuid.LowPart), + vendor = format_args!("{:#06x}", d.VendorId), + device = format_args!("{:#06x}", d.DeviceId), + flags = d.Flags, + "DXGI adapter" + ); + } + if desc1.is_some_and(|d| d.Flags & DXGI_ADAPTER_FLAG_SOFTWARE != 0) { + continue; // WARP / software renderer — never a streaming target + } + if !names.contains(&name) { + names.push(name); + } + } + names } diff --git a/crates/pf-client-core/src/video_d3d11.rs b/crates/pf-client-core/src/video_d3d11.rs index 1496795b..38d953c6 100644 --- a/crates/pf-client-core/src/video_d3d11.rs +++ b/crates/pf-client-core/src/video_d3d11.rs @@ -42,7 +42,7 @@ use ffmpeg_next as ffmpeg; use std::ffi::c_void; use std::ptr; use windows::core::{Interface, GUID}; -use windows::Win32::Foundation::HANDLE; +use windows::Win32::Foundation::{HANDLE, RECT}; use windows::Win32::Graphics::Direct3D::{D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_11_1}; use windows::Win32::Graphics::Direct3D11::{ D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Multithread, ID3D11Texture2D, @@ -678,6 +678,25 @@ impl D3d11vaDecoder { (_, _, true) => DXGI_COLOR_SPACE_YCBCR_FULL_G22_LEFT_P709, _ => DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709, }; + // The DECODE surface is DXVA-aligned (height rounded up to the profile's + // macroblock/tile alignment — 128 for HEVC/AV1), so it is TALLER than the + // frame: a 2400-line stream decodes into a 2432-line texture. Without an + // explicit source rect the processor blits the WHOLE surface — the padding + // rows (uninitialized NV12: Y=0,U=V=0, which converts to vivid green) land at + // the bottom of the output and the picture is squashed to fit. Clamp the + // source to the real frame; the dest stays the whole (frame-sized) slot. + // Live-hit on Intel 3840x2400 as a ~32 px green bar (2026-07-19). + video_context1.VideoProcessorSetStreamSourceRect( + &ring.vp, + 0, + true, + Some(&RECT { + left: 0, + top: 0, + right: width as i32, + bottom: height as i32, + }), + ); video_context1.VideoProcessorSetStreamColorSpace1(&ring.vp, 0, in_cs); video_context1.VideoProcessorSetOutputColorSpace1( &ring.vp, @@ -722,7 +741,16 @@ impl D3d11vaDecoder { // completion, and an unflushed deferred batch would add a driver-decided delay. context.Flush(); - log_layout_once(width, height, index, color.is_pq()); + let mut src_desc = D3D11_TEXTURE2D_DESC::default(); + src.GetDesc(&mut src_desc); + log_layout_once( + width, + height, + src_desc.Width, + src_desc.Height, + index, + color.is_pq(), + ); Ok(D3d11Frame { width, height, @@ -770,10 +798,20 @@ impl Drop for D3d11vaDecoder { } /// One-time dump of the first decoded surface's layout — the forensics for a new GPU/driver. -fn log_layout_once(width: u32, height: u32, index: u32, pq: bool) { +/// `tex_*` is the DXVA-aligned decode surface (>= the frame); the gap is the padding the +/// stream source rect excludes. +fn log_layout_once(width: u32, height: u32, tex_w: u32, tex_h: u32, index: u32, pq: bool) { use std::sync::atomic::{AtomicBool, Ordering}; static ONCE: AtomicBool = AtomicBool::new(true); if ONCE.swap(false, Ordering::Relaxed) { - tracing::info!(width, height, slice = index, pq, "D3D11VA first frame"); + tracing::info!( + width, + height, + tex_w, + tex_h, + slice = index, + pq, + "D3D11VA first frame" + ); } }