Zum Inhalt

tvOS „Physarum Ambient" — Lean-back-App (T1) — Design

Datum: 2026-06-15 Scope: Neues tvOS-Ziel der nativen App (apple/PhysarumApp, Ziel Physarum, supportedDestinations enthält tvOS bereits). Engine unberührt (kein Shader-/Param-Layout-Eingriff; identischer Metal-Pfad). Web-App unberührt. macOS/iOS-Verhalten unverändert (alle tvOS-Pfade #if os(tvOS)-gekapselt). Status: T1 implementiert (Commits 8f38a43…95e0442, 2026-06-15) — Build/Test grün (macOS 174/0, tvOS+iOS Sim-Build, EngineCheck, Sim-Render verifiziert); Geräte-Sign-off ausstehend (s. unten). (Brainstorm + Plan 2026-06-15.) Folge-Inkrement: T2 = Phone-als-Second-Screen-Controller (eigene Spec → Plan). Diese Spec definiert die Intent-Naht, an der T2 andockt, baut T2 aber nicht.

Strategischer Kontext

Apple TV ist der Sweet-Spot der Post-v1.4-Ambiance/Mood-Richtung („build for the artifact, not income"). tvOS ist kein Port der iPad-App, sondern ein eigener Lean-back-Durchgang auf geteiltem Fundament (Metal-Engine + Model/Presets geteilt; WKWebView fällt auf tvOS weg — WebGPU ist dort kein Thema). Ergebnis dieses Inkrements: eine eigenständige, mit der Siri Remote allein voll bedienbare Ambiance-App, die ein kuratiertes Preset-Set als atmende Playlist rendert.

Befund (Codebase-Map + Recherche, 2026-06-15)

Wiederverwendung ist hoch — ~80 % ist plattform-agnostisch: - 1:1 wiederverwendbar (Metal/Foundation, kein UIKit/AppKit): gesamte PhysarumEngine (SimEngine, Renderer, ShaderLibrary, Model/*), Player/RenderCoordinator.swift (Frame-Loop via MTKViewDelegate, Thermik-/Low-Power-Drossel, 3-Buffer-Semaphore), Player/LiveSnapshot.swift, Model/AppState.swift (select/advance/reset, Undo, Custom-Presets), Model/PresetStore.swift, Model/PresetCatalog.swift, Model/ParamRegistry.swift, Model/PresetMorph.swift (morphParams), Model/RandomStrategy.swift, Audio/AudioAnalyzer.swift (+ AudioBands/AudioFactors — auf tvOS inert, keine Quelle). - Build-Stand: tvOS ist bereits deklariert — project.yml deploymentTarget.tvOS: "17.0", Target Physarum.supportedDestinations: [macOS, iOS, tvOS]; PhysarumEngine/Package.swift platforms: [… .tvOS(.v17)]. Shader transpilieren plattform-agnostisch nach MSL (apple/scripts/transpile-shaders.sh → Resources/Shaders/*.metal); Apple TV 4K unterstützt Metal voll.

Blocker, die vor tvOS-Lauf gefixt werden müssen (heute unguarded iOS/iPad-APIs): 1. Capture/CaptureSaver.swift — PHPhotoLibrary/Photos (auf tvOS nicht vorhanden) → #if os(iOS) || os(macOS). 2. Audio/AudioInput.swift — AVAudioSession-Setup #if os(iOS); tvOS hat kein Mikrofon → AudioInput auf tvOS nicht instanziieren (Stub/weglassen). 3. Model/DeviceIdiom.swift — UIDevice.current.userInterfaceIdiom liefert auf tvOS .tv (fällt heute auf .phone zurück) → .tv-Fall ergänzen. 4. App/RootView.swift, Library/LibraryView.swift — horizontalSizeClass (tvOS kennt keine Size-Classes) und PresentationDetent-Sheets (tvOS hat keine Sheets) → tvOS nutzt nicht RootView, sondern eine eigene TVRootScene. 5. App/PhysarumApp.swift — @main Scene: tvOS-Zweig auf TVRootScene (statt der macOS/iOS-RootView). 6. PhysarumAppTests.supportedDestinations enthält tvOS nicht → für die pure tvOS-Logik ergänzen (oder geteiltes Test-Target).

tvOS-Plattform-Constraints (Recherche, mit Quellen): - App-Store-Regel 2.4.3: eine tvOS-App muss allein mit der Siri Remote voll bedienbar sein. „Funktioniert nur mit gekoppeltem Phone" = Ablehnung. → Das Phone (T2) ist additiv, der Remote-Befehlssatz muss vollständig sein. (Release ist laut Cockpit aufgeschoben → jetzt Design-Prinzip, später harter Gate.) - Idle-Timer/Screensaver: ohne UIApplication.shared.isIdleTimerDisabled = true startet nach Idle der Aerial-Screensaver. Flag setzt sich auf manchen tvOS-Releases zurück → ggf. re-asserten. - Kein Background-Rendering: GPU-Arbeit nach Backgrounding schlägt fehl (kIOGPUCommandBufferCallbackErrorBackgroundExecutionNotPermitted). Render-Loop bei scenePhase != .active pausieren + in-flight Command-Buffer drainen, bei Foreground fortsetzen. - Thermik: Apple TV 4K ist lüfterlos und drosselt unter Dauerlast. Mehrstunden-Vollbild-Metal ist der Worst Case → an ProcessInfo.thermalState adaptieren (Drossel-Logik existiert im RenderCoordinator). - Kein Drittanbieter-Screensaver/Aerial-Extension-API auf tvOS → es ist eine normale Full-screen-App, die sich per Idle-Timer selbst wach hält. - T2-Vorgriff (nur Naht hier): idiomatischer Phone↔TV-Link 2026 = DeviceDiscoveryUI (DDDevicePickerViewController auf tvOS → liefert NWConnection), Fallback rohes Network.framework + Bonjour. Braucht Apple TV 4K + echtes Gerät (kein Simulator), eine Verbindung pro App. GameController/CloudKit als Live-Transport ausgeschlossen.

Entscheidungen (aus dem Brainstorm)

  1. TV autark, Phone additiv. Vollständiger Siri-Remote-Befehlssatz auf dem TV; das Phone (T2) speist später denselben Intent-Kanal als reicher Live-Controller.
  2. T1 zuerst, eigenständig. Diese Spec = nur die Lean-back-App. T2 (Phone-Link) bekommt eigene Spec → Plan. T1 definiert die abstrakte Intent-Naht, baut T2 nicht.
  3. Auto-Cycle-Playlist als Default. Langsamer kuratierter Durchlauf mit weichem morphParams-Übergang; Remote kann skippen und den Cycle pinnen (= aktuelles Preset stehen lassen).
  4. Visuell-only T1; Phone-Mic-Audio ist T2-Ziel. Audio-Reaktivität auf dem TV inert (keine Quelle). Der Intent-Kanal wird so designt, dass T2 einen leichten Audio-Bänder-Stream vom Phone andocken kann.
  5. Architektur: SwiftUI-Shell + zentrale ControlIntent-Schicht (AmbientController als einziger Sink). UIKit nur als kleiner Press-Bridge, falls SwiftUIs tvOS-Gesten-API an einer Stelle nicht reicht.

Architektur

tvOS bekommt eine eigene Szene (TVRootScene), die RootView (macOS/iOS) nie berührt. Sie hostet das full-bleed MetalView plus eine summon-bare Overlay-Ebene. Den Kern bildet ein AmbientController (@Observable/ObservableObject) mit einem Eingang apply(_ intent: ControlIntent). Jede Eingabequelle — heute die Siri Remote, in T2 die NWConnection — wird zu ControlIntent übersetzt und in diesen Sink geleitet. Diese Intent-Naht ist der einzige Integrationspunkt für T2.

Siri Remote ─▶ RemoteInput ─┐
                            ├─▶ AmbientController.apply(intent) ─▶ AppState.select / Playback-State
PlaylistDirector (tick) ────┘                                     └─▶ RenderCoordinator.switchPreset | morph
(T2: NWConnection ─▶ decode ─▶ ControlIntent ─▶ same sink)         AmbientController publiziert State ─▶ (T2: an Phone)

Neue Dateien (tvOS-only, apple/PhysarumApp/Sources/TV/)

  • TVRootScene.swift — @main-tvOS-Zweig; baut RenderCoordinator + AmbientController, hostet TVPlayerView.
  • TVPlayerView.swift — full-bleed MetalView (neuer Representable-Zweig) + TVOverlayView als bedingtes Overlay; Idle-Timer-/Lifecycle-Handling.
  • AmbientController.swift — besitzt Playback-State (currentPresetID, isCycling, laufender Morph), playlist; apply(_ intent:); publiziert State für Overlay (und T2).
  • ControlIntent.swift — enum ControlIntent { case next, previous, togglePlaylist, jumpTo(Preset.ID), showOverlay, dismissOverlay }. Erweiterbar in T2 (setParam, audioBands) — als eigener Fall, kein Umbau.
  • PlaylistDirector.swift — Auto-Cycle-Logik (pure, testbar): Dwell-Timer (~60 s, tunebar), Reihenfolge/Auswahl des nächsten Presets, Morph-Trigger. Kein UIKit.
  • ~~RemoteInput.swift~~ — umgesetzt als pure remoteMoveIntent(_:) in ControlIntent.swift (host-testbar, #if os(tvOS) || os(macOS)); die SwiftUI-Gesten-Adapter (onMoveCommand/onPlayPauseCommand/onExitCommand) leben direkt in TVPlayerView. Kein separates File nötig.
  • TVOverlayView.swift — summon-bares SwiftUI-Overlay: aktueller Preset-Name, horizontales fokussierbares Preset-Karussell (nutzt vorhandene Resources/Thumbnails) zum Direktsprung, Auto/Pin-Toggle; Auto-Dismiss nach Idle.

Angepasste Dateien

  • Player/MetalView.swift — #if os(tvOS) UIViewRepresentable-Zweig (Kernlogik makeCoordinator/sync wiederverwendet; ~20 LOC Plattform-Block).
  • Model/DeviceIdiom.swift — .tv-Fall.
  • Model/SimFormat.swift (FieldTier) — neuer .tv-Tier mit eigener Identität; FieldTier.current mappt tvOS → .tv. Startwert konservativ, final am Gerät getunt (netzbetrieben → potenziell großzügiger als Phone, aber Mehrstunden-Thermik ist der Constraint).
  • Model/PlayerPolicy.swift — tvOS-Pause-/FPS-Heuristik (Background-Pause, thermalState-Drossel; bestehende pure Fns wiederverwenden).
  • App/PhysarumApp.swift — #if os(tvOS) Scene → TVRootScene.
  • Capture/CaptureSaver.swift, Audio/AudioInput.swift — Plattform-Guards (s. Befund 1–2).
  • project.yml — ggf. tvOS-spezifische Signing-Overrides; Info-Keys: tvOS braucht kein UILaunchScreen, kein NSPhotoLibraryAddUsageDescription, kein NSMicrophoneUsageDescription.

Datenfluss im Detail

  • Render: RenderCoordinator treibt SimEngine + Renderer pro Frame in das MTKView. Full-bleed 16:9 via vorhandenem adaptivem Sim-Format (Feld nimmt Screen-Aspect an → randlos).
  • Steuerung: RemoteInput mappt Remote-Gesten → ControlIntent → AmbientController.apply. Der Controller mutiert AppState.select(...) bzw. Playback-State und ruft RenderCoordinator.switchPreset / startet einen Morph.
  • Auto-Cycle: PlaylistDirector-Tick erzeugt intern .next; der Übergang interpoliert via morphParams über ~3–5 s (kein harter Schnitt).
  • T2-Naht (nicht in T1 gebaut): eingehende NWConnection-Nachricht dekodiert zu denselben ControlIntents; AmbientController publiziert seinen State (aktuelles Preset, Cycling) zurück, damit das Phone-UI spiegelt; Phone-Mic-Bänder gehen als künftiger Intent-/Stream-Fall an RenderCoordinator.applyAudio.

Preset-Set & Übergänge

  • Playlist-Quelle: die 25 gebündelten Built-in-Presets (Resources/Presets), in kuratierter Reihenfolge. (Custom-Presets entstehen nur über den Editor, den T1 nicht hat → in T1 keine Custom-Presets; T2 kann sie später beisteuern.)
  • Übergang: morphParams-Interpolation ab-/zugehend über ~3–5 s. Dwell pro Preset ~60 s. Beide Werte als Konstanten mit klarer Benennung (am Gerät tunebar).

Ambient-Lifecycle

  • isIdleTimerDisabled = true beim Erscheinen; bei Bedarf re-asserten.
  • Render-Loop pausieren + Command-Buffer drainen bei scenePhase != .active / Background; bei Foreground fortsetzen (in-flight-Buffer vor dem Suspend abschließen).
  • preferredFramesPerSecond an ProcessInfo.thermalState koppeln (bestehende Drossel-Logik). .tv-FieldTier hält die Dauerlast konservativ.

Siri-Remote-Mapping (2.4.3-vollständig)

Eingabe Aktion
Swipe/Klick ◀ ▶ vorheriges / nächstes Preset (.previous / .next)
Play/Pause Playlist pinnen ↔ wieder atmen lassen (.togglePlaylist)
Swipe ▲ Overlay einblenden (.showOverlay)
Menu / Back Overlay schließen (.dismissOverlay); ohne Overlay → App verlassen (System)
Klick (Select) im Overlay zum fokussierten Preset springen (.jumpTo)

Standard-Gesten der 2nd-gen Siri Remote (Clickpad-Swipe + 5-Wege-Klick) funktionieren auch auf rein direktionalen Remotes; Menu muss bei offenem Overlay konsumiert werden (sonst Sprung zum Home-Screen).

Testing

  • Pure Logik auf macOS-Host (bestehendes Muster, swift test / xcodebuild test): PlaylistDirector (Auswahl/Dwell/Reihenfolge/Morph-Scheduling), RemoteInput→ControlIntent-Mapping, AmbientController-State-Transitions (next/prev/pin, jumpTo, Overlay-Toggle).
  • Engine-Rendering: identischer Metal-Pfad, kein neuer Shader → bestehende Host-GPU-Tests decken die Optik.
  • PhysarumAppTests um tvOS-Destination erweitern (für die pure tvOS-Logik).
  • Geräte-Sign-off (Johannes, ATV 4K vorhanden ✓): full-bleed-Render, vollständiges Remote-Set, Mehrstunden-Thermik, Idle-Timer-/Screensaver-Suppression, Background-Pause/Resume. T1 ist im tvOS-Simulator entwickelbar (Render + Remote); Thermik/Real-Feel und der ganze spätere T2-Link laufen auf der vorhandenen Hardware. (DeviceDiscoveryUI für T2 braucht ATV 4K, kein HD — erfüllt.)

Top Shelf / Store

  • Statisches 1920×720-Hero-Bild (gerendertes Frame) als Top-Shelf-Banner — nice-to-have, kein Gate. TVTopShelfContentProvider (Preset-Tiles mit Deep-Link) optional später.
  • Positionierung Entertainment/Lifestyle; Differenzierung gegen die verschärfte 4.2/4.3-„kein statisches Wallpaper"-Hürde = generative Echtzeit-Engine + morphende Playlist + (T2) Cross-Device-Steuerung + Audio.

Non-Goals (T1)

  • Kein Phone-Link / DeviceDiscoveryUI / Network.framework (= T2).
  • Keine Audio-Reaktivität (keine Quelle auf tvOS; kommt via Phone-Mic in T2).
  • Kein Editor, kein Capture/Export, keine Custom-Presets, kein Photos-Save auf tvOS.
  • Kein nativer Release (laut Cockpit aufgeschoben).
  • Engine/Shader/Param-Layout unverändert; Web/macOS/iOS unverändert.

Offene Punkte (am Gerät zu klären)

  • Apple TV 4K vorhanden ✓ (Gen am Gerät zu bestätigen — alle ATV-4K-Gens erfüllen die Metal-/DeviceDiscoveryUI-Anforderung; der genaue .tv-FieldTier-Wert wird ohnehin am Gerät getunt).
  • .tv-FieldTier-Werte (Feldgröße/Agent-Cap) — Startwert konservativ, final per Mehrstunden-Thermik-Test.
  • Dwell-/Morph-Dauern — Default gesetzt, am Gerät feinjustieren.
  • isIdleTimerDisabled-Reset-Verhalten auf tvOS 26 — am Gerät verifizieren (Reports spannen tvOS 11–14). Re-Assert auf scenePhase == .active ist implementiert.

Umsetzung & Geräte-Sign-off (2026-06-15)

Subagent-driven gebaut (6 Plan-Tasks, je Impl→Spec-Review→Quality-Review) + holistischer 3-Linsen-Final-Review (Korrektheit / Integration-Regress / tvOS-HIG-Lifecycle). Der HIG-Review fand einen Laufzeit-2.4.3-Fehler trotz grünem Build: das Overlay-Karussell war nicht fokussierbar (Player-Layer fing die Swipes ab) → gefixt (Player-Layer gibt Fokus frei, wenn das Overlay offen ist; Karussell bekommt Fokus via @FocusState + .focusSection(), landet auf dem aktuellen Preset). Plus: GPU-Drain vor Suspend, Idle-Timer-Re-Assert.

Verifiziert (headless): macOS 174 Tests/0 Fehler · tvOS-Sim-Build · iOS-Sim-Build · iOS-Test-Build · EngineCheck · tvOS-Sim-Launch + Full-bleed-Render-Screenshot.

Geräte-Sign-off (Johannes, ATV 4K) — noch offen, headless nicht prüfbar: - [ ] Full-bleed-Render am echten Gerät (16:9, randlos). - [ ] Auto-Cycle + Morph-Übergänge weich (kein harter Schnitt; Dwell/Morph-Feel — Defaults 60 s / 4 s, tunebar). - [ ] Siri-Remote-Set vollständig allein bedienbar: Swipe ◀▶ prev/next · Play/Pause pin/resume · Swipe ▲ Overlay · im Overlay: Karussell navigierbar + Select springt (der gefixte 2.4.3-Pfad) · Menu schließt Overlay / verlässt sonst zu Home. - [ ] Mehrstunden-Thermik (lüfterloses ATV 4K; ggf. .tv-FieldTier von 512 anheben oder FPS-Drossel justieren). - [ ] Idle-Timer/Aerial-Suppression über einen Background-Zyklus hinweg. - [ ] Background-Pause/Resume sauber (kein GPU-Background-Crash).