From f9c56eaf5cfb89c279253008ab83e09ee5dc8de2 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 31 Jul 2026 19:33:40 +0200 Subject: [PATCH] feat(android): Automatic prefers AV1 where the silicon says it should MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The P3 format A/B (NP3 ↔ RTX 4090, identical conditions) measured AV1 ~1.2 ms faster end-to-end than HEVC with slightly better codec-pure decode time. Under "Automatic" the client now sends AV1 as its soft preference when this device hardware-decodes it (the advertised AV1 bit is already gated on a real, non-blocked hardware decoder) AND it lacks FEATURE_PartialFrame — a partial-frame device keeps HEVC, whose slice-progressive overlap AV1 cannot ride (no slices, the chunked poll never arms). The host honors the preference only inside its probed shared codec set, so an AV1-less encoder still resolves HEVC, and an explicit user choice wins unchanged. The codec picker caption mirrors the same rule so "Automatic" says what it does on this device. Co-Authored-By: Claude Fable 5 --- .../kotlin/io/unom/punktfunk/HostConnect.kt | 21 +++++++++++++++---- .../io/unom/punktfunk/SettingsScreen.kt | 12 ++++++++++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/HostConnect.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/HostConnect.kt index 37ed8a26..100a8e27 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/HostConnect.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/HostConnect.kt @@ -47,15 +47,27 @@ suspend fun connectToHost( // sockets) — must be applied before connect, since sockets are tagged at creation. NativeBridge.nativeSetLowLatencyMode(settings.lowLatencyMode) val multiSlice = VideoDecoders.multiSliceTolerant() + val partialFrame = VideoDecoders.partialFrameCapable() // Slice-progressive delivery: decoder truth AND the async decode loop — the legacy // sync loop feeds whole AUs only, so parts must never arrive when it is selected. - val frameParts = settings.lowLatencyMode && VideoDecoders.partialFrameCapable() + val frameParts = settings.lowLatencyMode && partialFrame + val codecBits = VideoDecoders.decodableCodecBits() + // Automatic codec (P5, measured NP3 ↔ RTX 4090): AV1 beat HEVC by ~1.2 ms end-to-end at + // identical conditions, so under "Automatic" this device prefers AV1 when it hardware- + // decodes it (the AV1 bit is only ever set for a real, non-blocked hardware decoder) AND + // it lacks FEATURE_PartialFrame — a partial-frame device keeps HEVC, whose slice overlap + // AV1 cannot ride (AV1 has no slices; the host's chunked poll never arms). The host + // honors the preference only inside the probed shared codec set, so an AV1-less encoder + // still resolves HEVC. An explicit user choice always wins unchanged. + val preferredCodec = settings.preferredCodec().takeIf { it != 0 } + ?: if (codecBits and 4 != 0 && !partialFrame) 4 else 0 // The connect-time capability readout (`adb logcat -s pf.caps`): the P2 slice pipeline // is client-inert unless BOTH probes pass — this line says which decoder failed one. Log.i( "pf.caps", VideoDecoders.capsReport() + - " → multiSlice=$multiSlice parts=$frameParts (lowLatency=${settings.lowLatencyMode})", + " → multiSlice=$multiSlice parts=$frameParts prefer=$preferredCodec" + + " (lowLatency=${settings.lowLatencyMode})", ) NativeBridge.nativeConnect( host, port, w, h, hz, @@ -65,8 +77,9 @@ suspend fun connectToHost( frameParts, settings.audioChannels, // What this device can decode (H.264|HEVC always, AV1 when a real decoder exists) + - // the user's soft codec preference — the host resolves the emitted codec from both. - VideoDecoders.decodableCodecBits(), settings.preferredCodec(), timeoutMs, + // the soft codec preference (user choice, or the Automatic AV1 rule above) — the + // host resolves the emitted codec from both. + codecBits, preferredCodec, timeoutMs, launch, // The host's approval-list / trust-store label for this device — the same // Build.MODEL convention the pairing dialogs use for nativePair. diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/SettingsScreen.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/SettingsScreen.kt index 20ae8660..7ae7c11c 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/SettingsScreen.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/SettingsScreen.kt @@ -673,12 +673,22 @@ private fun DisplaySettings(s: Settings, update: (Settings) -> Unit, context: an // Only codecs this device can actually decode are offered — a preference the client never // advertises would be a dead setting (see [codecOptionsFor]). val av1Capable = remember { VideoDecoders.pickDecoder("video/av01") != null } + // Mirror the Automatic AV1 rule in HostConnect (hardware AV1 AND no partial-frame + // support) so the picker says what "Automatic" actually does on THIS device. + val autoPrefersAv1 = remember { + VideoDecoders.decodableCodecBits() and 4 != 0 && !VideoDecoders.partialFrameCapable() + } SettingDropdown( label = "Video codec", options = codecOptionsFor(s.codec, av1Capable), selected = s.codec, field = "codec", - caption = "A preference — the host falls back if it can't encode this one.", + caption = if (autoPrefersAv1) { + "A preference — the host falls back if it can't encode this one. " + + "Automatic prefers AV1 on this device." + } else { + "A preference — the host falls back if it can't encode this one." + }, ) { c -> update(s.copy(codec = c)) } // HDR is only meaningful on a panel that can present HDR10; on an SDR display the toggle is