feat(android): live stats HUD + low-latency decode tuning
apple / swift (push) Successful in 54s
windows-msix / package (push) Successful in 1m1s
decky / build-publish (push) Has been cancelled
deb / build-publish (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
windows / build (push) Successful in 55s
audit / cargo-audit (push) Failing after 1m8s
android / android (push) Failing after 2m12s
ci / web (push) Successful in 31s
ci / docs-site (push) Successful in 31s
ci / bench (push) Successful in 4m31s
ci / rust (push) Successful in 6m31s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 35s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m44s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m25s
flatpak / build-publish (push) Successful in 5m5s
docker / deploy-docs (push) Successful in 20s
apple / swift (push) Successful in 54s
windows-msix / package (push) Successful in 1m1s
decky / build-publish (push) Has been cancelled
deb / build-publish (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
windows / build (push) Successful in 55s
audit / cargo-audit (push) Failing after 1m8s
android / android (push) Failing after 2m12s
ci / web (push) Successful in 31s
ci / docs-site (push) Successful in 31s
ci / bench (push) Successful in 4m31s
ci / rust (push) Successful in 6m31s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 35s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 2m44s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m25s
flatpak / build-publish (push) Successful in 5m5s
docker / deploy-docs (push) Successful in 20s
Stats HUD (mirrors the Apple client): the decode thread accumulates FPS, receive throughput, and capture->client latency (p50/p95, skew-corrected) in Rust (clients/android/native/src/stats.rs); nativeVideoStats drains a snapshot ~1 Hz over JNI as a DoubleArray. StreamScreen renders a Compose overlay (W*H@Hz / fps / Mb/s / latency, + dropped-under-loss), toggled by a Settings switch (persisted, default on) or a 3-finger tap. Performance (decode.rs): - ANativeWindow_setFrameRate(refresh_hz): align display vsync to the stream rate (no 60-in-120 judder); safe since minSdk 31 >= API 30. - Raise the decode thread toward URGENT_DISPLAY (best-effort setpriority) so background work can't preempt it under load. - Codec low-latency hints KEY_PRIORITY=0 (realtime) + KEY_OPERATING_RATE. Verified host-side: cargo build/clippy/fmt --workspace (the ungated stats + JNI accessor). The android-gated decode.rs (NDK) and the Kotlin build only in CI (android.yml: gradle + cargo-ndk) -- APIs verified against crate sources. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
//! renegotiation. Port the remaining orchestration from `clients/linux`.
|
||||
|
||||
use jni::objects::{JObject, JString};
|
||||
use jni::sys::{jboolean, jint, jlong};
|
||||
use jni::sys::{jboolean, jdoubleArray, jint, jlong, jsize};
|
||||
use jni::JNIEnv;
|
||||
use punktfunk_core::client::NativeClient;
|
||||
use punktfunk_core::config::{CompositorPref, GamepadPref, Mode};
|
||||
@@ -40,6 +40,8 @@ pub(crate) struct SessionHandle {
|
||||
struct VideoThread {
|
||||
shutdown: Arc<AtomicBool>,
|
||||
join: Option<JoinHandle<()>>,
|
||||
/// Live decode stats, written by the decode thread and drained ~1 Hz by `nativeVideoStats`.
|
||||
stats: Arc<crate::stats::VideoStats>,
|
||||
}
|
||||
|
||||
impl SessionHandle {
|
||||
@@ -331,13 +333,19 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStartVideo(
|
||||
}
|
||||
};
|
||||
let shutdown = Arc::new(AtomicBool::new(false));
|
||||
let stats = Arc::new(crate::stats::VideoStats::new());
|
||||
let client = h.client.clone();
|
||||
let sd = shutdown.clone();
|
||||
let st = stats.clone();
|
||||
let join = std::thread::Builder::new()
|
||||
.name("pf-decode".into())
|
||||
.spawn(move || crate::decode::run(client, window, sd))
|
||||
.spawn(move || crate::decode::run(client, window, sd, st))
|
||||
.ok();
|
||||
*guard = Some(VideoThread { shutdown, join });
|
||||
*guard = Some(VideoThread {
|
||||
shutdown,
|
||||
join,
|
||||
stats,
|
||||
});
|
||||
}
|
||||
|
||||
/// `NativeBridge.nativeStopVideo(handle)` — stop + join the decode thread (without closing the
|
||||
@@ -355,6 +363,50 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStopVideo(
|
||||
}
|
||||
}
|
||||
|
||||
/// `NativeBridge.nativeVideoStats(handle): DoubleArray?` — drain ~1 s of decode stats for the HUD.
|
||||
/// Returns 10 doubles
|
||||
/// `[fps, mbps, latP50Ms, latP95Ms, latValid, skewCorrected, width, height, refreshHz, framesDropped]`
|
||||
/// (the two flags are 1.0/0.0), or `null` when no decode thread is running. Poll ~1 Hz from the UI;
|
||||
/// each call resets the measurement window. Not android-gated — pure `jni` + connector reads, so it
|
||||
/// links on the host build too (Kotlin only ever calls it on device).
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeVideoStats(
|
||||
env: JNIEnv,
|
||||
_this: JObject,
|
||||
handle: jlong,
|
||||
) -> jdoubleArray {
|
||||
if handle == 0 {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
// SAFETY: live handle per the nativeConnect/nativeClose contract.
|
||||
let h = unsafe { &*(handle as *const SessionHandle) };
|
||||
let snap = match h.video.lock().unwrap().as_ref() {
|
||||
Some(vt) => vt.stats.drain(),
|
||||
None => return std::ptr::null_mut(), // not streaming → no stats
|
||||
};
|
||||
let mode = h.client.mode();
|
||||
let buf: [f64; 10] = [
|
||||
snap.fps,
|
||||
snap.mbps,
|
||||
snap.lat_p50_ms,
|
||||
snap.lat_p95_ms,
|
||||
if snap.lat_valid { 1.0 } else { 0.0 },
|
||||
if snap.skew_corrected { 1.0 } else { 0.0 },
|
||||
mode.width as f64,
|
||||
mode.height as f64,
|
||||
mode.refresh_hz as f64,
|
||||
h.client.frames_dropped() as f64,
|
||||
];
|
||||
let arr = match env.new_double_array(buf.len() as jsize) {
|
||||
Ok(a) => a,
|
||||
Err(_) => return std::ptr::null_mut(),
|
||||
};
|
||||
if env.set_double_array_region(&arr, 0, &buf).is_err() {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
arr.into_raw()
|
||||
}
|
||||
|
||||
/// `NativeBridge.nativeStartAudio(handle)` — start the Opus→AAudio playback thread. No-op if already
|
||||
/// started or on a `0` handle. Best-effort: a failure leaves video streaming.
|
||||
#[cfg(target_os = "android")]
|
||||
|
||||
Reference in New Issue
Block a user