20f0d2802f
Three levers to lower and steady decode latency on Snapdragon (Adreno) devices:
- ADPF (Adaptive Performance Framework): a new dlsym-resolved hint session
(native/src/adpf.rs; API-33+, resolved at runtime so there's no build-time
link dependency and libpunktfunk_android.so still loads on API 31/32) tells
the CPU governor the video pipeline runs a per-frame real-time workload, so it
keeps those threads on fast cores at high clocks. It now covers all three
latency-critical threads — the pf-decode feed/drain/present loop, the core
data-plane pump (UDP receive + FEC reassembly), and the audio thread — via a
new generic hot-thread registry on NativeClient (register_hot_thread /
hot_thread_ids; the pump self-registers). The session is built lazily on the
first presented frame, since ADPF createSession rejects a set containing any
not-yet-live tid.
- operating-rate -> Short.MAX ("as fast as possible"): pushes the Qualcomm
decoder to run each frame at max clocks instead of merely sustaining the
display rate at a power-saving clock that adds per-frame decode latency.
- appCategory="game": makes the app eligible for OEM Game Mode / Game Dashboard
performance profiles.
The core registry is cross-platform (gettid on Linux/Android, a no-op
elsewhere) — no Android-specific pollution of the shared core. Host workspace +
64 core tests green; Android arm64-v8a + x86_64 (platform 31) build + clippy
clean. On-device Snapdragon validation pending.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
138 lines
6.7 KiB
Rust
138 lines
6.7 KiB
Rust
//! Android Adaptive Performance Framework (ADPF) — CPU performance hints for the decode thread.
|
|
//!
|
|
//! ADPF lets a latency-critical app tell the platform "these threads run a repeating workload with
|
|
//! this per-cycle deadline, and here's how long they *actually* took." The kernel's CPU governor
|
|
//! (on Qualcomm Snapdragon in particular — its ADPF backend is among the most responsive) then keeps
|
|
//! those threads on the fast cores at high clocks instead of migrating them to a little core or
|
|
//! down-clocking between frames. For a stream client the win is on the in-process hot path we
|
|
//! control — the `pf-decode` feed/drain/present loop — *not* the hardware codec itself (that decodes
|
|
//! in the mediacodec service, a separate process we can't hint); keeping our loop from being
|
|
//! scheduled late directly trims the jitter between "AU received" and "buffer released to the
|
|
//! Surface." It complements the codec-side `operating-rate`/`priority` hints, which push the codec's
|
|
//! own clocks.
|
|
//!
|
|
//! The `APerformanceHint_*` API arrived in NDK **API level 33**. minSdk is 31, so we CANNOT link the
|
|
//! symbols directly: a `libpunktfunk_android.so` carrying an unresolved
|
|
//! `APerformanceHint_createSession` import fails to load on API 31/32 devices
|
|
//! (`System.loadLibrary` throws) even if the code path is never taken. Instead we resolve the
|
|
//! entry points from `libandroid.so` with `dlsym` at runtime — absent on < 33 ⇒
|
|
//! [`HintSession::create`] returns `None` and the decode loop simply runs without hints.
|
|
|
|
use std::ffi::c_void;
|
|
use std::os::raw::c_int;
|
|
|
|
// `APerformanceHint_*` function-pointer types. The manager/session handles are opaque, so we treat
|
|
// them as `*mut c_void`.
|
|
type GetManagerFn = unsafe extern "C" fn() -> *mut c_void;
|
|
type CreateSessionFn = unsafe extern "C" fn(*mut c_void, *const i32, usize, i64) -> *mut c_void;
|
|
type ReportFn = unsafe extern "C" fn(*mut c_void, i64) -> c_int;
|
|
type UpdateTargetFn = unsafe extern "C" fn(*mut c_void, i64) -> c_int;
|
|
type CloseFn = unsafe extern "C" fn(*mut c_void);
|
|
|
|
/// The entry points we use, resolved once from `libandroid.so`, plus the process-wide manager.
|
|
struct Api {
|
|
create_session: CreateSessionFn,
|
|
report: ReportFn,
|
|
update_target: UpdateTargetFn,
|
|
close: CloseFn,
|
|
manager: *mut c_void,
|
|
}
|
|
|
|
/// Resolve the ADPF entry points + the process manager, or `None` on API < 33 (symbols absent) or if
|
|
/// the manager is unavailable.
|
|
fn resolve_api() -> Option<Api> {
|
|
// SAFETY: `dlopen` of an always-present system library with a NUL-terminated name; it returns
|
|
// null on failure (checked below). `libandroid.so` is already mapped into every app process, so
|
|
// this only bumps its refcount — we intentionally never `dlclose` (process-lifetime handle).
|
|
let lib = unsafe { libc::dlopen(c"libandroid.so".as_ptr(), libc::RTLD_NOW) };
|
|
if lib.is_null() {
|
|
return None;
|
|
}
|
|
// SAFETY: `dlsym` on the valid handle above with NUL-terminated symbol names; each returns null
|
|
// when the symbol is absent (device API < 33), which we check before transmuting the non-null
|
|
// pointer to its fn-pointer type (layout-compatible; a resolved symbol is a valid code address).
|
|
unsafe {
|
|
let get_manager = libc::dlsym(lib, c"APerformanceHint_getManager".as_ptr());
|
|
let create_session = libc::dlsym(lib, c"APerformanceHint_createSession".as_ptr());
|
|
let report = libc::dlsym(lib, c"APerformanceHint_reportActualWorkDuration".as_ptr());
|
|
let update_target = libc::dlsym(lib, c"APerformanceHint_updateTargetWorkDuration".as_ptr());
|
|
let close = libc::dlsym(lib, c"APerformanceHint_closeSession".as_ptr());
|
|
if get_manager.is_null()
|
|
|| create_session.is_null()
|
|
|| report.is_null()
|
|
|| update_target.is_null()
|
|
|| close.is_null()
|
|
{
|
|
return None; // device API < 33 — no ADPF
|
|
}
|
|
let get_manager = std::mem::transmute::<*mut c_void, GetManagerFn>(get_manager);
|
|
let manager = get_manager();
|
|
if manager.is_null() {
|
|
return None;
|
|
}
|
|
Some(Api {
|
|
create_session: std::mem::transmute::<*mut c_void, CreateSessionFn>(create_session),
|
|
report: std::mem::transmute::<*mut c_void, ReportFn>(report),
|
|
update_target: std::mem::transmute::<*mut c_void, UpdateTargetFn>(update_target),
|
|
close: std::mem::transmute::<*mut c_void, CloseFn>(close),
|
|
manager,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// A live ADPF hint session bound to a set of thread ids. Dropping it closes the session. Holds raw
|
|
/// handles, so it is `!Send`/`!Sync` — created and used only on the `pf-decode` thread.
|
|
pub struct HintSession {
|
|
api: Api,
|
|
session: *mut c_void,
|
|
}
|
|
|
|
impl HintSession {
|
|
/// Open a session hinting `tids` with an initial per-frame target of `target_ns` nanoseconds.
|
|
/// `None` when ADPF is unavailable (device API < 33) or the platform declines — the caller then
|
|
/// runs unhinted (a no-op, not an error).
|
|
pub fn create(target_ns: i64, tids: &[i32]) -> Option<Self> {
|
|
if target_ns <= 0 || tids.is_empty() {
|
|
return None;
|
|
}
|
|
let api = resolve_api()?;
|
|
// SAFETY: `api.manager` is the live process manager returned above; `tids` is a valid slice
|
|
// of `len` i32s that `createSession` copies; it returns null on failure (checked).
|
|
let session =
|
|
unsafe { (api.create_session)(api.manager, tids.as_ptr(), tids.len(), target_ns) };
|
|
if session.is_null() {
|
|
return None;
|
|
}
|
|
Some(Self { api, session })
|
|
}
|
|
|
|
/// Report the wall-clock time the hinted thread spent producing the last displayed frame. When
|
|
/// it exceeds the session target the governor boosts the cores running the thread; when it
|
|
/// stays under, clocks may relax. No-op on a non-positive duration (the API rejects it).
|
|
pub fn report_actual(&self, actual_ns: i64) {
|
|
if actual_ns <= 0 {
|
|
return;
|
|
}
|
|
// SAFETY: `self.session` is a live session for `self`'s lifetime.
|
|
unsafe { (self.api.report)(self.session, actual_ns) };
|
|
}
|
|
|
|
/// Update the per-frame target (e.g. after a mid-session refresh-rate change). Unused today —
|
|
/// the decode thread restarts on renegotiation — but kept for that path.
|
|
#[allow(dead_code)]
|
|
pub fn update_target(&self, target_ns: i64) {
|
|
if target_ns <= 0 {
|
|
return;
|
|
}
|
|
// SAFETY: `self.session` is a live session for `self`'s lifetime.
|
|
unsafe { (self.api.update_target)(self.session, target_ns) };
|
|
}
|
|
}
|
|
|
|
impl Drop for HintSession {
|
|
fn drop(&mut self) {
|
|
// SAFETY: `self.session` was created by `createSession` and is closed exactly once, here.
|
|
unsafe { (self.api.close)(self.session) };
|
|
}
|
|
}
|