feat(resize/apple): Match-window mid-stream resize trigger + settings (C3)

design/midstream-resolution-resize.md Phase 2, Apple client. The stream mode
follows the session window/scene: a windowed macOS window resize or an iPad
Stage Manager / Split View scene change renegotiates the host's virtual
display + encoder via the existing PunktfunkConnection.requestMode, so a
windowed session streams native-resolution pixels instead of scaling.
Decode/present need nothing — VideoToolbox recreates its session on the
keyframe-derived format-description change (§1 table).

- MatchWindowFollower (new): the shared D2 trigger discipline — physical
  pixels even-floored + clamped ≥320×200, debounce to resize-end, ≥1 s
  between requests, skip a size equal to the live mode, request each distinct
  size at most once (stops re-asking a rejected size / looping on a host
  rollback). Pure normalize/request core is unit-tested (MatchWindowTests).
- macOS StreamLayerView: fed from layoutPresenter() (bounds → convertToBacking),
  guarded to once-in-a-window.
- iOS StreamViewController: fed from viewDidLayoutSubviews (bounds × render
  scale); iOS-only (iPhone fullscreen no-ops, tvOS uses AVDisplayManager).
- Settings: "Match window" toggle in the Stream mode section (iOS + macOS),
  DefaultsKey.matchWindow, read per session by the follower.

Verified on a Linux Swift 6.1.2 toolchain (the app target needs AppKit/UIKit,
unavailable here): the real MatchWindowFollower.swift type-checks in Swift 5
mode against a connection stub, and the pure discipline + the follower's
decision path pass a standalone harness (drag-settle + grow → exactly two
switches, refresh preserved, no re-request loop). A full build + on-device run
(macOS window, iPad Stage Manager) remains for a Mac.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-11 13:36:03 +02:00
parent d0d9bd5bfb
commit 89ff326ebf
7 changed files with 228 additions and 3 deletions
@@ -11,6 +11,15 @@ public enum DefaultsKey {
public static let streamWidth = "punktfunk.width"
public static let streamHeight = "punktfunk.height"
public static let streamHz = "punktfunk.hz"
/// Match-window resolution policy (design/midstream-resolution-resize.md D1/D2): when on, the
/// stream mode FOLLOWS the session view the connect asks for the view's pixel size and a
/// mid-session resize (a windowed macOS window, an iPad Stage Manager / Split View scene)
/// renegotiates the host's virtual display + encoder (`PunktfunkConnection.requestMode`), so a
/// windowed session streams native-resolution pixels instead of scaling. Off (default): the
/// explicit `streamWidth`/`streamHeight` are used and never auto-resized (a fullscreen session
/// is native either way, so this degenerates to Auto-native there). Read per session by the
/// stream views' `MatchWindowFollower`.
public static let matchWindow = "punktfunk.matchWindow"
public static let compositor = "punktfunk.compositor"
public static let gamepadType = "punktfunk.gamepadType"
public static let gamepadID = "punktfunk.gamepadID"