Files
punktfunk/docs/releases
enricobuehler 4c587ef531
ci / docs-site (pull_request) Failing after 34s
ci / bun-nix (pull_request) Successful in 41s
ci / web (pull_request) Successful in 1m9s
apple / swift (pull_request) Successful in 1m34s
apple / screenshots (pull_request) Skipped
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m6s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m5s
ci / rust-arm64 (pull_request) Successful in 5m4s
android / android (pull_request) Successful in 5m17s
nix / flake (pull_request) Failing after 10m14s
ci / rust (pull_request) Successful in 15m52s
docs(release): split the technical half into CHANGELOG.md, and lead with a TL;DR
main went from 35ba64ca to a8a4b11f while this branch sat open — 190 more commits,
taking 0.25.0 to 327 since v0.24.0 and making it the largest release so far. That
scale is what forced the restructure.

THE SPLIT. Through v0.24.0 the engineering detail lived in an `## Under the hood
(for developers)` section at the bottom of the notes. It had grown to 21 dense
bullets sitting under the user-facing half — the exact burying the voice rules
exist to prevent, and it would only have got worse here. So:

  * `CHANGELOG.md` at the repo root is now the technical half, newest release
    first. It opens with a version table that lists every protocol number
    INCLUDING the unchanged ones, because "did the driver protocol move?" is the
    question an embedder most often needs answered and "no" is a real answer.
    Then breaking changes, capability bits, wire planes, env vars, and the rest.
  * `docs/releases/vX.Y.Z.md` keeps a short `## For developers` pointer and
    otherwise contains no internal names at all.
  * The link targets the file AT THE TAG, not at main. A release's notes are
    frozen; a main link would silently start describing a later release.
  * README.md and TEMPLATE.md now document this as the ritual rather than
    leaving it a one-off, including a new rule 7 requiring the TL;DR.

The notes are SHORTER than before the merge — 83 lines against 108 — while
covering three times as much. That is the point.

TL;DR, six bullets: FFmpeg gone from the desktop clients; AV1 at 4K shipping half
of every frame; HDR/10-bit leaving half the encoder idle; Windows hosts minting
their own audio devices; the controller sweep; and settings silently resetting.
The last bullet points at `## Before you update` rather than restating it.

WHAT I DELIBERATELY DID NOT PROMOTE. #96 (HEVC DPB) and the rav1d half of #97 are
ship-blockers for the NEW decode stack, not live field bugs — verified, `git
ls-tree v0.24.0 -- crates/` has none of pf-vkdecode/pf-bitstream/pf-dxvadec/
pf-vaadec and `dpb_limit` did not exist at the tag. Nobody on a released build
has ever hit them. They are in Fixed and in the changelog, not the TL;DR, which
is reserved for things a reader is living with today. (A memory note claiming
shipped 0.24.x clients carry the HEVC bug was wrong and has been corrected — it
was about to drive a release decision.)

Also kept out of user-facing Fixed: the plugin-UI empty panel (fixes the origin
split from earlier in THIS release, so it folds into that change — but its 47993
firewall consequence IS in `## Before you update`, because an upgraded host keeps
a 47992-only rule and every plugin interface goes blank), and the pad-audio
WASAPI path fix (repairs a Windows build break in never-shipped code).

HONESTY CARRIED FORWARD rather than smoothed. The notes say plainly that the
Android overlay change did not make the stream faster, that nothing is
recoverable from the Windows non-C: settings loss, that Deck HDR still needs
Steam's own display setting, and that VB-CABLE should be left installed. The
changelog's verification section says the FFmpeg-deletion milestone never
executed on a GPU, pf-vaadec has never decoded a frame anywhere, openh264 has
never run on glass, and controller audio has never touched a real DualSense.

Play's "What's new" refreshed and re-fitted: swapped the TV-profiles line for the
safe-area/notch fix, which is visible to every modern phone user. First draft came
out at 525 chars; trimmed to 497/500, verified with android.yml's gate logic.

Re-verified after the merge (Cargo.lock conflicted — took main's and regenerated
the bump): lock diff versions-only 35/35 against origin/main, `cargo metadata
--locked` resolves (39 members; fec-rs, pf-driver-proto, usbip-sim and the newly
vendored cros-codecs keep their own versions), `cargo fmt --all --check` clean in
both workspaces, doc lazy-continuation scanner 0 hits over 579 files, notes body 0
internal-vocabulary hits above `## For developers`.

