Files
punktfunk/docs/releases
enricobuehler f702f27bd3
apple / swift (pull_request) Successful in 1m20s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 1m14s
ci / docs-site (pull_request) Successful in 1m19s
android / android (pull_request) Successful in 5m33s
ci / rust-arm64 (pull_request) Successful in 5m21s
ci / rust (pull_request) Failing after 5m56s
windows / build (aarch64-pc-windows-msvc) (pull_request) Failing after 41s
windows / build (x86_64-pc-windows-msvc) (pull_request) Failing after 43s
chore(release): bump workspace version to 0.25.0
A minor bump: 98 commits since v0.24.0. The headline is DualSense pad audio
(PR #23) — a wired DualSense playing a game's voice-coil haptics and its own
speaker, streamed from the host, on Android and the desktop session client
against a Windows host with Steam's driver present. Behind it: the haptics
sweep's twelve milestones closing more than twenty controller faults across
every client and both hosts; the audio quality/latency work (256 kbps stereo,
the Steam Streaming Microphone endpoint root cause, and the de-jitter ratchet
that left audio permanently behind the picture); and MTU resilience plus
mid-session shard renegotiation, which turns the silent all-black stream on a
sub-1330-byte path into a diagnosed warning that heals itself. Plus the Decky
plugin reduced to a launcher, system-button routing with hold-Select, gamepad-UI
profiles on all three UIs, `discover`/`launch --request-access` in the CLI, and
the Sunshine false-conflict and crashed-host display-restore fixes.

The canary base is already 0.25 — scripts/ci/pf-version.sh derives it as one
minor ahead of the latest stable tag — so this is the version canary has been
publishing against all along.

Wire protocol stays at 2: every addition this cycle is optional or
capability-gated (an optional trailing max_shard_payload on Hello, the 0x08/0x09
renegotiation pair, the 0xD1 pad-audio plane, the 0xD2 redundant desktop-audio
plane, MAX_DATAGRAM_BYTES 2048 -> 9216). C ABI moves 14 -> 16 in two steps: 15
retroactively declares the floor that guarantees the rumble policy engine's C
surface (which shipped while the constant still read 7), and 16 adds the
pad-audio surface and mirrors its two capability bits. Four new capability bits
land in the client/host bytes (audio redundancy 0x04/0x20, pad audio 0x08/0x40);
the video-caps byte was NOT touched and stays full from 0.23.0, so the standing
"next video cap needs a second byte and an ABI bump" note still holds. host_caps
is now down to its last free bit (0x80). Virtual-display driver protocol 6 and
the Windows gamepad channel 3 are untouched — pf-driver-proto is byte-for-byte
identical to v0.24.0. The generated header is in sync (ABI 16, both cap mirrors).

Breaking for C embedders: 149 unprefixed macros are now PUNKTFUNK_-prefixed
(139 #defines renamed in the checked-in header). Mechanical to fix, and it
cannot break silently — the old spellings cease to exist, so it is always an
undeclared-identifier error rather than the wrong value a colliding #define
used to produce.

Lock touched for the 32 workspace members only, via `cargo update --workspace`:
diff against origin/main is versions-only, 32 insertions and 32 deletions. Unlike
the last cut there is no third-party crate sitting on the outgoing version to
trip the count — `wasapi` is at 0.23.0 and was never a candidate. `cargo metadata
--locked` resolves (35 members; fec-rs, pf-driver-proto and usbip-sim keep their
own versions by design). `cargo fmt --all --check` clean in both the main and
packaging/windows/drivers workspaces. Doc lazy-continuation scanner: 0 hits over
521 files — that regex is the exact defect that made the first v0.23.0 tag go red
on Windows clippy, and no Windows leg runs on a main push, so main being green
proves nothing about the tag fan-out.

api/openapi.json is deliberately left at 0.23.0: it tracks API edits and lags,
as in every prior cut. It is now two releases behind and worth a look.

Notes at docs/releases/v0.25.0.md, per docs/releases/README.md — authored with
the bump so CI's ensure_release seeds the release body at tag creation. Body
voice checked programmatically: 0 internal-vocabulary hits above `## Under the
hood`. Play's "What's new" at docs/releases/whatsnew/v0.25.0.txt (494/500 chars),
verified by running android.yml's gate logic verbatim against it, including the
byte-identical-to-another-release check.
2026-08-05 00:05:40 +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. 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.