Compare commits

...
Author SHA1 Message Date
enricobuehler ea5afbaa8c Merge pull request '0.28.1 notes — the Mac microphone loop is a headline fix the notes had never heard of' (#224) from worktree-release-0281-notes into main 2026-08-14 11:26:53 +00:00
enricobuehler 832a5ffd8d docs(release): the Mac microphone loop is a headline fix, and the notes had never heard of it
Ten more commits landed after the 0.28.1 release commit — the deb image fix, the
two macOS audio ones (#221 + #223) and the TV screenshot automation — so the
release paperwork no longer described the release.

CHANGELOG: 50 -> 60 commits since v0.28.0. Nothing else moves; the version table
is unchanged on every row, re-verified against the tag (`include/`,
`crates/pf-driver-proto`, `plugin-kit/package.json` and `sdk/` are all still
byte-identical to v0.28.0, so the C ABI stays 19). #221 brought its own CHANGELOG
section, so the technical half already covered it.

NOTES: the user-facing file had no mention of the macOS fault at all, and it is
headline-grade — streaming from a Mac with the mic on cut audio AND froze input
on a ~2.5 s metronome, with turning the microphone off as the only workaround. It
now leads the summary paragraph, has a TL;DR line and a full Fixed entry
explaining the loop in plain terms (a mic that cannot run echo cancellation, each
failed attempt knocking out the working path and thereby triggering the next).

The TL;DR was also trimmed from nine multi-line bullets to seven one-liners.
`docs/releases/README.md` asks for 3-6, and this release has an unusual number of
genuinely severe entries — seven is the honest floor without hiding one, and the
long-form detail was already duplicated below in Fixed, which is where it belongs.
The Apple stats-overlay and Apple TV colour bullets lost their TL;DR slots and
keep their Fixed entries.

`SessionAudio.start()` being asynchronous on macOS is added to the notes' `For
developers` paragraph — it is the one embedder-visible edge in #223, and an
embedder who only reads the notes would otherwise meet it at runtime.

Play notes are untouched and still accurate: the only commit to touch
clients/android since is `b6b3c10c`, which is screenshot CI, not app behaviour.

Gates on this tree: fmt clean, `cargo metadata --locked` consistent,
`cargo test -p punktfunk-core` 210 passed, C ABI harness abi_version=19,
`api/openapi.json` and the docs-site copy still byte-identical.
2026-08-14 13:15:08 +02:00
enricobuehler 76c677a8f8 Merge pull request 'The TV storefronts were the only ones with no automated screenshot captures' (#222) from worktree-store-shots-tv-automation into main
Reviewed-on: unom/punktfunk#222
2026-08-14 11:11:46 +00:00
enricobuehler 7cb70bf6ea Merge pull request 'Apple audio engine starts leave the main thread — input never waits on the audio server' (#223) from worktree-macos-mic-rebuild-loop into main 2026-08-14 11:05:55 +00:00
enricobuehler b6b3c10cb5 ci(screenshots): the TV storefronts were the only ones with no automated captures
Google Play's Android TV slot needs 16:9 1920x1080 shots and the App Store
needs Apple TV 1920x1080 — neither existed as automation output:

- apple.yml screenshots job now runs the tvos leg. The harness supported it
  all along (tools/screenshots.sh tvos); what the job was missing is the
  Tier-3 tvOS xcframework slices (nightly + -Zbuild-std, same recipe the
  distribute job uses on this runner) and an explicit scene list — the
  gamepad-console scenes are compiled out on tvOS, and an UNKNOWN scene
  name falls back to a normal app launch, which would silently capture the
  real empty app. Still best-effort: a tvOS hiccup warns, never reds.
- TvScreenshotTest renders the console scenes + the stream HUD at Android
  TV geometry (w960dp-h540dp-television-xhdpi = native 1920x1080, no
  resampling), prefixed tv- so the artifact separates the form factors.
  Verified locally: 6 scenes, all 1920x1080.

android-screenshots.yml needs no change — it runs the whole unit-test task
and uploads the whole roborazzi output dir.
2026-08-14 12:55:03 +02:00
enricobuehler d669064dc0 Merge pull request 'The macOS device-change recovery answered itself — mic-on streams cut audio and input every ~2.5 s' (#221) from worktree-macos-mic-rebuild-loop into main
Reviewed-on: unom/punktfunk#221
2026-08-14 10:49:37 +00:00
enricobuehler d4ad8be6bf Merge pull request 'The gamescope deb image never needed x11-xcb until we started building the WSI layer' (#220) from worktree-gamescope-deb-x11xcb into main
Reviewed-on: unom/punktfunk#220
2026-08-14 10:49:24 +00:00
enricobuehler 1b28a7f7f1 fix(ci): the gamescope deb image never needed x11-xcb until we started building the WSI layer
The v0.28.1 deb leg failed for real, and the package it costs is the whole
punktfunk-gamescope .deb:

    gamescope/layer/meson.build:3:14: ERROR: Dependency "x11-xcb" not found, tried pkgconfig

Not a flake and not the pin. v0.28.1 flipped
`-Denable_gamescope_wsi_layer=true` in build-punktfunk-gamescope.sh (it was off
before, on the recorded and false premise that the layer is version-independent
of the compositor). The layer is a separate meson subdir with its own dependency
set, and it wants x11-xcb — which the compositor never did. So an image that had
been sufficient for every previous release stopped being sufficient the moment
the layer started building, and nothing named the new dep anywhere.

Debian is the only channel that has to name it: Arch's libx11 and Fedora's
libX11-devel both ship x11-xcb.pc themselves, which is why arch.yml and rpm.yml
build the same tree fine and only the trixie image came up short.

Asserted as well as installed. The image already asserts the wayland-server
floor at build time, on the argument that the one version deciding whether the
image can do its job should fail loudly HERE rather than inside a deb.yml run —
and this is the same class, only worse: a missing x11-xcb does not fail the
compositor build, it fails the layer's, and the layer is the only route to an
HDR10 swapchain for a nested game. Losing it silently produces a package that
looks completely healthy and denies every game HDR, which is precisely the
failure v0.28.1 exists to end. The assertion means the next dependency the layer
grows fails at image build instead of mid-release.

ORDERING, for whoever lands this: docker.yml rebuilds the image on a push to
main (its key hashes the ci/ tree, so this change busts it), and deb.yml's
gamescope job consumes `:latest`. Let the image publish before the deb job that
needs it runs — on a release cut that means merging this, letting docker.yml
finish, and only then pushing the tag. The failed job saved no cache, so the tag
run rebuilds against the new image rather than restoring the broken state.

NOT verified locally: no Docker on this machine, so the image was not built and
the layer was not compiled here. The package name is confirmed against Debian's
own package index (libx11-xcb-dev ships x11-xcb.pc, and exists in trixie), and
the assertion added here is what proves it in CI — if the name were wrong the
image build fails loudly instead of the deb leg failing quietly.
2026-08-14 12:03:55 +02:00
5 changed files with 102 additions and 22 deletions
+21 -10
View File
@@ -676,20 +676,23 @@ jobs:
# Skipped on PRs (cost); runs on main pushes + manual dispatch. Needs the build/test job green
# first, and is a separate job so a capture hiccup can never red the core signal.
#
# Scope = the two REQUIRED iOS sizes (iPhone 6.9" + iPad 13"), captured on the Simulator
# (`simctl io screenshot`, no Screen Recording grant needed). macOS and tvOS are deliberately
# NOT in CI: the self-hosted runner is headless (no window-server session), so the mac window
# capture can't run there; tvOS needs the Tier-3 build-std slice. Generate those two locally on
# a GUI Mac with `clients/apple/tools/screenshots.sh macos tvos`.
# Scope = the two REQUIRED iOS sizes (iPhone 6.9" + iPad 13") + Apple TV (1920×1080), captured
# on the Simulator (`simctl io screenshot`, no Screen Recording grant needed). The tvOS slice is
# Tier-3 (nightly -Zbuild-std, same as the distribute job — slow cold, cached on the self-hosted
# runner). The tvOS scene list is explicit: the gamepad-console scenes are iOS/macOS-only, and an
# unknown scene name falls back to a NORMAL app launch — the capture would silently be of the
# real empty app. macOS stays deliberately NOT in CI: the runner is headless (no window-server
# session), so the mac window capture can't run there — generate it locally on a GUI Mac with
# `clients/apple/tools/screenshots.sh macos`.
screenshots:
needs: swift
if: gitea.event_name != 'pull_request'
runs-on: macos-arm64
timeout-minutes: 75
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
- name: Rust toolchain + iOS Simulator targets
- name: Rust toolchain + iOS Simulator targets (+ nightly for the tvOS slices)
run: |
if ! command -v rustup >/dev/null && [ ! -x "$HOME/.cargo/bin/rustup" ]; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
@@ -699,6 +702,10 @@ jobs:
dirname "$RUSTUP" >> "$GITHUB_PATH"
"$RUSTUP" target add aarch64-apple-darwin x86_64-apple-darwin \
aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios
# tvOS targets are tier-3 (no prebuilt std) — build-xcframework.sh compiles them with
# nightly + -Zbuild-std, so ensure nightly + rust-src are present (see the swift job).
"$RUSTUP" toolchain install nightly --profile minimal
"$RUSTUP" component add rust-src --toolchain nightly
# Shared compile cache. The script handles the macOS side (user-prefix install +
# GITHUB_PATH, bsdtar globbing) — see scripts/ci/ensure-sccache.sh.
@@ -735,10 +742,10 @@ jobs:
-mtime +7 -exec rm -rf {} + 2>/dev/null || true
fi
- name: Build PunktfunkCore.xcframework (mac + iOS slices)
run: BUILD_IOS=1 bash scripts/build-xcframework.sh
- name: Build PunktfunkCore.xcframework (mac + iOS + tvOS slices)
run: BUILD_IOS=1 BUILD_TVOS=1 bash scripts/build-xcframework.sh
- name: Capture screenshots (iPhone 6.9" + iPad 13"; auto-creates the Simulators)
- name: Capture screenshots (iPhone 6.9" + iPad 13" + Apple TV; auto-creates the Simulators)
working-directory: clients/apple
env:
SETTLE: "8" # Simulators settle slower than a local run
@@ -746,6 +753,10 @@ jobs:
# Independent invocations: one platform failing skips it, not the other.
bash tools/screenshots.sh ios || echo "::warning::iOS (iPhone 6.9\") screenshots skipped"
bash tools/screenshots.sh ipad || echo "::warning::iPad 13\" screenshots skipped"
# tvOS shoots only the scenes that exist there — the 0609 gamepad-console scenes are
# compiled out on tvOS (native focus engine), and an unknown name = a normal app launch.
SCENES="01-stream 02-hosts 05-settings 03-pair" \
bash tools/screenshots.sh tvos || echo "::warning::Apple TV screenshots skipped"
echo "Produced:"; ls -la screenshots || true
- name: Shut the Simulators down (leaked booted sims once piled up 846 deep)
+1 -1
View File
@@ -14,7 +14,7 @@ with the version table of the release you are moving to, then read **Breaking ch
## v0.28.1
50 commits since v0.28.0.
60 commits since v0.28.0.
A patch release in the strict sense: **nothing on the wire, in the C ABI, in the driver protocol or
in the plugin contract moves.** Every host, client, driver and plugin built against v0.28.0 keeps
+15
View File
@@ -51,6 +51,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libxdamage-dev libxcomposite-dev libxrender-dev libxext-dev libxxf86vm-dev \
libxtst-dev libx11-dev libxres-dev libxmu-dev libxcursor-dev libxi-dev \
libxfixes-dev libxkbcommon-dev libxkbcommon-x11-dev libcap-dev libdrm-dev \
# x11-xcb is needed by the VULKAN WSI LAYER (layer/meson.build), not by the compositor — so it
# was not missed until v0.28.1 started building the layer beside the binary. Debian is the only
# channel that needs it named: Arch's libx11 and Fedora's libX11-devel both carry x11-xcb.pc
# themselves, while Debian splits it into its own -dev package.
libx11-xcb-dev \
libinput-dev libudev-dev libpipewire-0.3-dev libseat-dev libsdl2-dev \
libluajit-5.1-dev libavif-dev libdecor-0-dev hwdata libglm-dev libbenchmark-dev \
libvulkan-dev libxcb1-dev libxcb-composite0-dev libxcb-xfixes0-dev libxcb-res0-dev \
@@ -66,3 +71,13 @@ RUN set -eux; \
pkg-config --atleast-version=1.23.1 wayland-server \
|| { echo "wayland-server $have < 1.23.1 — the vendored wlroots will not configure" >&2; exit 1; }; \
echo "wayland-server $have — OK"
# The layer's own floor, asserted for the same reason: a missing x11-xcb does not fail the
# COMPOSITOR build, it fails `layer/meson.build` — and the layer is the only route to an HDR10
# swapchain for a nested game, so losing it silently ships a package that looks healthy and denies
# every game HDR. This is exactly how v0.28.1's deb leg broke, one release after the layer was
# added; assert it here so the next dep the layer grows fails at image build, not mid-release.
RUN set -eux; \
pkg-config --exists x11-xcb \
|| { echo "x11-xcb absent — the Vulkan WSI layer will not configure (need libx11-xcb-dev)" >&2; exit 1; }; \
echo "x11-xcb $(pkg-config --modversion x11-xcb) — OK"
@@ -0,0 +1,55 @@
package io.unom.punktfunk.screenshots
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onRoot
import com.github.takahirom.roborazzi.captureRoboImage
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
/**
* The same Roborazzi harness as ScreenshotTest, at Android TV geometry: 960×540dp in the
* `television` UI mode at xhdpi (2.0×) = 1920×1080 px the Play Store's 16:9 TV screenshot size,
* captured 1:1 with no resampling. Only the screens that exist on a TV are shot here: the
* gamepad-console shell (what LEANBACK_LAUNCHER opens into) and the in-stream view. Files are
* prefixed `tv-` so the artifact separates the form factors.
*/
@RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(sdk = [36], qualifiers = "w960dp-h540dp-television-xhdpi")
class TvScreenshotTest {
@get:Rule
val compose = createAndroidComposeRule<ComponentActivity>()
private val out = "build/outputs/roborazzi"
private fun shootRoot(name: String, content: @androidx.compose.runtime.Composable () -> Unit) {
compose.mainClock.autoAdvance = false
compose.setContent { ShotTheme(content) }
compose.mainClock.advanceTimeBy(800)
compose.onRoot().captureRoboImage("$out/tv-$name.png")
}
@Test
fun stream() = shootRoot("stream") { StreamScene(io.unom.punktfunk.StatsVerbosity.COMPACT) }
@Test
fun streamDetailed() =
shootRoot("stream-detailed") { StreamScene(io.unom.punktfunk.StatsVerbosity.DETAILED) }
@Test
fun consoleHome() = shootRoot("console-home") { ConsoleHomeScene() }
@Test
fun consoleSettings() = shootRoot("console-settings") { ConsoleSettingsScene() }
@Test
fun consoleControllers() = shootRoot("console-controllers") { ConsoleControllersScene() }
@Test
fun connectingConsole() = shootRoot("connecting-console") { ConnectConsoleScene() }
}
+10 -11
View File
@@ -1,20 +1,18 @@
Wire-compatible with 0.28.0 — everything you have already paired keeps working, and you can update one side at a time. Nothing here changes how a host and a client agree on what to send each other, so an old client on a new host, or the other way round, streams exactly as it does today.
This is a fix release, and most of it is about things that were wrong in ways nothing announced. On Windows, your Steam library could stay completely empty after the last update, and a Punktfunk host sitting idle — nothing connected, nothing streaming — could wreck the frame rate of a game you were playing on that same machine. An Android TV box could be silent for a whole session; a Steam Deck's Punktfunk home would not open; games could not reach high dynamic range (HDR) at all on Linux machines running our own compositor, while the Steam interface in front of them glared; and an Apple TV was being pushed into HDR mode by streams that were not HDR at all, lifting blacks to grey.
This is a fix release, and most of it is about things that were wrong in ways nothing announced. On Windows, your Steam library could stay completely empty after the last update, and a Punktfunk host sitting idle — nothing connected, nothing streaming — could wreck the frame rate of a game you were playing on that same machine. Streaming from a Mac with the microphone on cut the sound and froze the controls every couple of seconds. An Android TV box could be silent for a whole session; a Steam Deck's Punktfunk home would not open; games could not reach high dynamic range (HDR) at all on Linux machines running our own compositor, while the Steam interface in front of them glared; and an Apple TV was being pushed into HDR mode by streams that were not HDR at all, lifting blacks to grey.
Several of these came from people who reported them carefully enough to be found. One of them, the crash that lost your game on reconnect, was tracked down, fixed and proven on his own hardware by **luxus** — that fix is in this release as he wrote it.
## TL;DR
- **On Windows, your Steam games went missing after updating to 0.28.0.** The Steam add-on could publish nothing, because Steam keeps its artwork in Program Files and the host was not allowed to read it. Fixed — and one unreadable cover no longer throws away the whole library with it.
- **An idle Punktfunk host could ruin a game you were playing on that same machine**, down to 25 frames per second at worst, with nothing connected and no stream running. Two separate causes, both fixed, both reported from Helldivers 2.
- **No sound at all on some Android TV boxes** — an NVIDIA Shield in particular — is fixed, and the app now recovers when your TV or receiver changes mode mid-stream instead of staying silent for the rest of the session.
- **The Punktfunk home would not open on a Steam Deck after updating to 0.28.0.** That is fixed, along with the missing statistics overlay in streams that came from the same fault.
- **Games can finally use high dynamic range on Linux hosts running Punktfunk's compositor.** They could not before, on essentially any machine, and nothing said so.
- **HDR streams looked washed out while the Steam interface looked glaring** — one brightness mistake causing both, now corrected.
- **On an Apple TV, ordinary streams were switching the television into HDR** and lifting blacks to grey — and those same streams were being drawn with no colour information at all.
- **If your Apple device's statistics overlay ever showed impossibly good numbers, that is why** — three separate faults made those figures unreliable. Old measurements are worth re-taking.
- **Streaming your desktop with the mouse pointer forwarded failed outright on Hyprland and Sway**, giving you a black picture. It affected every current install, not only old ones.
- **On Windows, your Steam games went missing after 0.28.0** — the add-on could publish nothing, because Steam keeps its artwork somewhere the host was not allowed to read.
- **An idle host could ruin a game running on that same Windows machine**, down to 25 frames per second, with nothing connected and no stream running.
- **Streaming from a Mac with your microphone on cut the sound and froze your controls every couple of seconds** — and turning the mic off was the only way round it.
- **No sound at all on some Android TV boxes**, an NVIDIA Shield in particular — and sound now returns by itself when your TV changes mode mid-stream.
- **The Punktfunk home would not open on a Steam Deck after 0.28.0**, and streams elsewhere lost their statistics overlay to the same fault.
- **Streaming your desktop with the pointer forwarded gave a black picture on Hyprland and Sway** — every current install, not just old ones.
- **High dynamic range was broken in both directions on Linux hosts:** games could not reach it at all, and everything else was mapped a stop too bright. Both fixed.
## Before you update
@@ -38,6 +36,7 @@ Several of these came from people who reported them carefully enough to be found
- **On Windows, the Steam add-on synced nothing and your grid stayed empty.** Steam installs to Program Files and keeps its artwork there, and the host's list of places it is allowed to read art from covered the launchers that install per-user — but not Steam. Every cover the add-on offered was rejected, and because one bad cover failed the whole submission, that meant every Steam game rather than one thumbnail. Both halves are fixed: Steam's own install locations are now allowed (including on a second drive), and a cover that cannot be served is dropped so the rest of your library still arrives. This only affected Windows — every Steam layout on Linux was already covered — and it arrived with 0.28.0, when the built-in Steam scanner was replaced by the add-on.
- **A Punktfunk host that was doing nothing at all could wreck a game running on the same Windows machine** — 25 frames per second at worst, with no client connected and no stream ever started. Two independent causes, both from the same set of reports: the host presents a controller speaker that looks exactly like a real DualSense's, and a game that finds it engages its DualSense audio path against a device nothing is feeding; and the host was claiming your default microphone permanently, so any game holding an open voice input stalled on a virtual mic that only runs during a stream. The controller speaker is now hidden except while a controller is actually attached, and your microphone is only borrowed for the length of a stream. Machines already stuck repair themselves on the next start.
- **Streaming your desktop with the pointer forwarded gave a black picture on Hyprland and Sway.** Punktfunk asked the desktop's screen-sharing service for the pointer in a form neither compositor offers, and the request was refused before it ever reached them — so the whole capture failed rather than losing just the pointer. It now asks what is actually available and takes the best on offer. Measured against fully up-to-date Hyprland and portal versions: this was every current install, and updating them would not have helped. You will now see a single pointer drawn by the host instead of your client drawing its own.
- **Streaming from a Mac with the microphone on cut the audio and froze the controls on a two-and-a-half second beat**, while the picture carried on untouched. Some microphones — a multi-channel audio interface set as your Mac's input, for instance — cannot run the echo-cancelling mode Punktfunk asks for. Each failed attempt knocked out the working audio path, and the knock-out was itself the signal that triggered the next attempt, so it went round forever. Your controls froze on the same beat because that rebuild happened on the part of the app that also sends your keyboard and mouse. All three links in that loop are broken now, and audio setup no longer happens where your input lives — so even a genuine change of sound device no longer costs you control of the stream. Until now the only workaround was turning the client's microphone off.
- **An Android TV box could have no audio for an entire session.** Reported on an NVIDIA Shield with a host and settings that played fine everywhere else, and video unaffected. The app asked Android for an audio device and then trusted the answer: a device that accepted the request but played nothing was never noticed, a device that refused to start disabled sound for the rest of the session, and — most common on a television — a device that went away during an HDMI mode change was never reopened. It now proves that sound is actually being taken before it accepts a device, and reopens automatically when your TV or receiver changes mode. TV boxes now use the ordinary shared audio path rather than the exclusive low-latency one, which was never buying them the latency it cost.
- **The Punktfunk home would not open on a Steam Deck after updating to 0.28.0.** Both the Decky button and the library shortcut failed. The same fault silently removed the statistics and capture overlays inside streams on other machines. It came from a graphics library sizing itself to what the machine's graphics driver supported rather than to what Punktfunk had asked for, which only became a problem once drivers moved ahead of us.
- **Games could not use high dynamic range on Linux hosts running Punktfunk's compositor** — on essentially every machine, not a rare configuration. Reaching HDR requires a small graphics component that has to match the compositor exactly, and we shipped the compositor without it, leaving the host guessing whether the one your distribution provided was compatible. Because we build our own compositor, that guess was almost always wrong in the direction of switching HDR off. We now ship the matching component ourselves, on every Linux packaging channel.
@@ -61,4 +60,4 @@ Most of the rest of this release exists because people took the trouble to repor
Protocol, ABI, driver and embedder detail — including the version table and the (empty) breaking-changes list — is in [CHANGELOG.md](https://git.unom.io/unom/punktfunk/src/tag/v0.28.1/CHANGELOG.md).
Nothing breaks: the wire protocol, the C ABI, the virtual-display driver protocol and the plugin contract are all unchanged from v0.28.0. Two things are worth reading if they apply to you: every Linux packaging channel now ships a second gamescope artifact and a package without it fails to build rather than degrading quietly, and Android's `nativeStartAudio` takes one more argument.
Nothing breaks: the wire protocol, the C ABI, the virtual-display driver protocol and the plugin contract are all unchanged from v0.28.0. Three things are worth reading if they apply to you: every Linux packaging channel now ships a second gamescope artifact and a package without it fails to build rather than degrading quietly, Android's `nativeStartAudio` takes one more argument, and `SessionAudio.start()` is now asynchronous on macOS as it already was on iOS and tvOS — playback is live shortly after the call rather than on return.