The store's four marketing frames become screenshot scenes on both platforms #233

Merged
enricobuehler merged 7 commits from worktree-store-marketing-scenes into main 2026-08-14 19:49:34 +00:00
Owner

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 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.
enricobuehler added 7 commits 2026-08-14 19:20:50 +00:00
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).
fix(screenshots): captures grow the status bar Robolectric never had
ci / rust-arm64 (pull_request) Successful in 1m57s
ci / bun-nix (pull_request) Successful in 48s
ci / docs-site (pull_request) Successful in 1m55s
apple / swift (pull_request) Successful in 2m3s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 8m58s
android / android (pull_request) Successful in 6m12s
ci / rust (pull_request) Successful in 27m10s
026dbe6153
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.
enricobuehler merged commit d7e66fafe1 into main 2026-08-14 19:49:34 +00:00
enricobuehler deleted branch worktree-store-marketing-scenes 2026-08-14 19:49:35 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#233