feat(clients/android): OnFrameRendered display stage — HUD headline becomes capture→displayed
The long-deferred Android display stage (design/stats-unification.md; plan 4.1 of design/client-parity-and-network-resilience.md): AMediaCodec_setOnFrameRenderedCallback (API 26, under the minSdk-28 floor ⇒ hard-linked via ndk-sys) reports SurfaceFlinger's per-frame render timestamp, giving the HUD the spec's `display` = decoded→displayed term and the directly-measured capture→displayed end-to-end headline on both decode loops. Falls back per spec to the v1 capture→decoded endpoint on any window without render callbacks (the platform may drop them under load), and to it permanently if registration is refused. - The render timestamp arrives on CLOCK_MONOTONIC; it's re-based onto CLOCK_REALTIME against monotonic-now at callback time, which also cancels the (batchable) callback delivery lag. - The `ndk` crate exposes neither the callback nor the codec pointer needed to bind it raw, so the workspace pins `ndk` 0.9.0 to a vendored copy (clients/android/native/ vendor/ndk) whose ONLY change makes MediaCodec::as_ptr public — the "as_ptr patch". Workspace-excluded so host builds never compile it; drop when upstream exposes either. - nativeVideoStats grows to 26 doubles (22–25: dispValid, displayP50, e2eDispP50/P95; 0–21 unchanged for older readers); StatsOverlay moves headline endpoint + equation together so the equation always tiles the headline interval. Verified: host cargo check/test/clippy, aarch64-linux-android check/clippy, Kotlin app+kit+tests compile, roborazzi HUD render shows the full 4-term equation. Device verification rides plan 4.2's phone A/B. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
//! Bindings for [`AInputQueue`]
|
||||
//!
|
||||
//! [`AInputQueue`]: https://developer.android.com/ndk/reference/group/input#ainputqueue
|
||||
|
||||
use std::io::Result;
|
||||
use std::os::raw::c_int;
|
||||
use std::ptr::{self, NonNull};
|
||||
|
||||
#[cfg(feature = "api-level-33")]
|
||||
use jni_sys::{jobject, JNIEnv};
|
||||
|
||||
use crate::event::InputEvent;
|
||||
#[cfg(doc)]
|
||||
use crate::event::KeyEvent;
|
||||
use crate::looper::ForeignLooper;
|
||||
use crate::utils::status_to_io_result;
|
||||
|
||||
/// A native [`AInputQueue *`]
|
||||
///
|
||||
/// An input queue is the facility through which you retrieve input events.
|
||||
///
|
||||
/// [`AInputQueue *`]: https://developer.android.com/ndk/reference/group/input#ainputqueue
|
||||
#[derive(Debug)]
|
||||
pub struct InputQueue {
|
||||
ptr: NonNull<ffi::AInputQueue>,
|
||||
}
|
||||
|
||||
// It gets shared between threads in `ndk-glue`
|
||||
unsafe impl Send for InputQueue {}
|
||||
unsafe impl Sync for InputQueue {}
|
||||
|
||||
impl InputQueue {
|
||||
/// Construct an [`InputQueue`] from the native pointer.
|
||||
///
|
||||
/// # Safety
|
||||
/// By calling this function, you assert that the pointer is a valid pointer to an NDK [`ffi::AInputQueue`].
|
||||
pub unsafe fn from_ptr(ptr: NonNull<ffi::AInputQueue>) -> Self {
|
||||
Self { ptr }
|
||||
}
|
||||
|
||||
/// Returns the [`InputQueue`] object associated with the supplied
|
||||
/// [Java `InputQueue`][`android.view.InputQueue`] object.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// This function should be called with a healthy JVM pointer and with a non-null
|
||||
/// [`android.view.InputQueue`], which must be kept alive on the Java/Kotlin side.
|
||||
///
|
||||
/// The returned native object holds a weak reference to the Java object, and is only valid as
|
||||
/// long as the Java object has not yet been disposed. You should ensure that there is a strong
|
||||
/// reference to the Java object and that it has not been disposed before using the returned
|
||||
/// object.
|
||||
///
|
||||
/// [`android.view.InputQueue`]: https://developer.android.com/reference/android/view/InputQueue
|
||||
#[cfg(feature = "api-level-33")]
|
||||
#[doc(alias = "AInputQueue_fromJava")]
|
||||
pub unsafe fn from_java(env: *mut JNIEnv, input_queue: jobject) -> Option<Self> {
|
||||
let ptr = unsafe { ffi::AInputQueue_fromJava(env, input_queue) };
|
||||
Some(Self::from_ptr(NonNull::new(ptr)?))
|
||||
}
|
||||
|
||||
pub fn ptr(&self) -> NonNull<ffi::AInputQueue> {
|
||||
self.ptr
|
||||
}
|
||||
|
||||
/// Returns the next available [`InputEvent`] from the queue.
|
||||
///
|
||||
/// Returns [`None`] if no event is available.
|
||||
#[doc(alias = "AInputQueue_getEvent")]
|
||||
pub fn event(&self) -> Result<Option<InputEvent>> {
|
||||
let mut out_event = ptr::null_mut();
|
||||
let status = unsafe { ffi::AInputQueue_getEvent(self.ptr.as_ptr(), &mut out_event) };
|
||||
match status_to_io_result(status) {
|
||||
Ok(()) => {
|
||||
debug_assert!(!out_event.is_null());
|
||||
Ok(Some(unsafe {
|
||||
InputEvent::from_ptr(NonNull::new_unchecked(out_event))
|
||||
}))
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`true`] if there are one or more events available in the input queue.
|
||||
#[doc(alias = "AInputQueue_hasEvents")]
|
||||
pub fn has_events(&self) -> bool {
|
||||
match unsafe { ffi::AInputQueue_hasEvents(self.ptr.as_ptr()) } {
|
||||
0 => false,
|
||||
1 => true,
|
||||
r => unreachable!("AInputQueue_hasEvents returned non-boolean {}", r),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the key for standard pre-dispatching that is, possibly deliver it to the current IME
|
||||
/// to be consumed before the app.
|
||||
///
|
||||
/// Returns [`Some`] if it was not pre-dispatched, meaning you can process it right now. If
|
||||
/// [`None`] is returned, you must abandon the current event processing and allow the event to
|
||||
/// appear again in the event queue (if it does not get consumed during pre-dispatching).
|
||||
///
|
||||
/// Also returns [`None`] if `event` is not a [`KeyEvent`].
|
||||
#[doc(alias = "AInputQueue_preDispatchEvent")]
|
||||
pub fn pre_dispatch(&self, event: InputEvent) -> Option<InputEvent> {
|
||||
match unsafe { ffi::AInputQueue_preDispatchEvent(self.ptr.as_ptr(), event.ptr().as_ptr()) }
|
||||
{
|
||||
0 => Some(event),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Report that dispatching has finished with the given [`InputEvent`].
|
||||
///
|
||||
/// This must be called after receiving an event with [`InputQueue::event()`].
|
||||
#[doc(alias = "AInputQueue_finishEvent")]
|
||||
pub fn finish_event(&self, event: InputEvent, handled: bool) {
|
||||
unsafe {
|
||||
ffi::AInputQueue_finishEvent(self.ptr.as_ptr(), event.ptr().as_ptr(), handled as c_int)
|
||||
}
|
||||
}
|
||||
|
||||
/// Add this input queue to a [`ForeignLooper`] for processing.
|
||||
///
|
||||
/// See [`ForeignLooper::add_fd()`] for information on the `ident`, `callback`, and `data` params.
|
||||
#[doc(alias = "AInputQueue_attachLooper")]
|
||||
pub fn attach_looper(&self, looper: &ForeignLooper, id: i32) {
|
||||
unsafe {
|
||||
ffi::AInputQueue_attachLooper(
|
||||
self.ptr.as_ptr(),
|
||||
looper.ptr().as_ptr(),
|
||||
id,
|
||||
None,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove this input queue from the [`ForeignLooper`] it is currently attached to.
|
||||
#[doc(alias = "AInputQueue_detachLooper")]
|
||||
pub fn detach_looper(&self) {
|
||||
unsafe { ffi::AInputQueue_detachLooper(self.ptr.as_ptr()) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user