838a1239cf
apple / swift (push) Successful in 1m7s
release / apple (push) Successful in 8m27s
windows-host / package (push) Successful in 7m59s
apple / screenshots (push) Failing after 2m38s
android / android (push) Successful in 4m44s
ci / rust (push) Failing after 46s
arch / build-publish (push) Successful in 5m32s
ci / web (push) Successful in 50s
ci / docs-site (push) Successful in 1m2s
deb / build-publish (push) Successful in 4m44s
ci / bench (push) Successful in 5m7s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
decky / build-publish (push) Successful in 13s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 11m9s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 11m14s
docker / deploy-docs (push) Successful in 13s
The clients' codec pickers sent a preference the host threw away: host_wire_caps() hardcoded HEVC for every GPU backend, so the native path never emitted H.264/AV1 regardless of what the user chose. Host: advertise what the backend actually encodes, mirroring the GameStream serverinfo logic — software openh264 emits H.264; probed backends (Linux VAAPI, Windows AMF/QSV) advertise their per-GPU CodecSupport via the new wire_mask(); NVENC keeps the Moonlight-validated H.264|HEVC|AV1 static superset; an empty probe falls back to the superset so auto clients still resolve HEVC. Gate 10-bit to HEVC (like the 4:4:4 gate) now that a client can steer the codec — Main10 is the only 10-bit encode path. Fix the web-console stats label hardcoded to "hevc" (new Codec::label(), shared with the GameStream register_session mapping). Android: replace the hardcoded H264|HEVC Hello advertisement with a videoCodecs param fed by VideoDecoders.decodableCodecBits() (AV1 bit only when a real hardware video/av01 decoder exists — the decode loop was already mime-driven); offer AV1 in the codec picker on capable devices. Decky: add the missing "Video codec" dropdown (auto/hevc/h264/av1) to the plugin settings, round-tripping the same codec key the flatpak client reads. Apple: unchanged by design (AnnexB.swift is NAL-only, AV1 is never advertised); refresh the stale "hosts don't emit AV1 on the native path yet" comments here and host-side. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
148 lines
5.8 KiB
Swift
148 lines
5.8 KiB
Swift
// The option lists every settings surface renders from — one source of truth shared by the
|
|
// touch/desktop SettingsView (Pickers), the tvOS pushed selection rows, and the gamepad settings
|
|
// screen (GamepadSettingsView's left/right cycling). Pure data + small pure helpers; anything that
|
|
// reads live view state (e.g. the bitrate slider mapping) stays on SettingsView.
|
|
|
|
#if os(macOS)
|
|
import AppKit
|
|
#endif
|
|
import PunktfunkKit
|
|
import SwiftUI
|
|
|
|
enum SettingsOptions {
|
|
/// Compositor choices — the `tag` is the wire value (`PunktfunkConnection.Compositor` raw).
|
|
static let compositors: [(label: String, tag: Int)] = [
|
|
("Automatic", 0),
|
|
("KWin (KDE Plasma)", 1),
|
|
("wlroots (Sway / Hyprland)", 2),
|
|
("Mutter (GNOME)", 3),
|
|
("gamescope", 4),
|
|
]
|
|
|
|
static let audioChannels: [(label: String, tag: Int)] = [
|
|
("Stereo", 2),
|
|
("5.1 Surround", 6),
|
|
("7.1 Surround", 8),
|
|
]
|
|
|
|
/// Virtual-pad types — the `tag` is the wire value (`PunktfunkConnection.GamepadType` raw).
|
|
static let padTypes: [(label: String, tag: Int)] = [
|
|
("Automatic", 0),
|
|
("Xbox 360", 1),
|
|
("Xbox One", 3),
|
|
("DualSense", 2),
|
|
("DualShock 4", 4),
|
|
]
|
|
|
|
static let hudPlacements: [(label: String, tag: String)] =
|
|
HUDPlacement.allCases.map { ($0.label, $0.rawValue) }
|
|
|
|
/// Video-codec preference (`DefaultsKey.codec`) — a soft preference the host falls back from.
|
|
/// No AV1: this client's VideoToolbox path decodes H.264/HEVC only (AnnexB.swift is NAL-only),
|
|
/// so it never advertises AV1 — offering it here would be a dead setting.
|
|
static let codecs: [(label: String, tag: String)] = [
|
|
("Automatic", "auto"),
|
|
("HEVC (H.265)", "hevc"),
|
|
("H.264 (AVC)", "h264"),
|
|
]
|
|
|
|
// MARK: - Bitrate
|
|
|
|
/// Discrete bitrate steps for the surfaces with no Slider (tvOS pushed pickers, the gamepad
|
|
/// settings' left/right cycling), up to the same 3 Gbps ceiling the slider has.
|
|
static let bitratePresets: [(label: String, tag: Int)] = [
|
|
("Automatic", 0),
|
|
("10 Mbps", 10_000),
|
|
("20 Mbps", 20_000),
|
|
("40 Mbps", 40_000),
|
|
("80 Mbps", 80_000),
|
|
("150 Mbps", 150_000),
|
|
("300 Mbps", 300_000),
|
|
("500 Mbps", 500_000),
|
|
("1 Gbps", 1_000_000),
|
|
("1.5 Gbps", 1_500_000),
|
|
("2 Gbps", 2_000_000),
|
|
("3 Gbps", 3_000_000),
|
|
]
|
|
|
|
/// The presets plus the currently stored value when it isn't one of them (set via the touch
|
|
/// slider or a synced device) — so the current choice stays visible/selectable.
|
|
static func bitrateOptions(current: Int) -> [(label: String, tag: Int)] {
|
|
var options = bitratePresets
|
|
if !options.contains(where: { $0.tag == current }) {
|
|
options.insert(
|
|
(SpeedTestSheet.mbpsLabel(kbps: current) + " (custom)", current), at: 1)
|
|
}
|
|
return options
|
|
}
|
|
|
|
// MARK: - Controllers
|
|
|
|
/// "Use controller" choices: Automatic, every forwardable controller, and — so a stale pin
|
|
/// stays visible instead of leaving the selection tag-less — any pinned id that is NOT among
|
|
/// the selectable (extended) entries, present-but-unusable included.
|
|
@MainActor
|
|
static func controllerOptions(_ gamepads: GamepadManager) -> [(label: String, tag: String)] {
|
|
let selectable = gamepads.controllers.filter(\.isExtended)
|
|
var options: [(label: String, tag: String)] = [("Automatic", "")]
|
|
options += selectable.map { ($0.name, $0.id) }
|
|
if !gamepads.preferredID.isEmpty,
|
|
!selectable.contains(where: { $0.id == gamepads.preferredID }) {
|
|
options.append(("Unavailable controller", gamepads.preferredID))
|
|
}
|
|
return options
|
|
}
|
|
|
|
#if os(iOS) || os(macOS)
|
|
// MARK: - Stream mode (iOS + macOS pickers; tvOS builds its own preset list)
|
|
|
|
/// 16:9 then ultrawide presets; the device's native mode is prepended by `resolutionModes`.
|
|
static let resolutionPresets: [(name: String, w: Int, h: Int)] = [
|
|
("720p", 1280, 720),
|
|
("1080p", 1920, 1080),
|
|
("1440p", 2560, 1440),
|
|
("4K", 3840, 2160),
|
|
("Ultrawide 1080p", 2560, 1080),
|
|
("Ultrawide 1440p", 3440, 1440),
|
|
("Super ultrawide", 5120, 1440),
|
|
]
|
|
|
|
/// This device's native mode first, then the presets, deduped by dimensions (native wins a
|
|
/// tie).
|
|
@MainActor
|
|
static func resolutionModes() -> [(name: String, w: Int, h: Int)] {
|
|
var native: [(name: String, w: Int, h: Int)] = []
|
|
#if os(iOS)
|
|
let bounds = UIScreen.main.nativeBounds // portrait-oriented pixels
|
|
native = [("This device",
|
|
Int(max(bounds.width, bounds.height)),
|
|
Int(min(bounds.width, bounds.height)))]
|
|
#else
|
|
if let screen = NSScreen.main {
|
|
let scale = screen.backingScaleFactor
|
|
native = [("This display",
|
|
Int(screen.frame.width * scale),
|
|
Int(screen.frame.height * scale))]
|
|
}
|
|
#endif
|
|
var seen = Set<String>()
|
|
return (native + resolutionPresets).filter { seen.insert("\($0.w)x\($0.h)").inserted }
|
|
}
|
|
|
|
/// Refresh rates the device can actually display (no point asking the host to render frames
|
|
/// the screen can't show), plus any stored custom value so it stays selectable.
|
|
@MainActor
|
|
static func refreshRates(including current: Int) -> [Int] {
|
|
#if os(iOS)
|
|
let maxHz = UIScreen.main.maximumFramesPerSecond
|
|
#else
|
|
let maxHz = NSScreen.main?.maximumFramesPerSecond ?? 60
|
|
#endif
|
|
var rates = [60, 120, 240].filter { $0 <= maxHz }
|
|
if rates.isEmpty { rates = [maxHz] }
|
|
if !rates.contains(current) { rates.append(current) }
|
|
return rates.sorted()
|
|
}
|
|
#endif
|
|
}
|