Wire 2. C ABI 14 -> 17 (15 rumble floor, 16 pad audio, 17 session end reason).
Driver protocol 6 and gamepad channel 3 untouched. host_caps is down to its last
free bit (0x80) and video_caps has been full since 0.23.0 — both now stated in the
changelog rather than left to be rediscovered.
2026-08-07 20:24:08 +02:00
..

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

  1. Write the notes. Add docs/releases/vX.Y.Z.md in the same commit (or PR) as the version bump. Copy TEMPLATE.md and fill it in. This file is the single source of truth for the body.
  2. Tag & push. git tag -a vX.Y.Z … && git push origin vX.Y.Z fans out to the build workflows. Whichever one wins the create race seeds the release body from this file (scripts/ci/gitea-release.shensure_release, and its PowerShell twin). The release page shows the notes immediately.
  3. Wait for green. Let every platform's CI finish and go green.
  4. Announce. Dispatch the announce workflow (.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 -rc tag is refused unless allow_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.

Google Play "What's new": whatsnew/vX.Y.Z.txt

Play shows its own release notes on the Play Store listing and in the Play Store app, and caps them at 500 characters per language — the vX.Y.Z.md body is two orders of magnitude too long, so it gets its own short file: docs/releases/whatsnew/vX.Y.Z.txt.

Write it for a phone/TV user, not a host operator: only what changed in the Android app is worth their 500 characters. Plain text (Play renders no markdown), one bullet per line, same voice rules as below. Copy whatsnew/TEMPLATE.txt.

A vX.Y.Z tag without this file fails the android job before it builds. This is a hard gate, not a warning, because the failure it prevents is silent: when the file is missing Play does not show an empty "What's new" — it carries the previous release's text onto the new version, so the store listing describes a build nobody is getting, and nothing surfaces that but reading the listing. It is the same shape as the v0.22.3 notes announcing a feature that release never contained. The gate also rejects a file byte-identical to another release's, which is that bug reached by copy-paste instead of by omission.

The gate runs first in the job, so a miss costs a second and leaves nothing half-published — no build, no assets on the Gitea release, nothing on Play. Two more checks sit downstream: play-upload.py refuses text over the 500-char cap (printing the real count) before it uploads, because the API only rejects oversized notes at commit, by which point the AAB is already on Play.

Canary is exempt: it has no curated notes, and Play reusing text for internal testers costs nothing.

Same freeze rule as the notes: once the tag exists, this file is the record of what that versionCode shipped.

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.)

  1. 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.
  2. 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.
  3. 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.
  4. Be specific and honest — no vague "various improvements"; a reader should know exactly what changed.
  5. 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.
  6. No protocol / ABI / driver / embedder detail in this file at all. It goes in the root CHANGELOG.md (see below), and the notes carry a single short ## For developers section linking there. Nothing else in vX.Y.Z.md may use an internal name.
  7. Open with a ## TL;DR — three to six bullets naming only what most readers would be sorry to miss, each one line. A large release is exactly where a reader gives up, and the TL;DR is what they read instead of giving up. If something needs the reader to act, it belongs here and in ## Before you update, not buried in ## Fixed.

The technical half: root CHANGELOG.md

Why it is separate. Through v0.24.0 the engineering detail lived in an ## Under the hood (for developers) section at the bottom of each release's notes. That worked while releases were small. It stopped working: v0.25.0 is 300+ commits, and the section had grown long enough to bury the user-facing half it was appended to — the exact failure the voice rules exist to prevent. The two audiences also want different shapes. A user reads one release and wants prose; an embedder wants to diff across releases and see when the ABI moved, which is a table, not a paragraph.

So: vX.Y.Z.md is for people who use Punktfunk, CHANGELOG.md is for people who build against it, and neither has to compromise for the other.

Format. Newest release first, one ## vX.Y.Z section each. Lead with a version table (wire protocol, C ABI, driver protocol, gamepad channel — every row, marked unchanged where it did not move, because "unchanged" is the answer an embedder most often needs). Then breaking changes, then whatever else matters: capability bits, new environment variables, wire additions, workspace members. Internal names are the point here — use them.

Linking. The notes link to the file at the tag, not at main: https://git.unom.io/unom/punktfunk/src/tag/vX.Y.Z/CHANGELOG.md. A release's notes are frozen; a link to main would silently start describing a later release.

Same freeze rule. Add the release's section in the version-bump commit, alongside the notes.

The short annotated-tag message stays separate and short (a headline + a paragraph); it is the tag object's message, not this file.