The store plan wants four frames — stream, library, controllers, settings — on Android and
Apple. The screenshot harness could shoot two of them. This adds the missing two and fixes four
ways the captures were lying about what the app looks like.
The two missing frames
Library. Android gets a LibraryScene built on the real Coverflow (made internal, shell
rebuilt around it) rather than a lookalike. The art has to arrive synchronously or Robolectric's
frozen mainClock captures empty posters, so coil-test's FakeImageLoaderEngine answers the
requests inline. Apple gets 11-library, with the game entries decoded from JSON — the
memberwise init is internal to PunktfunkKit, so Codable is the construction surface from a
test target.
Controllers, with controllers in them. Both platforms could only shoot the empty state,
because the screens read live hardware. Android grows a PadInfo model and a padsOverride on
both screens; Apple's ControllerTestView is refactored to render a value model (ShotPad + InputSnapshot) with the live path flattening DiscoveredController and sampling at 30 Hz, so init(shotPads:) can inject instead. The scenes stage a DualSense on p1 and an Xbox pad on p2
with real VID:PIDs.
Four ways the captures were wrong
The frames were portrait. The app is built for horizontal use and the store frames showed
it upright. They go landscape.
No status bar. Robolectric renders without insets, so the Pixel renders put the screen
title straight through the punch-hole once composited onto a device. ShotStatusFrame adds a
believable bar, cleared in the centre for the hole — on for touch screens, off for immersive
and TV.
A sunset Apple TV set. The aurora screens read the live uiPalette, so a simulator that
had been used before shipped whatever palette it had saved. Pinned in ScreenshotHostView via PUNKTFUNK_SHOT_PALETTE, violet by default.
The macOS leg built the harness out of existence.shoot_macos built -c release while
the entire harness is behind #if DEBUG, so the normal app launched, never printed PF_SHOT_WINDOW, and every scene "never reported a window" — the Killed: 9 in the log was
the script's own cleanup, not the cause. Debug build now; same pixels, SwiftUI has no
release-only visuals.
Also here: the add-host sheet becomes a scene (the Blender store scenes were showing a phone
screen three designs old), and 11-library joins the tvOS scene list in apple.yml. 12-controllers deliberately does not — that view is #if DEBUG && !os(tvOS), a pre-existing
guard, since tvOS uses the native focus engine.
Verification
Android is verified — the full 40-shot set is captured and staged in unom/punktfunk-shots.
Apple compiles and all 29 shots (iPhone 8, iPad 8, Apple TV 5 at 3840×2160, macOS 8) were
captured locally.
Two things to watch on the first CI run of the screenshots job:
The tvOS leg is unproven on the runner. Locally, tvOS-simulator builds fail on both Xcode
26.6 and the 27 beta: the planner schedules swiftui-navigation-transitions' macro targets for
the tvOS triple and never plans their swift-syntax dependencies. The local workaround is
temporarily unlinking the product from the tvOS target; it is documented in screenshots.sh.
The CI leg is best-effort and warns rather than reds, so if the runner hits the same wall the
job stays green with no tvOS captures.
Eyeball 12-controllers for clipping at the bottom — the card budget for two pads on one
canvas was an estimate.
simctl io screenshot cannot write to an external volume (TCC denies it), so OUT must be on
the internal disk — relevant only for local runs.
The store plan wants four frames — stream, library, controllers, settings — on Android and
Apple. The screenshot harness could shoot two of them. This adds the missing two and fixes four
ways the captures were lying about what the app looks like.
## The two missing frames
**Library.** Android gets a `LibraryScene` built on the real `Coverflow` (made internal, shell
rebuilt around it) rather than a lookalike. The art has to arrive synchronously or Robolectric's
frozen `mainClock` captures empty posters, so coil-test's `FakeImageLoaderEngine` answers the
requests inline. Apple gets `11-library`, with the game entries decoded from JSON — the
memberwise init is internal to PunktfunkKit, so `Codable` is the construction surface from a
test target.
**Controllers, with controllers in them.** Both platforms could only shoot the empty state,
because the screens read live hardware. Android grows a `PadInfo` model and a `padsOverride` on
both screens; Apple's `ControllerTestView` is refactored to render a value model (`ShotPad` +
`InputSnapshot`) with the live path flattening `DiscoveredController` and sampling at 30 Hz, so
`init(shotPads:)` can inject instead. The scenes stage a DualSense on p1 and an Xbox pad on p2
with real VID:PIDs.
## Four ways the captures were wrong
- **The frames were portrait.** The app is built for horizontal use and the store frames showed
it upright. They go landscape.
- **No status bar.** Robolectric renders without insets, so the Pixel renders put the screen
title straight through the punch-hole once composited onto a device. `ShotStatusFrame` adds a
believable bar, cleared in the centre for the hole — on for touch screens, off for immersive
and TV.
- **A sunset Apple TV set.** The aurora screens read the live `uiPalette`, so a simulator that
had been used before shipped whatever palette it had saved. Pinned in `ScreenshotHostView` via
`PUNKTFUNK_SHOT_PALETTE`, violet by default.
- **The macOS leg built the harness out of existence.** `shoot_macos` built `-c release` while
the entire harness is behind `#if DEBUG`, so the normal app launched, never printed
`PF_SHOT_WINDOW`, and every scene "never reported a window" — the `Killed: 9` in the log was
the script's own cleanup, not the cause. Debug build now; same pixels, SwiftUI has no
release-only visuals.
Also here: the add-host sheet becomes a scene (the Blender store scenes were showing a phone
screen three designs old), and `11-library` joins the tvOS scene list in `apple.yml`.
`12-controllers` deliberately does not — that view is `#if DEBUG && !os(tvOS)`, a pre-existing
guard, since tvOS uses the native focus engine.
## Verification
Android is verified — the full 40-shot set is captured and staged in `unom/punktfunk-shots`.
Apple compiles and all 29 shots (iPhone 8, iPad 8, Apple TV 5 at 3840×2160, macOS 8) were
captured locally.
Two things to watch on the first CI run of the `screenshots` job:
1. **The tvOS leg is unproven on the runner.** Locally, tvOS-simulator builds fail on both Xcode
26.6 and the 27 beta: the planner schedules swiftui-navigation-transitions' macro targets for
the tvOS triple and never plans their swift-syntax dependencies. The local workaround is
temporarily unlinking the product from the tvOS target; it is documented in `screenshots.sh`.
The CI leg is best-effort and warns rather than reds, so if the runner hits the same wall the
job stays green with no tvOS captures.
2. **Eyeball `12-controllers` for clipping at the bottom** — the card budget for two pads on one
canvas was an estimate.
`simctl io screenshot` cannot write to an external volume (TCC denies it), so `OUT` must be on
the internal disk — relevant only for local runs.
The store plan's PICK & PLAY and FEEL THE GAME shots had no scene on any
platform: the library screen's state comes off the network, and the
controllers screens enumerate InputDevices, of which Robolectric has none
(the old shot honestly said 'no controller detected' — a palette proof
that sells nothing).
- Android library: Coverflow goes internal and LibraryScene rebuilds the
real shell around it (aurora, header, hint bar) with a mock shelf.
Cover art is answered synchronously by coil-test's FakeImageLoaderEngine
with generated gradient posters, so the frozen animation clock never
races an async load. Shot at phone portrait+landscape and TV geometry.
- Android controllers: PadRow renders a PadInfo model instead of a raw
InputDevice (padInfoOf maps real devices; both screens take a
padsOverride). The scenes inject the two pads the listing names —
DualSense (player 1) and Xbox (player 2), real VID:PIDs.
- Apple library: ShotLibrary composes the real LibraryCoverflowView with
a JSON-decoded mock shelf (GameEntry's memberwise init is internal to
PunktfunkKit; Codable is the public construction surface), registered
as cross-platform scene 11-library and added to the store set + the
tvOS CI scene list. Artless entries settle to their deterministic
fallback posters, which is also what keeps the shot offline.
Apple controllers stays a follow-up: ControllerTestView binds to live
GCController hardware and has no injection surface yet.
Verified: all 31 Roborazzi scenes render; the new tv-library,
phone-library and controllers shots reviewed by eye.
ControllerTestView drew straight from GCController/GCExtendedGamepad, and a
GCController cannot be constructed — the store plan's FEEL THE GAME frame
had no Apple scene. Every card now renders plain values (ShotPad,
InputSnapshot): the live path flattens the active DiscoveredController and
samples the pad into a snapshot on each 30 Hz tick, the screenshot harness
hands the panel pads that were never connected via a default-nil shotPads
parameter (the seam Android's ControllersScreen grew in 0a468c96). Live
behavior is unchanged — same cards, same order, same live feeds.
The 12-controllers scene injects the two pads the listing names — the
DualSense leading with the feedback surface (adaptive-trigger effects,
rumble backend, lightbar + player LEDs), the Xbox pad carrying the input
readout frozen mid-game; transport/battery/player ride in the header's
detail line because the panel has no dedicated battery row. Registered in
the iOS/macOS block and the store set only: ControllerTestView does not
build on tvOS, so the tvOS CI scene list is untouched.
Portrait captures show a layout nobody streams in. The touch controllers
frame and the library shot now render at landscape phone geometry (the
portrait library variant is gone), a console-controllers-landscape frame
joins the set, and the Apple 12-controllers scene rotates: on the
landscape canvas the two pads sit as side-by-side columns — one
ControllerTestView per pad — so neither story is cut by the short height.
Known wart, deliberate: the console landscape frame's floating legend
overlaps the second pad card mid-scroll; the styled composite crops above
it, and the touch variant carries the uncropped two-card view.
The aurora screens read the LIVE uiPalette default, and shot mode never
forced one: the Apple TV Simulator had a sunset palette persisted from
manual use, so every tvOS capture came out pink-on-pale while the iPhone
set stayed violet. ScreenshotHostView now pins the palette (violet, or
PUNKTFUNK_SHOT_PALETTE) before the scene mounts.
Also documents the local tvOS-SIMULATOR wall in screenshots.sh: Xcode
26.6 and the 27 beta plan the macro targets swiftui-navigation-transitions
pulls in for the tvOS triple and never schedule their swift-syntax deps
('unable to resolve module dependency') — prebuilts on or off. Only the
tvOS target links that package, which is why iOS and device builds never
hit it. Local workaround, since HomeView's use is canImport-guarded:
temporarily unlink the product from the tvOS target, capture, restore.
The whole shot harness is #if DEBUG, and shoot_macos built -c release —
so the binary launched as the NORMAL app, never printed PF_SHOT_WINDOW,
and every scene 'never reported a window' while the script SIGKILLed a
perfectly healthy app. Build debug: SwiftUI has no release-only visuals,
and the harness actually exists there. All eight mac scenes capture now.
The Blender store scenes render whatever screens/ holds, and theirs were
June captures of the pre-console UI. Fresh captures existed for hosts and
pair but the add-host sheet had no scene: AddHostSheet's state is hoisted
(ConnectScreen keeps half-typed values across dismissal), so the scene
passes a filled form straight in.
Two capture-truth fixes with it: dialog scenes advance the frozen clock
1.6 s (a ModalBottomSheet's entrance spring is still mid-rise at 0.8 s),
and the add-host shot uses Pixel-like geometry (411×915dp @ 420 dpi —
same 1080×2400 px, but the dp headroom is what lets the Connect button,
the row carrying the resolution promise, fit in frame).
Robolectric renders no system UI and zero insets, so every phone capture
was missing the status bar and its content sat where the bar belongs — on
the Pixel store render the app title collided with the camera punch-hole.
ShotStatusFrame draws a plausible bar (time left, radios right, the
CENTRE left empty for the hole) and pushes the scene below it, the same
geometry real insets produce; height mirrors a Pixel's tall bar measured
off a real capture. On for the touch screens, off for the immersive
surfaces (stream, console shell, TV) that hide the real bar too.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The store plan wants four frames — stream, library, controllers, settings — on Android and
Apple. The screenshot harness could shoot two of them. This adds the missing two and fixes four
ways the captures were lying about what the app looks like.
The two missing frames
Library. Android gets a
LibraryScenebuilt on the realCoverflow(made internal, shellrebuilt around it) rather than a lookalike. The art has to arrive synchronously or Robolectric's
frozen
mainClockcaptures empty posters, so coil-test'sFakeImageLoaderEngineanswers therequests inline. Apple gets
11-library, with the game entries decoded from JSON — thememberwise init is internal to PunktfunkKit, so
Codableis the construction surface from atest target.
Controllers, with controllers in them. Both platforms could only shoot the empty state,
because the screens read live hardware. Android grows a
PadInfomodel and apadsOverrideonboth screens; Apple's
ControllerTestViewis refactored to render a value model (ShotPad+InputSnapshot) with the live path flatteningDiscoveredControllerand sampling at 30 Hz, soinit(shotPads:)can inject instead. The scenes stage a DualSense on p1 and an Xbox pad on p2with real VID:PIDs.
Four ways the captures were wrong
it upright. They go landscape.
title straight through the punch-hole once composited onto a device.
ShotStatusFrameadds abelievable bar, cleared in the centre for the hole — on for touch screens, off for immersive
and TV.
uiPalette, so a simulator thathad been used before shipped whatever palette it had saved. Pinned in
ScreenshotHostViewviaPUNKTFUNK_SHOT_PALETTE, violet by default.shoot_macosbuilt-c releasewhilethe entire harness is behind
#if DEBUG, so the normal app launched, never printedPF_SHOT_WINDOW, and every scene "never reported a window" — theKilled: 9in the log wasthe script's own cleanup, not the cause. Debug build now; same pixels, SwiftUI has no
release-only visuals.
Also here: the add-host sheet becomes a scene (the Blender store scenes were showing a phone
screen three designs old), and
11-libraryjoins the tvOS scene list inapple.yml.12-controllersdeliberately does not — that view is#if DEBUG && !os(tvOS), a pre-existingguard, since tvOS uses the native focus engine.
Verification
Android is verified — the full 40-shot set is captured and staged in
unom/punktfunk-shots.Apple compiles and all 29 shots (iPhone 8, iPad 8, Apple TV 5 at 3840×2160, macOS 8) were
captured locally.
Two things to watch on the first CI run of the
screenshotsjob:26.6 and the 27 beta: the planner schedules swiftui-navigation-transitions' macro targets for
the tvOS triple and never plans their swift-syntax dependencies. The local workaround is
temporarily unlinking the product from the tvOS target; it is documented in
screenshots.sh.The CI leg is best-effort and warns rather than reds, so if the runner hits the same wall the
job stays green with no tvOS captures.
12-controllersfor clipping at the bottom — the card budget for two pads on onecanvas was an estimate.
simctl io screenshotcannot write to an external volume (TCC denies it), soOUTmust be onthe internal disk — relevant only for local runs.
The aurora screens read the LIVE uiPalette default, and shot mode never forced one: the Apple TV Simulator had a sunset palette persisted from manual use, so every tvOS capture came out pink-on-pale while the iPhone set stayed violet. ScreenshotHostView now pins the palette (violet, or PUNKTFUNK_SHOT_PALETTE) before the scene mounts. Also documents the local tvOS-SIMULATOR wall in screenshots.sh: Xcode 26.6 and the 27 beta plan the macro targets swiftui-navigation-transitions pulls in for the tvOS triple and never schedule their swift-syntax deps ('unable to resolve module dependency') — prebuilts on or off. Only the tvOS target links that package, which is why iOS and device builds never hit it. Local workaround, since HomeView's use is canImport-guarded: temporarily unlink the product from the tvOS target, capture, restore.