Files
punktfunk/clients/android/native/vendor/ndk/src/sync.rs
T
enricobuehler 13d1aa5738 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>
2026-07-09 11:35:16 +02:00

144 lines
4.4 KiB
Rust

//! Bindings for [sync functions]
//!
//! [sync functions]: https://developer.android.com/ndk/reference/group/sync
#![cfg(feature = "sync")]
use std::{
ffi::CStr,
fmt::Debug,
// TODO: Import from std::os::fd::{} since Rust 1.66
os::unix::io::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd},
ptr::NonNull,
};
#[doc(alias = "sync_file_info")]
#[repr(transparent)]
pub struct SyncFileInfo {
inner: NonNull<ffi::sync_file_info>,
}
impl Debug for SyncFileInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SyncFileInfo")
.field("name", &self.name())
.field("status", &self.status())
.field("flags", &self.flags())
.field("num_fences", &self.num_fences())
.field("fence_info", &self.fence_info())
.finish()
}
}
impl SyncFileInfo {
/// Retrieve detailed information about a sync file and its fences.
#[doc(alias = "sync_file_info")]
pub fn new(fd: BorrowedFd<'_>) -> Option<Self> {
let inner = NonNull::new(unsafe { ffi::sync_file_info(fd.as_raw_fd()) })?;
Some(Self { inner })
}
pub fn name(&self) -> &CStr {
let inner = unsafe { self.inner.as_ref() };
// TODO: Switch to CStr::from_bytes_until_nul (with c_char -> u8 transmute) since MSRV 1.69
// https://github.com/ash-rs/ash/pull/746
unsafe { CStr::from_ptr(inner.name.as_ptr()) }
}
pub fn status(&self) -> i32 {
let inner = unsafe { self.inner.as_ref() };
inner.status
}
pub fn flags(&self) -> u32 {
let inner = unsafe { self.inner.as_ref() };
inner.flags
}
pub fn num_fences(&self) -> usize {
let inner = unsafe { self.inner.as_ref() };
inner.num_fences as usize
}
/// Get the array of fence infos from the sync file's info.
#[doc(alias = "sync_get_fence_info")]
pub fn fence_info(&self) -> &[SyncFenceInfo] {
let inner = unsafe { self.inner.as_ref() };
if inner.num_fences == 0 {
&[]
} else {
let sync_fence_info = NonNull::new(inner.sync_fence_info as *mut _)
.expect("sync_fence_info cannot be null if num_fences > 0");
unsafe {
std::slice::from_raw_parts(sync_fence_info.as_ptr(), inner.num_fences as usize)
}
}
}
}
impl Drop for SyncFileInfo {
/// Free a [`struct@ffi::sync_file_info`] structure.
#[doc(alias = "sync_file_info_free")]
fn drop(&mut self) {
unsafe { ffi::sync_file_info_free(self.inner.as_ptr()) }
}
}
#[doc(alias = "sync_fence_info")]
#[repr(transparent)]
pub struct SyncFenceInfo(ffi::sync_fence_info);
impl Debug for SyncFenceInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SyncFenceInfo")
.field("obj_name", &self.obj_name())
.field("driver_name", &self.driver_name())
.field("status", &self.status())
.field("flags", &self.flags())
.field("timestamp_ns", &self.timestamp_ns())
.finish()
}
}
impl SyncFenceInfo {
pub fn obj_name(&self) -> &CStr {
// TODO: Switch to CStr::from_bytes_until_nul (with c_char -> u8 transmute) since MSRV 1.69
unsafe { CStr::from_ptr(self.0.obj_name.as_ptr()) }
}
pub fn driver_name(&self) -> &CStr {
// TODO: Switch to CStr::from_bytes_until_nul (with c_char -> u8 transmute) since MSRV 1.69
unsafe { CStr::from_ptr(self.0.driver_name.as_ptr()) }
}
pub fn status(&self) -> i32 {
self.0.status
}
pub fn flags(&self) -> u32 {
self.0.flags
}
pub fn timestamp_ns(&self) -> u64 {
self.0.timestamp_ns
}
}
/// Merge two sync files.
///
/// This produces a new sync file with the given name which has the union of the two original sync
/// file's fences; redundant fences may be removed.
///
/// If one of the input sync files is signaled or invalid, then this function may behave like
/// `dup()`: the new file descriptor refers to the valid/unsignaled sync file with its original
/// name, rather than a new sync file.
pub fn sync_merge(name: &CStr, fd1: BorrowedFd<'_>, fd2: BorrowedFd<'_>) -> OwnedFd {
unsafe {
OwnedFd::from_raw_fd(ffi::sync_merge(
name.as_ptr(),
fd1.as_raw_fd(),
fd2.as_raw_fd(),
))
}
}