A user's 1080p stream repeated its last row of pixels over the final few rows, so the
image looked stretched at the bottom.
The Vulkan-Video CSC pass sampled the decoded planes with the fullscreen triangle's
normalized 0..1 UVs, but its render target is built at the CROPPED frame size. Those are
not the same rectangle: FFmpeg sizes the decode pool from `avctx->coded_*`, and H.264
codes `16 * mb_height` — so a 1080-row picture decodes into a 1088-row pool. Destination
row 1079 sampled source row ~1087.5, dragging the 8 alignment rows into view and squashing
the picture 0.7%. Encoders fill that padding by replicating the last picture line, which
is why it reads as a smeared bottom row rather than garbage.
Confirmed on glass (.173, RTX, H.264 1080p, vulkan-video):
Vulkan Video first frame width=1920 height=1080 pool_w=1920 pool_h=1088
`VkVideoFrame` now carries the pool extent and `record_csc` takes a `uv_scale`, written to
the shader's `params.zw` — which the CSC shader already reserved for a use like this. The
chroma cositing offset is unchanged and stays correct: `textureSize` reports the pool
width, which is the space the scaled UV is already in.
Only the Vulkan-Video path passes a scale below 1.0. D3D11VA already clamps this in its
VideoProcessor blit (the same bug, seen as a green bar there because DXVA padding is
uninitialized rather than replicated); dmabuf imports its planes at the crop over the real
stride; PyroWave allocates its ring at exact stream dims. Apple and Android crop at the OS
layer. Every other call site passes [1.0, 1.0], so the change is inert there.
Also adds a one-time first-frame layout log mirroring the D3D11VA one, so the frame-vs-pool
gap is visible in the field instead of having to be re-derived from FFmpeg internals.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The arch package job (--features nvenc) tripped the same class of
Codec::PyroWave non-exhaustive matches as windows-host had, in
nvenc_cuda.rs (6 sites) — dispatch-guarded unreachable!() arms, plus
the vk_util-extraction leftover unused imports in vulkan_video.rs.
All Linux host feature combos (none / pyrowave / nvenc,vulkan-encode /
all three) now compile clean on .21.
Presenter: planar_csc.frag (+ committed .spv) — the 3-plane variant of
nv12_csc.frag (separate Cb/Cr R8 planes, same push-constant CSC-row
contract, siting correction self-disables at full-res chroma).
CscPass grows a shared builder + new_planar()/bind_planes_planar()
(GENERAL-layout descriptors — pyrowave planes stay GENERAL); the Vk
presenter builds the planar pass when the device passed the pyrowave
probe, FrameInput::PyroWave rides present_frame (no acquire barrier
needed: the decoder fence-completed and barriered the planes on the
same queue), and run.rs presents it with no demote rung (only device
loss ends the session).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Clients derive Y'CbCr->RGB from the stream's SIGNALED matrix x range x depth
via shared csc rows (Rust csc_rows + Swift CscRows) instead of hardcoded
709/2020 - a BT.601-signaled stream (a Linux host's RGB-input NVENC) no longer
renders with a constant hue error. Host-side signaling made honest across
NVENC/VAAPI/openh264/GameStream and the session plan's chroma/bit-depth.
Decoded color-bar fixtures (601/709 x limited/full) pin the math in tests on
both cores.
Same presenter, tvOS HDR: tvOS has no Metal EDR API and a bare PQ colorspace
tag composites UNTONE-MAPPED (the "overblown" Apple TV report), so HDR now
splits on the display's live EDR headroom - PQ passthrough when the
per-session AVDisplayManager mode switch landed (a real HDR10 output
tone-maps itself), else an in-shader PQ->SDR tone-map (203-nit reference
white, extended-Reinhard 1000-nit knee, 2020->709) into the proven SDR layer
config. The 10-bit stream keeps its full decode depth either way.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The client now advertises VIDEO_CAP_10BIT|HDR and carries the result all
the way to glass:
- csc_rows is bit-depth exact (10-bit limited code points differ from
8-bit by ~half a code) and folds in the P010/X6 MSB-packing factor;
new 10-bit white/black tests.
- The CSC shader grows a params block: mode 0 passes the transfer
through (SDR as-is, or PQ onto an HDR10 swapchain); mode 1 tonemaps
PQ→SDR in-shader (ST.2084 EOTF, 203-nit reference white exposure,
BT.2020→709, soft maxRGB rolloff, sRGB encode) for desktops without
an HDR surface. PUNKTFUNK_TONEMAP_PEAK tunes the rolloff.
- The presenter probes VK_EXT_swapchain_colorspace + an HDR10/ST.2084
10-bit surface format and flips modes in-band with the stream's PQ
signaling: fence-quiesce, then CSC pass + video image (10-bit
A2B10G10R10 intermediate — PQ in 8 bits bands) + overlay pipe +
swapchain rebuild through the deferred-destroy rules.
- P010 decodes through all three paths: Vulkan Video (X6 multiplanar
pool, R10X6 plane views), VAAPI dmabuf (R16/RG1616 plane imports),
software (swscale as before).
- session pump advertises the caps; the host still gates Main10 behind
its PUNKTFUNK_10BIT policy.
Probed on glass hardware: the KDE/NVIDIA surface exposes
A2B10G10R10+HDR10_ST2084, so true PQ passthrough is available there.
Known v1 gaps: software-decode PQ shows untonemapped (8-bit RGBA
carries the transfer baked); the SDR overlay composites unscaled onto
an HDR10 surface (dim OSD); no vkSetHdrMetadataEXT yet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The §6.1 presenter↔console-UI contract lands: pf-presenter exposes its
device (SharedDevice) and composites at most one premultiplied-alpha
quad per frame (new overlay.frag + LOAD render pass over the swapchain;
zero cost while the overlay returns None). pf-console-ui implements it
with skia-safe on the shared VkDevice: DirectContext via the ash
dispatch chain, a ring of two offscreen render targets (one-frame-in-
flight safe), damage-driven redraws — the OSD re-renders at 1 Hz, the
hint on capture toggles, nothing per-frame. Skia never touches the
swapchain. The session binary carries it behind the default ui feature:
4.9 MB stripped without, 10 MB with (measured).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The decoder's NV12 dmabuf imports per-plane (R8 + GR88, explicit DRM
format modifier, dedicated dup'd-fd import, FOREIGN→graphics acquire)
and a fullscreen-triangle render pass converts it into the presenter's
video image with the CICP-driven coefficients ported from video_gl.rs
(same tests, plus a rows-vs-matrix agreement check). SPIR-V is committed
(shaders/build.sh regenerates) so builds and CI need no toolchain. The
import extension set is probed at device creation; unsupported boxes and
3-failure streaks demote the decoder to software via the existing
force_software contract. The session binary now honors the Settings
decoder preference instead of forcing software.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>