// Render-resolution scaling — the pure geometry behind `DefaultsKey.renderScale`. The client asks // the host to render/encode at `chosen resolution × scale` and lets the presenter downscale the // larger decoded frame to the display (a Catmull-Rom minification, > 1 = supersampling for // sharpness) or upscale a smaller one (< 1 = a performance mode for a weak host GPU / thin link). // // This is where the multiplier is turned into a host-valid `Mode` dimension: multiply, preserve the // aspect ratio, floor to even (the host's `validate_dimensions` rejects odd sizes), and clamp to the // codec's per-axis ceiling so the connect can't ask for something the encoder will reject. Kept // dependency-free + side-effect-free so it's unit-tested (`RenderScaleTests`) and reused by both the // fixed-mode connect and the match-window follower. import Foundation public enum RenderScale { /// The supported multiplier range. Below 1 renders under native (upscaled on present); above 1 /// supersamples. The UI clamps its slider to this and the connect clamps the raw stored value. public static let range: ClosedRange = 0.5...4.0 /// The multipliers the picker offers. 1.0 (Native) is the default; the rest are the round stops /// users reason about. public static let presets: [Double] = [0.5, 0.67, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0] /// The encoder/host per-axis ceiling for a codec preference string (`DefaultsKey.codec`). H.264 /// tops out at 4096 px/axis; HEVC / AV1 / PyroWave (and "auto", which negotiates one of those in /// practice) at 8192. The host enforces the same walls in `codec.rs::validate_dimensions`. public static func maxDimension(codec: String) -> Int { codec == "h264" ? 4096 : 8192 } /// A compact user-facing label for a multiplier: "Native (1×)", "1.5×", "2× · supersample". /// Shared by every platform's picker so the wording stays identical. public static func label(_ scale: Double) -> String { if scale == 1.0 { return "Native (1×)" } let magnitude = String(format: "%g×", scale) return scale > 1 ? "\(magnitude) · supersample" : magnitude } /// Clamp a raw stored multiplier into `range`, treating a missing/zero value as 1.0 (Native). public static func sanitize(_ raw: Double) -> Double { guard raw > 0 else { return 1.0 } return min(max(raw, range.lowerBound), range.upperBound) } /// Apply `scale` to a base pixel size, preserving aspect, even-flooring each axis, and clamping /// uniformly so neither axis exceeds `maxDimension` (the larger axis lands on the cap, the ratio /// is kept). Also floors each axis at `minWidth`/`minHeight` (the host never accepts < 320×200). /// The result is a directly host-valid `Mode` width/height. public static func apply( baseWidth: Int, baseHeight: Int, scale rawScale: Double, maxDimension: Int, minWidth: Int = 320, minHeight: Int = 200 ) -> (width: UInt32, height: UInt32) { let scale = sanitize(rawScale) var w = Double(max(baseWidth, 1)) * scale var h = Double(max(baseHeight, 1)) * scale // Uniform down-clamp if either axis blew past the ceiling — keep the aspect ratio intact. let cap = Double(maxDimension) let over = max(w / cap, h / cap) if over > 1 { w /= over h /= over } let evenFloor: (Double, Int) -> UInt32 = { value, minimum in let clamped = max(Int(value.rounded(.down)), minimum) return UInt32(clamped / 2 * 2) } return (evenFloor(w, minWidth), evenFloor(h, minHeight)) } }