fix(client/d3d11va): green bar at the bottom — clamp the blit to the frame
ci / web (push) Successful in 47s
ci / docs-site (push) Successful in 1m8s
apple / swift (push) Successful in 1m18s
decky / build-publish (push) Successful in 23s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 13s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 6m49s
ci / bench (push) Successful in 7m2s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 5m4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
apple / screenshots (push) Successful in 6m12s
deb / build-publish (push) Successful in 9m49s
android / android (push) Successful in 13m29s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 6m53s
docker / deploy-docs (push) Successful in 27s
flatpak / build-publish (push) Successful in 6m32s
deb / build-publish-host (push) Successful in 13m26s
arch / build-publish (push) Successful in 17m13s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 8m0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m10s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 21m2s
ci / rust (push) Successful in 28m51s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 9m0s

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 14:44:05 +02:00
parent 1436c0b9b4
commit 1d587a259e
2 changed files with 77 additions and 14 deletions
+35 -10
View File
@@ -41,16 +41,41 @@ fn all_adapters() -> Vec<IDXGIAdapter> {
/// 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<String> {
const DXGI_ADAPTER_FLAG_SOFTWARE: u32 = 2; // dxgi.h; not in this windows-rs feature set
all_adapters()
.iter()
.filter(|a| {
a.cast::<windows::Win32::Graphics::Dxgi::IDXGIAdapter1>()
.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<String> = Vec::new();
for a in all_adapters() {
let desc1 = a
.cast::<windows::Win32::Graphics::Dxgi::IDXGIAdapter1>()
.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
}
+42 -4
View File
@@ -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"
);
}
}