From 8ee224e5db82bd61a75b2c89ce80c11e556bd4eb Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Mon, 3 Aug 2026 00:04:14 +0200 Subject: [PATCH] fix(android): advertise CLIENT_CAP_PAD_AUDIO, without which nothing is ever sent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A gap in the previous commits, and the same silent-failure shape as the two they fixed. There are TWO negotiations, not one: the per-pad render capabilities that ride a gamepad arrival (bits 8/9), which those commits set, and the SESSION-level CLIENT_CAP_PAD_AUDIO in the Hello, which they did not. Without the latter the host never sets HOST_CAP_PAD_AUDIO and emits no 0xD1 at all — so the per-pad bits would have had nothing to gate, and the renderer would have sat on a permanently empty plane with every other piece looking correct. Threaded as an explicit `padAudioOk` on nativeConnect rather than advertised unconditionally: the cap makes a Windows host provision pad endpoints at startup, and a user who has pad audio switched off should not pay for that. Found by tracing what an on-glass run against a real host would actually need, not by a test — there is no test that could have caught it, since both halves are individually well-formed. --- .../src/main/kotlin/io/unom/punktfunk/HostConnect.kt | 3 +++ .../kotlin/io/unom/punktfunk/kit/NativeBridge.kt | 4 ++++ clients/android/native/src/session/connect.rs | 12 +++++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) 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 100a8e27..fb5985be 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 @@ -84,6 +84,9 @@ suspend fun connectToHost( // The host's approval-list / trust-store label for this device — the same // Build.MODEL convention the pairing dialogs use for nativePair. Build.MODEL ?: "Android", + // Tier-A pad audio: ask for the 0xD1 plane only when a setting would render it, so a + // user with it off does not make the host provision endpoints it will never feed. + settings.padHaptics || settings.padSpeaker, ) } } diff --git a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/NativeBridge.kt b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/NativeBridge.kt index 909ae473..1caf8e83 100644 --- a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/NativeBridge.kt +++ b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/NativeBridge.kt @@ -69,6 +69,10 @@ object NativeBridge { * list and trust store show for it, same convention as [nativePair]'s `name`. `null`/blank ⇒ * the host falls back to a fingerprint-derived "device abcd1234" label. */ deviceName: String?, + /** Advertise `CLIENT_CAP_PAD_AUDIO` — the SESSION-level negotiation for the 0xD1 per-pad + * DualSense plane. Without it the host never sets `HOST_CAP_PAD_AUDIO` and emits nothing, + * so a captured pad's own render capabilities would have nothing to gate. */ + padAudioOk: Boolean, ): Long /** 64-hex SHA-256 of the cert the host presented on [handle]; valid after a successful connect. */ diff --git a/clients/android/native/src/session/connect.rs b/clients/android/native/src/session/connect.rs index 2b65edfb..982400f7 100644 --- a/clients/android/native/src/session/connect.rs +++ b/clients/android/native/src/session/connect.rs @@ -145,6 +145,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect<'lo timeout_ms: jint, launch: JString<'local>, device_name: JString<'local>, + pad_audio_ok: jboolean, ) -> jlong { let host: String = match env.get_string(&host) { Ok(s) => s.into(), @@ -268,7 +269,16 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect<'lo // CLIENT_CAP_PHASE_LOCK is honest: the async decode loop's presenter feeds // report_phase (advisory in v1 — the host arms on report receipt — but the Hello // should say what the client does). - punktfunk_core::quic::CLIENT_CAP_PHASE_LOCK, + // CLIENT_CAP_PAD_AUDIO is the SESSION-level negotiation, separate from the per-pad + // arrival bits: without it the host never sets HOST_CAP_PAD_AUDIO and never emits 0xD1, + // so declaring a pad's render caps later would have nothing to gate. Gated on the + // settings so a user with pad audio off does not make the host provision endpoints. + punktfunk_core::quic::CLIENT_CAP_PHASE_LOCK + | if pad_audio_ok != 0 { + punktfunk_core::quic::CLIENT_CAP_PAD_AUDIO + } else { + 0 + }, // Slice-progressive delivery, by decoder truth (Kotlin probes FEATURE_PartialFrame on // every decoder this device would use; `debug.punktfunk.force_parts` overrides for the // on-glass experiment): AU prefixes then arrive as `Frame::part` pieces and the decode