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.
Release notes
One file per stable release: docs/releases/vX.Y.Z.md. Its contents become the Gitea release
body verbatim and are the source of the Discord #releases announcement.
Why this exists
Releases used to be created by CI with an empty body; the notes were pasted in by hand afterward. That left a window where the release — and anything announcing it — carried no notes. Now the notes are authored before the tag is pushed, as part of the version bump, so the release is born complete and the announcement always has something to say.
The flow
- Write the notes. Add
docs/releases/vX.Y.Z.mdin the same commit (or PR) as the version bump. CopyTEMPLATE.mdand fill it in. This file is the single source of truth for the body. - Tag & push.
git tag -a vX.Y.Z … && git push origin vX.Y.Zfans out to the build workflows. Whichever one wins the create race seeds the release body from this file (scripts/ci/gitea-release.sh→ensure_release, and its PowerShell twin). The release page shows the notes immediately. - Wait for green. Let every platform's CI finish and go green.
- Announce. Dispatch the
announceworkflow (.gitea/workflows/announce.yml) with the tag. It re-asserts this file over the live release (so any late edit wins) and posts an embed to Discord#releases. Pressing "go" is the quality gate — a half-built release is never announced. Stable-only; a-rctag is refused unlessallow_prerelease=true.
Editing the notes after the tag is fine: update this file, then re-run step 4 (or PATCH the body via the API) — the announce step always re-syncs from the file, so the file stays authoritative even across a tag re-point.
Canary / -rc builds have no file here on purpose: they get no curated body and are not
announced.
Voice & format
Write for the people who USE Punktfunk to stream their games and desktops — not for the people who
build it. A non-engineer should finish knowing what's new and whether it affects them; an engineer
should never be confused or forced to decode internals. (See any recent vX.Y.Z.md for the target.)
- Lead with the benefit. Each entry = what the user can now do, what now works, or what stopped going wrong — in their words. Implementation is not the story.
- No internal vocabulary in the body. No protocol/message names, code type names, hex codes or hardware IDs, crate/component names, or API symbols. Translate any essential detail to plain language. Name things users recognize (iPad, Apple Pencil, Steam Deck, Android TV, the Windows sign-in screen) — not subsystems.
- Group as New / Improved / Fixed, each a bold one-line lead-in + a tight plain explanation.
Skimmable. The lead-in text before the first
##is what the Discord announcement shows, so make it a real, plain-language summary. - Be specific and honest — no vague "various improvements"; a reader should know exactly what changed.
- Compatibility line up top, in plain terms: can they update one side at a time? does their existing setup keep working? No version numbers in the lead.
- All protocol / ABI / driver / embedder detail goes in ONE
## Under the hood (for developers)section at the very bottom — the only place internal names and version numbers belong, clearly optional. The old dense engineering style survives only there.
The short annotated-tag message stays separate and short (a headline + a paragraph); it is the tag object's message, not this file.