"The mouse cursor isn't included in the captured image" sat in Known Limits
contradicting the section above it, which already explained that the host draws
the pointer back in. Both are half-true and the difference matters to a reader
choosing whether to install anything: gamescope does leave the pointer out, you
do still see one, and what it costs is a full pass over every frame — plus, on
the fastest encode paths, the pointer itself, because a fixed-function front
end has nowhere to blend it.
Which is the real argument for `punktfunk-gamescope` on a box that will never
turn HDR on, and the page never made it.
Release notes gain the spawn-flag verification and the packaging wiring.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gamescope keeps the pointer out of its PipeWire node — it lives on a hardware
plane for scanout, and `paint_pipewire()` composites a separate, reduced frame
that never includes it. So punktfunk has always reconstructed it from XFixes
and blended it in host-side.
That blend is what has been blocking the zero-CSC encode path, and the cost is
larger than it sounds: `VulkanVideoEncoder::open` refuses the RGB-direct (EFC)
source for any session with `cursor_blend`, because that front end is
fixed-function and has no blend stage. `cursor_blend_for` sets it
unconditionally for gamescope. Net effect: a gamescope session paid a
full-frame colour-conversion pass per frame, forever, for a pointer.
So put the cursor where it belongs. A second carried gamescope patch adds
`--pipewire-composite-cursor` (off by default — the node has never carried it,
and a consumer that draws its own would get two), painting it with the same
`MouseCursor::paint` call the scanout composite uses. It scales for free:
paint_pipewire has already set `currentOutputWidth/Height` to the capture size,
which is what that function scales against. The repaint test grows the cursor's
state beside the commit ids — a pointer-only move produces no commit, so
without it the composited cursor would freeze on a static screen while the real
one moved, and a cursor that became hidden would never be erased.
Host side, the `+pfhdr` marker becomes a monotonic PATCH LEVEL, so one probe
answers every capability the session must know before it is planned (level 1 =
HDR formats, level 2 = the cursor flag). `cursor_blend_for` and the
`gamescope_cursor` resolver both consult it through one helper, because they
have to agree: the reader without the blend is a wasted X11 connection, the
blend without the reader is a stream with no pointer, and both together with a
gamescope that paints its own would draw two.
⚠ The two indirect spawn modes carry the flag through `PF_HDR_ARGS`, so this
shares a dependency with the HDR flags: a session that ignores
`GAMESCOPE_BIN`/`PATH` and execs the distro's gamescope gets neither. HDR fails
loudly there (negotiation timeout + SDR latch); a missing cursor would be
silent. Noted in packaging/gamescope/README.md as worth a post-spawn
`/proc/<pid>/cmdline` check if it ever bites.
`ServerCodecModeSupport` layered `SCM_HEVC_MAIN10` and never `SCM_AV1_MAIN10`,
on the stated theory that "the GameStream AV1 path is left off until
live-confirmed". But the SDR baseline has always offered AV1 **Main8** to every
client, so that path is either live or it is not — the DEPTH was never the
uncertain part, and the omission only cost AV1-preferring clients their HDR.
Now that the encoders probe 10-bit per codec, each bit is gated on that codec's
own `can_encode_10bit` AND the SDR baseline already advertising it. A box that
does HEVC Main10 but not 10-bit AV1 — or the reverse — advertises the truth
instead of one bit standing in for both.
Two gates follow from that:
* `host_hdr_capable` becomes codec-agnostic (ANY 10-bit-capable codec makes the
host HDR-capable). It was asking about HEVC alone, which would have hidden
HDR entirely on a hypothetical AV1-only-10-bit box;
* the RTSP honor gains the per-session half: a client that negotiated the codec
this host CANNOT do 10-bit with degrades to 8-bit SDR there, rather than
being handed a PQ label over an 8-bit stream. H.264 always lands there —
there is no 10-bit H.264 encode anywhere.
The unit test now pins each bit independently, including the two one-without-
the-other cases a single shared flag got wrong in both directions.
Three fixes to the same mistake: deciding what the Vulkan Video backend can do
from a table in our heads rather than from the driver, and routing everything
that didn't fit to libav VAAPI — where a session loses real RFI recovery and
the cursor blend for no reason the hardware asked for.
**Capability probe, per codec AND depth.** `probe_encode_support`'s "is there
an encode queue" boolean becomes `VulkanEncodeCaps { supported, eight_bit,
ten_bit }`, answered by `vkGetPhysicalDeviceVideoCapabilitiesKHR` against the
very profile chain the session open builds. So the dispatcher's prediction
cannot disagree with reality: a capable device keeps the Vulkan path, an
incapable one routes to VAAPI BEFORE burning a failed open, and the
cursor-blend mirror stays honest for free. This is the shape the direct-SDK
NVENC path already uses for its codec GUIDs.
**AV1 10-bit.** It was excluded on a guess about driver coverage; now the
device answers. `color_config()` carries `high_bitdepth` + the BT.2020/PQ CICP
triplet in both the `StdVideoAV1ColorConfig` and the sequence-header OBU we
bit-pack ourselves — they must stay identical or the driver's frame OBUs parse
against a header we didn't write. `high_bitdepth` sits BEFORE the CICP bytes,
so getting it wrong doesn't just mislabel the depth, it puts every following
field one bit out of phase; the new test reads the packed bits back.
**Zero-CSC RGB-direct in HDR.** The EFC probe assumed BT.709 and BGRA. It now
asks for the model this session's colourimetry needs (`MODEL_YCBCR_2020` for
10-bit — the extension has always had it) and for the CAPTURED format as an
encode-source format, and the session create-info selects the matching model.
An HDR session with no pointer to composite therefore hands the captured
buffer straight to the fixed-function front end and runs no host CSC at all.
Sessions that DO composite a pointer keep the compute CSC, unchanged: the EFC
cannot blend, and that rule outranks everything.
Also: `can_encode_10bit` on AMD/Intel now reports the union of VAAPI's and
Vulkan Video's answers instead of VAAPI's alone. `open_amd_intel` tries Vulkan
first and falls back, so either one being able to encode Main10 makes the
session 10-bit-capable — answering `false` because only one of them said yes
stranded encodable HDR sessions at 8 bits.
The Vulkan Video backend was 8-bit for no structural reason — the API has
`VK_VIDEO_COMPONENT_BIT_DEPTH_10_BIT` and `PROFILE_IDC_MAIN_10` in the very
fields this pinned to 8 and MAIN, and AMD VCN and Intel both encode Main10.
It was six hardcoded sites, and the cost of leaving them was paid twice over:
an HDR session had to take libav VAAPI, losing real RFI loss recovery AND the
compute CSC's cursor blend — which on gamescope is the only way the pointer
reaches the stream at all, since gamescope has no embedded-cursor mode.
An HDR session now opens a Main10 profile with 10-bit component depths, a
`G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16` picture + DPB, and an SPS carrying
`bit_depth_*_minus8 = 2` with the BT.2020/PQ CICP triplet instead of BT.709.
`rgb2yuv10.comp` is the CSC's twin, and the two interesting parts of it are:
* it is a PURE 3x3 matrix. The samples arrive already PQ-encoded (gamescope
composites into the PQ container), so BT.2020 NCL applies to the code values
as they are — there is no transfer function to apply here and applying one
would be wrong;
* the scratch planes are `R16`/`RG16`, not the picture's plane formats. The
10-bit ycbcr plane formats are not storage-image formats, so the shader
writes the value into the HIGH bits by hand (`code10 << 6`, hence the
`64/65535` factor and not `1/1023`) into planes that are merely
SIZE-compatible with the picture's — which is all `vkCmdCopyImage` requires.
Scope and safety:
* HEVC only. AV1 10-bit encode has far thinner driver coverage, and a session
open is not the place to gamble on it — those stay on VAAPI, as does a device
that fails the Main10 profile query inside the open (the pre-existing "failed
Vulkan open falls back to VAAPI" net, no new probe needed).
* HDR pins the compute-CSC arm over the EFC RGB-direct one, which the EFC could
not serve anyway: its fixed-function conversion is 8-bit BT.709 narrow with
no knob for BT.2020.
* `open_inner` binds `hdr` to the parameter-set HEADER bytes, so the depth flag
is `ten_bit` there — the one name collision this change had to route around.
Five pages asserted "gamescope's capture output is 8-bit" as a flat fact. It is
a fact about the STOCK binary, so say that instead, and say what to install to
change it: a new "HDR on gamescope" section covering the extra package, the two
knobs, what has to line up for a session to go HDR at all, and the two things
that surprise people — SDR content rides the same PQ stream at
`--hdr-sdr-content-nits`, and AMD/Intel HDR sessions currently lose the
composited pointer (the encode path that carries 10-bit is the one that cannot
blend it).
The roadmap's "parked / blocked" entry keeps the half that is still blocked
(Mutter's `RecordVirtual` is SDR-only through the GNOME 51 dev branch) and
drops the half that no longer is.
Duplicated the entire v0.20.1 fix bundle verbatim instead of
following house convention (each vX.Y.Z.md covers only what changed
since the immediately preceding release file — see v0.19.1 -> v0.19.2,
neither restates the other). v0.20.1 is a real, already-tagged
release with its own notes file; 0.21.0 only needed to add the
monitor-streaming feature, the KDE registry fix found while building
it, and the CI-only winget fix, with a pointer back to v0.20.1 for
the rest.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
42 commits since v0.20.0 (v0.20.1 was tagged but never announced;
superseded by this release rather than repointed, since real new
functionality landed after it was cut). Minor, not patch: the
headline is streaming one of the machine's own physical monitors
instead of always creating a virtual display — pick it from a new
console card or pin it in host.env, on KWin, Mutter, sway and
Hyprland, in both the Punktfunk app and Moonlight.
Also folds in the full v0.20.1 fix bundle: three Windows install
blockers found within hours of 0.20.0, GameStream/Moonlight compat
back to opt-in on fresh installs, three gamescope Game Mode takeover
faults, a laptop-panel stall misdiagnosis, and the PyroWave
high-bitrate latency-creep bundle — plus a KWin 6.7 regression found
while building the monitor feature (silent kscreen-doctor fallback
on every session) and a winget release-verification CI fix.
Wire protocol stays at 2, the embeddable C ABI at 13 and the Windows
virtual-display driver protocol at 6. GET /display/monitors is a new
additive endpoint; the display policy gains an optional
capture_monitor field. No embedder rebuild required.
Notes authored ahead of the tag per docs/releases/README.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>