6c976e9dc5
apple / swift (push) Successful in 1m14s
ci / web (push) Successful in 1m42s
ci / docs-site (push) Successful in 2m0s
release / apple (push) Successful in 5m32s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
apple / screenshots (push) Successful in 6m33s
ci / bench (push) Successful in 8m15s
decky / build-publish (push) Successful in 29s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 54s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
android / android (push) Successful in 14m14s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
arch / build-publish (push) Successful in 18m11s
deb / build-publish (push) Successful in 18m29s
docker / deploy-docs (push) Successful in 28s
ci / rust (push) Successful in 28m37s
The W7/W8 client reconciliation committed the CONSUMERS of the render-scale setting and the toggle-fullscreen notification (ContentView/FullscreenController et al.) while their definitions were still uncommitted working-tree state from the Apple-features effort — apple.yml red on main (run 10657). Lands the two definition files (PunktfunkShared/RenderScale.swift + the DefaultsKeys additions) so main builds; the rest of that effort's WIP stays in its working tree. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
73 lines
3.6 KiB
Swift
73 lines
3.6 KiB
Swift
// 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<Double> = 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))
|
||
}
|
||
}
|