diff --git a/clients/apple/Sources/PunktfunkClient/Session/SessionModel.swift b/clients/apple/Sources/PunktfunkClient/Session/SessionModel.swift index 8f6b9a3c..9e0df31e 100644 --- a/clients/apple/Sources/PunktfunkClient/Session/SessionModel.swift +++ b/clients/apple/Sources/PunktfunkClient/Session/SessionModel.swift @@ -192,8 +192,12 @@ final class SessionModel: ObservableObject { #endif }() let hdrCapable = hdrEnabled && displayHDR - // 4:4:4 opt-out (default on); the hardware-decode probe below is the real gate. - let want444 = (UserDefaults.standard.object(forKey: DefaultsKey.enable444) as? Bool) ?? true + // 4:4:4 opt-IN (default off): full chroma is a per-client choice — a clear win for + // desktop/text work, but at a fixed bitrate it spends bits on chroma that game content + // doesn't visibly need, and the encode/decode pixel rate rises. The host allows it by + // default (PUNKTFUNK_444, default on), so this toggle is the one real switch; the + // hardware-decode probe below still gates what can actually be advertised. + let want444 = (UserDefaults.standard.object(forKey: DefaultsKey.enable444) as? Bool) ?? false Task.detached(priority: .userInitiated) { // PunktfunkConnection.init blocks on the QUIC handshake — keep it off the main // actor. The persistent identity is presented on every connect so a paired diff --git a/clients/apple/Sources/PunktfunkClient/Settings/GamepadSettingsView.swift b/clients/apple/Sources/PunktfunkClient/Settings/GamepadSettingsView.swift index 6e723de7..b7bde578 100644 --- a/clients/apple/Sources/PunktfunkClient/Settings/GamepadSettingsView.swift +++ b/clients/apple/Sources/PunktfunkClient/Settings/GamepadSettingsView.swift @@ -26,7 +26,7 @@ struct GamepadSettingsView: View { @AppStorage(DefaultsKey.bitrateKbps) private var bitrateKbps = 0 @AppStorage(DefaultsKey.audioChannels) private var audioChannels = 2 @AppStorage(DefaultsKey.hdrEnabled) private var hdrEnabled = true - @AppStorage(DefaultsKey.enable444) private var enable444 = true + @AppStorage(DefaultsKey.enable444) private var enable444 = false @AppStorage(DefaultsKey.codec) private var codec = "auto" @AppStorage(DefaultsKey.micEnabled) private var micEnabled = true // The overlay tier's raw string (rows tag by rawValue); the absent-key default runs the diff --git a/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Sections.swift b/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Sections.swift index 7008cc39..c9c9e089 100644 --- a/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Sections.swift +++ b/clients/apple/Sources/PunktfunkClient/Settings/SettingsView+Sections.swift @@ -380,8 +380,9 @@ extension SettingsView { Text("Codec is a preference; the host falls back if it can't encode your choice. " + "HDR (HDR10) and full chroma (4:4:4) are HEVC-only, and each engages only when " + "both this device and the host support it — otherwise the stream stays 8-bit " - + "4:2:0 SDR. 4:4:4 sharpens text and UI for extra bandwidth. Applies from the " - + "next session.") + + "4:2:0 SDR. 4:4:4 (off by default) sharpens text and UI — best for desktop " + + "work; for games the bits are better spent at 4:2:0. Applies from the next " + + "session.") .font(.geist(12, relativeTo: .caption)) .foregroundStyle(.secondary) } diff --git a/clients/apple/Sources/PunktfunkClient/Settings/SettingsView.swift b/clients/apple/Sources/PunktfunkClient/Settings/SettingsView.swift index 4172f0ef..8e346289 100644 --- a/clients/apple/Sources/PunktfunkClient/Settings/SettingsView.swift +++ b/clients/apple/Sources/PunktfunkClient/Settings/SettingsView.swift @@ -32,7 +32,7 @@ struct SettingsView: View { @AppStorage(DefaultsKey.allowVRR) var allowVRR = true #endif @AppStorage(DefaultsKey.hdrEnabled) var hdrEnabled = true - @AppStorage(DefaultsKey.enable444) var enable444 = true + @AppStorage(DefaultsKey.enable444) var enable444 = false @AppStorage(DefaultsKey.libraryEnabled) var libraryEnabled = true @AppStorage(DefaultsKey.fullscreenWhileStreaming) var fullscreenWhileStreaming = true @AppStorage(DefaultsKey.micEnabled) var micEnabled = true diff --git a/crates/punktfunk-core/src/quic/msgs.rs b/crates/punktfunk-core/src/quic/msgs.rs index 904c4a29..1f770ccd 100644 --- a/crates/punktfunk-core/src/quic/msgs.rs +++ b/crates/punktfunk-core/src/quic/msgs.rs @@ -80,11 +80,13 @@ pub const VIDEO_CAP_10BIT: u8 = 0x01; /// [`Hello::video_caps`] bit: the client can present BT.2020 PQ HDR10 (implies 10-bit). pub const VIDEO_CAP_HDR: u8 = 0x02; /// [`Hello::video_caps`] bit: the client can decode a full-chroma **4:4:4** HEVC stream (HEVC -/// Range Extensions / Rec.ITU-T H.265 `chroma_format_idc = 3`). The host emits 4:4:4 ONLY when this -/// bit is set, the host opted in (`PUNKTFUNK_444`), the codec is HEVC, **and** the GPU/driver -/// actually supports a 4:4:4 encode (probed) — otherwise the session stays 4:2:0 and -/// [`Welcome::chroma_format`] reflects the real resolved value. Independent of 10-bit/HDR (4:4:4 is a -/// chroma decision, bit depth is a depth decision; the two may combine where the hardware allows). +/// Range Extensions / Rec.ITU-T H.265 `chroma_format_idc = 3`) AND its user turned 4:4:4 on (a +/// client-side setting, default OFF — the per-session policy switch). The host emits 4:4:4 ONLY +/// when this bit is set, the host allows it (`PUNKTFUNK_444`, default on), the codec is HEVC, +/// **and** the GPU/driver actually supports a 4:4:4 encode (probed) — otherwise the session stays +/// 4:2:0 and [`Welcome::chroma_format`] reflects the real resolved value. Independent of +/// 10-bit/HDR (4:4:4 is a chroma decision, bit depth is a depth decision; the two may combine +/// where the hardware allows). pub const VIDEO_CAP_444: u8 = 0x04; /// [`Hello::video_caps`] bit: the client consumes per-AU host-timing datagrams /// ([`HOST_TIMING_MAGIC`], 0xCF) — the host's capture→send duration per frame, letting the client diff --git a/crates/punktfunk-host/src/config.rs b/crates/punktfunk-host/src/config.rs index 17bf511c..b6c489a2 100644 --- a/crates/punktfunk-host/src/config.rs +++ b/crates/punktfunk-host/src/config.rs @@ -51,9 +51,12 @@ pub struct HostConfig { pub zerocopy: Option, /// `PUNKTFUNK_10BIT` — host policy gate for HEVC Main10 (only honored when the client also advertised 10-bit). pub ten_bit: bool, - /// `PUNKTFUNK_444` — host policy gate for full-chroma HEVC 4:4:4 (Range Extensions). Honored only - /// when the client also advertised 4:4:4, the codec is HEVC, and the GPU/driver supports a 4:4:4 - /// encode (probed) — otherwise the session stays 4:2:0. Independent of `ten_bit` (chroma vs depth). + /// `PUNKTFUNK_444` — host policy gate for full-chroma HEVC 4:4:4 (Range Extensions). + /// **Default ON** (since the pipeline went zero-copy + honest end-to-end, 2026-07-10): the + /// host merely *allows* 4:4:4 — a session only becomes 4:4:4 when the client explicitly + /// advertised it (a client-side setting, default OFF), the codec is HEVC, the capture can + /// deliver full chroma, and the GPU/driver passed the encode probe — otherwise 4:2:0. + /// `PUNKTFUNK_444=0`/`false`/`off`/`no` disables. Independent of `ten_bit` (chroma vs depth). pub four_four_four: bool, /// `PUNKTFUNK_PERF` — per-stage timing instrumentation. pub perf: bool, @@ -102,7 +105,16 @@ impl HostConfig { ) }), ten_bit: flag("PUNKTFUNK_10BIT"), - four_four_four: flag("PUNKTFUNK_444"), + // Default ON, explicit-off grammar (the client's own 4:4:4 setting — default OFF — + // is the real switch; see the field doc). + four_four_four: val("PUNKTFUNK_444") + .map(|s| { + !matches!( + s.trim().to_ascii_lowercase().as_str(), + "0" | "false" | "off" | "no" + ) + }) + .unwrap_or(true), perf: flag("PUNKTFUNK_PERF"), video_source: val("PUNKTFUNK_VIDEO_SOURCE"), compositor: val("PUNKTFUNK_COMPOSITOR"), diff --git a/crates/punktfunk-host/src/punktfunk1.rs b/crates/punktfunk-host/src/punktfunk1.rs index defbe0ee..7a917d20 100644 --- a/crates/punktfunk-host/src/punktfunk1.rs +++ b/crates/punktfunk-host/src/punktfunk1.rs @@ -933,13 +933,15 @@ async fn serve_session( "encode bit depth" ); - // Resolve the chroma subsampling: full-chroma HEVC 4:4:4 only when ALL of — the host opted in - // (PUNKTFUNK_444), the client advertised VIDEO_CAP_444, the session is single-process (the - // two-process WGC relay encodes 4:2:0 in v1), and the active GPU/driver actually supports a - // 4:4:4 encode (probed, cached). The native path always encodes HEVC. We resolve this BEFORE - // the Welcome so `chroma_format` reflects what we'll really emit — the honest-downgrade - // channel: if any gate fails the client is told 4:2:0 before it builds its decoder. The probe - // opens a tiny encoder; it runs only when both opt-ins are set and is cached after the first. + // Resolve the chroma subsampling: full-chroma HEVC 4:4:4 only when ALL of — the host + // allows it (PUNKTFUNK_444, default ON; the CLIENT's 4:4:4 setting — default OFF — is the + // per-session policy switch behind VIDEO_CAP_444), the client advertised VIDEO_CAP_444, + // the session is single-process (the two-process WGC relay encodes 4:2:0 in v1), and the + // active GPU/driver actually supports a 4:4:4 encode (probed, cached). The native path + // always encodes HEVC. We resolve this BEFORE the Welcome so `chroma_format` reflects + // what we'll really emit — the honest-downgrade channel: if any gate fails the client is + // told 4:2:0 before it builds its decoder. The probe opens a tiny encoder; it runs only + // when the earlier gates pass and is cached after the first. let host_wants_444 = crate::config::config().four_four_four; let client_supports_444 = hello.video_caps & punktfunk_core::quic::VIDEO_CAP_444 != 0; // The active capturer must be able to deliver a full-chroma (RGB) source — the honest-downgrade