feat(android): Automatic prefers AV1 where the silicon says it should

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 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 00:40:58 +02:00
co-authored by Claude Fable 5
parent b69ef02f4d
commit f9c56eaf5c
2 changed files with 28 additions and 5 deletions
@@ -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.
@@ -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