89ff326ebf
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>
44 lines
2.0 KiB
Swift
44 lines
2.0 KiB
Swift
// The Match-window trigger discipline (design/midstream-resolution-resize.md D2), as pure
|
||
// functions — the same rules the session binary's `resize_decision` unit-tests: physical pixels
|
||
// even-floored and clamped ≥ 320×200, skip a size equal to the live mode, and request each
|
||
// distinct size at most once (so a rejected size / a host rollback can't loop).
|
||
|
||
import XCTest
|
||
|
||
@testable import PunktfunkKit
|
||
|
||
final class MatchWindowTests: XCTestCase {
|
||
func testNormalizeEvenFloorsAndClamps() {
|
||
// Odd pixels floor to even (the host rejects odd dimensions).
|
||
let a = MatchWindow.normalize(widthPx: 1001, heightPx: 601)
|
||
XCTAssertEqual(a.width, 1000)
|
||
XCTAssertEqual(a.height, 600)
|
||
// Already-even sizes pass through.
|
||
let b = MatchWindow.normalize(widthPx: 2560, heightPx: 1440)
|
||
XCTAssertEqual(b.width, 2560)
|
||
XCTAssertEqual(b.height, 1440)
|
||
// Tiny / zero clamp to the host floor.
|
||
let c = MatchWindow.normalize(widthPx: 100, heightPx: 80)
|
||
XCTAssertEqual(c.width, 320)
|
||
XCTAssertEqual(c.height, 200)
|
||
let z = MatchWindow.normalize(widthPx: 0, heightPx: -4)
|
||
XCTAssertEqual(z.width, 320)
|
||
XCTAssertEqual(z.height, 200)
|
||
}
|
||
|
||
func testRequestSkipsEqualAndAlreadyRequested() {
|
||
// A new size (different from the live mode, not yet requested) → request it.
|
||
let r = MatchWindow.request(
|
||
target: (1000, 600), current: (1280, 720), lastRequested: (800, 500))
|
||
XCTAssertEqual(r?.width, 1000)
|
||
XCTAssertEqual(r?.height, 600)
|
||
// Equal to the live mode → nothing to do.
|
||
XCTAssertNil(MatchWindow.request(
|
||
target: (1280, 720), current: (1280, 720), lastRequested: nil))
|
||
// Already requested once → don't re-ask (covers a rejected size AND a host rollback:
|
||
// accepted → rebuild failed → corrective ack restored the old mode must not loop).
|
||
XCTAssertNil(MatchWindow.request(
|
||
target: (1000, 600), current: (1280, 720), lastRequested: (1000, 600)))
|
||
}
|
||
}
|