feat(android): the client advertises multi-slice tolerance per-decoder

The P0 gating branch deliberately left Android off VIDEO_CAP_MULTI_SLICE
("embedder-set decoder truth") — this is that deferred item, and the P2
slice-pipeline prerequisite. VideoDecoders.multiSliceTolerant() probes the
pick for every advertised codec: tolerant unless any pick is an Amlogic
decoder (the 0.17.0 device-rebooting wedge) or uninspectable (a null pick =
platform default → conservatively single-slice). nativeConnect carries the
bit; the JNI ORs it into the Hello's video_caps beside the panel-truth
HDR bits. Phones (Qualcomm/Exynos) advertise it; the Amlogic TV boxes the
gate exists for never will.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 14:14:46 +02:00
co-authored by Claude Fable 5
parent 19392918ff
commit 43868af1f5
4 changed files with 38 additions and 3 deletions
@@ -49,7 +49,7 @@ suspend fun connectToHost(
host, port, w, h, hz,
identity.certPem, identity.privateKeyPem, pinHex,
settings.bitrateKbps, settings.compositor, gamepadPref,
hdrEnabled, settings.audioChannels,
hdrEnabled, VideoDecoders.multiSliceTolerant(), 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,
@@ -47,6 +47,10 @@ object NativeBridge {
compositorPref: Int,
gamepadPref: Int,
hdrEnabled: Boolean,
/** Every decoder this device would use tolerates multi-slice AUs
* ([VideoDecoders.multiSliceTolerant]) — advertises `VIDEO_CAP_MULTI_SLICE`; false keeps
* the host at single-slice frames (the safe pre-0.17 wire shape). */
multiSliceOk: Boolean,
audioChannels: Int,
/** `quic::CODEC_*` bitfield of codecs this device decodes ([VideoDecoders.decodableCodecBits]);
* `0` falls back to H.264|HEVC. The host resolves the emitted codec from this ∩ its GPU. */
@@ -53,6 +53,28 @@ object VideoDecoders {
fun decodableCodecBits(): Int =
1 or 2 or (if (pickDecoder("video/av01") != null) 4 else 0)
/**
* Whether EVERY decoder this device would use tolerates multi-slice access units — the
* `VIDEO_CAP_MULTI_SLICE` advertisement (decoder truth, per the 0.17.0 field regression:
* Amlogic HEVC decoders wedge the whole DEVICE on multi-slice frames — Chromecast with
* Google TV, onn 4K — which is why Moonlight requests single-slice for every hardware
* decoder). We advertise per-family instead: tolerant unless the pick for ANY advertised
* codec is an Amlogic decoder or can't be inspected (a null pick = the platform default —
* uninspectable, so conservatively single-slice). Probed once at connect time; the host
* defaults to >1 slice only toward clients that set the bit.
*/
fun multiSliceTolerant(): Boolean {
val mimes = buildList {
add("video/avc")
add("video/hevc")
if (decodableCodecBits() and 4 != 0) add("video/av01")
}
return mimes.all { mime ->
val name = pickDecoder(mime)?.name?.lowercase() ?: return@all false
!name.startsWith("omx.amlogic") && !name.startsWith("c2.amlogic")
}
}
fun pickDecoder(mime: String): DecoderChoice? {
if (mime.isEmpty()) return null
val infos = runCatching { MediaCodecList(MediaCodecList.REGULAR_CODECS).codecInfos }
+11 -2
View File
@@ -115,6 +115,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect<'lo
compositor_pref: jint,
gamepad_pref: jint,
hdr_enabled: jboolean,
multi_slice_ok: jboolean,
audio_channels: jint,
video_codecs: jint,
preferred_codec: jint,
@@ -182,11 +183,19 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect<'lo
// sends a proper 8-bit BT.709 stream rather than PQ the panel would mis-tone-map. AMediaCodec
// decodes Main10 from the SPS and the decode loop signals the Surface HDR dataspace + static
// metadata (see crate::decode).
if hdr_enabled != 0 {
// 10-bit/HDR by panel truth (above) + multi-slice by DECODER truth: Kotlin probes every
// decoder this device would use (`VideoDecoders.multiSliceTolerant` — Amlogic wedges the
// whole device on multi-slice AUs, the 0.17.0 field regression) and only then may the
// host default to >1 slice per frame (its sub-frame readback / the P2 slice pipeline).
(if hdr_enabled != 0 {
punktfunk_core::quic::VIDEO_CAP_10BIT | punktfunk_core::quic::VIDEO_CAP_HDR
} else {
0
},
}) | (if multi_slice_ok != 0 {
punktfunk_core::quic::VIDEO_CAP_MULTI_SLICE
} else {
0
}),
// Requested surround layout (2 = stereo / 6 = 5.1 / 8 = 7.1). The host clamps to what it can
// capture and echoes the resolved count in `connector.audio_channels`, which drives the
// decoder + AAudio layout (read in `crate::audio::AudioPlayback::start`). Anything else