feat(protocol): per-AU host-timing plane (0xCF) — split host+network latency (stats phase 2)
The unified-stats equation's host+network stage was one opaque number because the wire carried nothing but pts_ns. Now the host reports its own share per frame: when the client's Hello sets VIDEO_CAP_HOST_TIMING (0x08), the send thread emits a 13-byte 0xCF datagram — [tag][pts_ns u64][host_us u32] — right after the AU's last packet leaves the socket, so host_us = capture→fully-sent (capture read/convert, encode, FEC+seal, paced send) against the same anchor the wire pts carries. Clients correlate by pts_ns and derive network = (received + clock_offset − pts) − host_us; the two terms tile per frame by construction. Back-compat is free in all four combinations: old clients ignore unknown datagram tags, old hosts ignore unknown cap bits (client keeps the combined stage). The hardened data-plane format is untouched — this rides the established QUIC side-plane pattern (0xC8…0xCE). NativeClient ORs the bit in unconditionally and exposes next_host_timing(); the C ABI gains PunktfunkHostTiming + punktfunk_connection_next_host_timing (additive). The synthetic host emits 0xCF too, so pure-loopback protocol tests cover the plane. The probe reports the split (host_p50/p95_us · net_p50/p95_us) and is our direct analogue of Sunshine's "host processing latency" — ours additionally includes the paced send. Validated on loopback (synthetic host + probe, debug build): 240/240 AUs matched, host_p50 6.5 ms + net_p50 6.4 ms ≈ capture→received p50 13.0 ms. Core suite + new 0xCF roundtrip/truncation test green; host+core+probe clippy clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -635,6 +635,22 @@ impl PunktfunkHdrMeta {
|
||||
}
|
||||
}
|
||||
|
||||
/// One access unit's host-side processing time ([`punktfunk_connection_next_host_timing`]):
|
||||
/// capture → fully sent, i.e. the whole host pipeline (capture read/convert, encode, FEC+seal,
|
||||
/// paced send). Correlate to the AU whose `PunktfunkFrame::pts_ns` equals `pts_ns`, then
|
||||
/// `network = (received_instant + clock_offset − pts_ns) − host_us` — the unified stats HUD's
|
||||
/// `host` / `network` split (design/stats-unification.md Phase 2). Best-effort: a lost datagram
|
||||
/// means that frame simply contributes no sample.
|
||||
#[cfg(feature = "quic")]
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PunktfunkHostTiming {
|
||||
/// The AU's capture stamp (host capture clock — matches `PunktfunkFrame::pts_ns` exactly).
|
||||
pub pts_ns: u64,
|
||||
/// Host capture→sent duration, µs.
|
||||
pub host_us: u32,
|
||||
}
|
||||
|
||||
/// `PunktfunkRichInput::kind` — a touchpad contact (`finger`/`active`/`x`/`y` valid).
|
||||
pub const PUNKTFUNK_RICH_TOUCHPAD: u8 = 1;
|
||||
/// `PunktfunkRichInput::kind` — a motion sample (`gyro`/`accel` valid).
|
||||
@@ -1759,6 +1775,49 @@ pub unsafe extern "C" fn punktfunk_connection_next_hdr_meta(
|
||||
})
|
||||
}
|
||||
|
||||
/// Pull the next per-AU host timing (0xCF) into `*out`: the host's capture→sent duration for one
|
||||
/// access unit, correlated to the AU by `pts_ns` (see [`PunktfunkHostTiming`]).
|
||||
/// [`PunktfunkStatus::NoFrame`] on timeout, [`PunktfunkStatus::Closed`] once the session ended.
|
||||
/// A stats consumer drains this non-blockingly (`timeout_ms = 0`) alongside its frame samples;
|
||||
/// an older host never emits any — keep showing the combined `host+network` stage then. Same
|
||||
/// threading rules as [`punktfunk_connection_next_rumble`] (one puller, may run alongside the
|
||||
/// other planes).
|
||||
///
|
||||
/// # Safety
|
||||
/// `c` is a valid connection handle; `out` is writable for one `PunktfunkHostTiming`.
|
||||
#[cfg(feature = "quic")]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn punktfunk_connection_next_host_timing(
|
||||
c: *mut PunktfunkConnection,
|
||||
out: *mut PunktfunkHostTiming,
|
||||
timeout_ms: u32,
|
||||
) -> PunktfunkStatus {
|
||||
guard(|| {
|
||||
let c = match unsafe { c.as_ref() } {
|
||||
Some(c) => c,
|
||||
None => return PunktfunkStatus::NullPointer,
|
||||
};
|
||||
if out.is_null() {
|
||||
return PunktfunkStatus::NullPointer;
|
||||
}
|
||||
match c
|
||||
.inner
|
||||
.next_host_timing(std::time::Duration::from_millis(timeout_ms as u64))
|
||||
{
|
||||
Ok(t) => {
|
||||
unsafe {
|
||||
*out = PunktfunkHostTiming {
|
||||
pts_ns: t.pts_ns,
|
||||
host_us: t.host_us,
|
||||
}
|
||||
};
|
||||
PunktfunkStatus::Ok
|
||||
}
|
||||
Err(e) => e.status(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Read the session's resolved colour signalling + encode bit depth (from the host's Welcome).
|
||||
/// Each out pointer is filled when non-NULL: `primaries`/`transfer`/`matrix` are CICP code points
|
||||
/// (BT.709 = 1; BT.2020 = 9; PQ transfer = 16, HLG = 18; BT.2020-NCL matrix = 9), `full_range` is
|
||||
|
||||
Reference in New Issue
Block a user