diff --git a/clients/android/native/src/decode.rs b/clients/android/native/src/decode.rs index e519d5dd..523b29e5 100644 --- a/clients/android/native/src/decode.rs +++ b/clients/android/native/src/decode.rs @@ -202,6 +202,8 @@ fn run_sync( let mut fed: u64 = 0; let mut rendered: u64 = 0; let mut discarded: u64 = 0; + // AUs larger than the codec input buffer, dropped whole (see `feed`/`feed_ready`). + let mut oversized_dropped: u64 = 0; // The AU waiting for a free codec input buffer. `feed` is non-blocking; on transient input // pressure the AU stays parked here instead of being dropped (a drop forces a keyframe // round-trip) and we only pop the next one once it's queued. @@ -296,7 +298,7 @@ fn run_sync( // and excluded, so ADPF sees this thread's real per-frame CPU cost, not the poll timeout. let work_t0 = Instant::now(); if let Some(frame) = pending.take() { - if feed(&codec, &frame.data, frame.pts_ns / 1000) { + if feed(&codec, &client, &frame.data, frame.pts_ns / 1000, &mut oversized_dropped) { fed += 1; if fed % 300 == 0 { log::info!("decode: fed={fed} rendered={rendered} discarded={discarded}"); @@ -886,6 +888,8 @@ fn run_async( let mut fed: u64 = 0; let mut rendered: u64 = 0; let mut discarded: u64 = 0; + // AUs larger than the codec input buffer, dropped whole (see `feed`/`feed_ready`). + let mut oversized_dropped: u64 = 0; let mut last_dropped = client.frames_dropped(); let mut last_kf_req: Option = None; // Productive (dispatch+feed+present) time between displayed frames; reported to ADPF once one is @@ -930,7 +934,14 @@ fn run_async( if fmt_dirty { apply_hdr_dataspace(&codec, &window, &mut applied_ds); } - feed_ready(&codec, &mut pending_aus, &mut free_inputs, &mut fed); + feed_ready( + &codec, + &client, + &mut pending_aus, + &mut free_inputs, + &mut fed, + &mut oversized_dropped, + ); let had_output = !ready.is_empty(); present_ready( &codec, @@ -1098,12 +1109,15 @@ fn dispatch_event( /// Queue as many parked AUs as there are free input buffer slots (async mode: the indices come from /// `InputAvailable` callbacks, not a dequeue). Each AU is copied into its codec input buffer and -/// submitted; a too-large AU is truncated (logged) rather than dropped. +/// submitted; an AU larger than the buffer is DROPPED (+ a recovery keyframe requested) — a +/// truncated AU is corrupt input the decoder chews on silently, poisoning the reference chain. fn feed_ready( codec: &MediaCodec, + client: &NativeClient, pending_aus: &mut VecDeque, free_inputs: &mut VecDeque, fed: &mut u64, + oversized_dropped: &mut u64, ) { while !pending_aus.is_empty() && !free_inputs.is_empty() { let idx = free_inputs.pop_front().unwrap(); @@ -1114,14 +1128,20 @@ fn feed_ready( continue; }; let au = &frame.data; - let n = au.len().min(dst.len()); - if n < au.len() { + if au.len() > dst.len() { + // The slot was never queued, so it stays ours — recycle it for the next AU. + free_inputs.push_front(idx); + *oversized_dropped += 1; log::warn!( - "decode: AU {} > input buffer {}, truncated", + "decode: AU {} > input buffer {} — dropped ({} so far), requesting keyframe", au.len(), - dst.len() + dst.len(), + *oversized_dropped ); + let _ = client.request_keyframe(); + continue; } + let n = au.len(); // SAFETY: `au` (wire AU) and `dst` (codec input buffer) are distinct allocations, both valid // for `n` bytes; `MaybeUninit` is layout-identical to `u8`, so this initializes dst[..n]. unsafe { @@ -1307,27 +1327,40 @@ fn try_set_frame_rate(window: &NativeWindow, frame_rate: f32, is_tv: bool) -> bo /// Try to copy one access unit into a codec input buffer and queue it, without blocking. Returns /// `false` only on `TryAgainLater` (no input buffer free) — the caller keeps the AU pending and /// retries; a hard dequeue/queue error counts as consumed (retrying can't salvage the AU, and -/// parking it forever would wedge the loop on a broken codec). -fn feed(codec: &MediaCodec, au: &[u8], pts_us: u64) -> bool { +/// parking it forever would wedge the loop on a broken codec). An AU larger than the input +/// buffer is DROPPED (+ a recovery keyframe requested), never truncated — a truncated AU is +/// corrupt input the decoder chews on silently, poisoning the reference chain. +fn feed( + codec: &MediaCodec, + client: &NativeClient, + au: &[u8], + pts_us: u64, + oversized_dropped: &mut u64, +) -> bool { match codec.dequeue_input_buffer(Duration::ZERO) { Ok(DequeuedInputBufferResult::Buffer(mut buf)) => { let n = { let dst = buf.buffer_mut(); - let n = au.len().min(dst.len()); - if n < au.len() { + if au.len() > dst.len() { + *oversized_dropped += 1; log::warn!( - "decode: AU {} > input buffer {}, truncated", + "decode: AU {} > input buffer {} — dropped ({} so far), requesting keyframe", au.len(), - dst.len() + dst.len(), + *oversized_dropped ); + let _ = client.request_keyframe(); + 0 // return the slot with zero valid bytes — a no-op input, not corrupt data + } else { + let n = au.len(); + // SAFETY: `au` and `dst` are distinct allocations (wire AU vs. codec buffer), + // both valid for `n` bytes; `MaybeUninit` is layout-identical to `u8`, so + // the cast write initializes exactly `dst[..n]`. + unsafe { + std::ptr::copy_nonoverlapping(au.as_ptr(), dst.as_mut_ptr().cast::(), n); + } + n } - // SAFETY: `au` and `dst` are distinct allocations (wire AU vs. codec buffer), both - // valid for `n` bytes; `MaybeUninit` is layout-identical to `u8`, so the cast - // write initializes exactly `dst[..n]`. - unsafe { - std::ptr::copy_nonoverlapping(au.as_ptr(), dst.as_mut_ptr().cast::(), n); - } - n }; if let Err(e) = codec.queue_input_buffer(buf, 0, n, pts_us, 0) { log::warn!("decode: queue_input_buffer: {e}"); diff --git a/crates/punktfunk-core/src/client.rs b/crates/punktfunk-core/src/client.rs index 83f18449..ccaf6e10 100644 --- a/crates/punktfunk-core/src/client.rs +++ b/crates/punktfunk-core/src/client.rs @@ -99,7 +99,8 @@ struct Negotiated { /// completes, so the old AU-based count cliffed to zero even though most bytes still arrived. #[derive(Default)] struct ProbeState { - /// A probe is in progress (set by `request_probe`, cleared by nothing — the latest one wins). + /// A probe is in progress: set by `request_probe`, cleared when the host's [`ProbeResult`] + /// lands (a re-probe just overwrites the whole state — the latest one wins). active: bool, /// `session.stats()` receive counters at the burst's start (snapshotted by the pump on its first /// tick while active) and latest, mirrored every pump iteration. @@ -215,6 +216,17 @@ const NOOP_CLOCK_FLUSHES_TO_DISARM: u32 = 2; /// FIRST no-op clock flush — the moment a step is actually suspected. const CLOCK_RESYNC_INTERVAL: Duration = Duration::from_secs(60); +/// Outbound mic uplink queue depth: 5 ms Opus frames, so 64 is ~320 ms of audio — far beyond +/// any worker stall a live mic session survives anyway. On overflow the FRESH frame is dropped +/// (a tokio mpsc can't shed from the head; by the time 320 ms are queued the stream is broken +/// either way, and the bound is about memory, not audio quality) and logged at debug. +const MIC_QUEUE: usize = 64; + +/// Outbound control-request queue depth. The requests are sparse (mode switches, keyframe +/// requests, ~1.3 loss reports/s, clock re-syncs) — 32 is hours of headroom; a full queue means +/// the control task is wedged, which callers treat as a closed session. +const CTRL_QUEUE: usize = 32; + /// The pre-decode video hand-off from the data-plane pump to the embedder. Unlike the side planes /// (self-contained samples that drop the newest on overflow), video AUs are reference-chained under the /// host's infinite GOP: dropping ANY frame mid-stream corrupts every dependent frame until the next @@ -353,11 +365,15 @@ pub struct NativeClient { host_timing: Mutex>, input_tx: tokio::sync::mpsc::UnboundedSender, /// Outbound mic frames `(seq, pts_ns, opus)` → encoded as 0xCB datagrams by the worker. - mic_tx: tokio::sync::mpsc::UnboundedSender<(u32, u64, Vec)>, + /// Bounded ([`MIC_QUEUE`]): a wedged worker drops fresh frames (logged) instead of queueing + /// audio-latency (and memory) without limit — mic is best-effort end to end. + mic_tx: tokio::sync::mpsc::Sender<(u32, u64, Vec)>, /// Outbound rich input (DualSense touchpad / motion) → 0xCC datagrams by the worker. rich_input_tx: tokio::sync::mpsc::UnboundedSender, /// Outbound control-stream requests (mode switch, speed test) → the worker's control task. - ctrl_tx: tokio::sync::mpsc::UnboundedSender, + /// Bounded ([`CTRL_QUEUE`]) — the requests are sparse; a full queue means the control task + /// is wedged/dead, and callers treat it like a closed session. + ctrl_tx: tokio::sync::mpsc::Sender, /// Speed-test accumulator, shared with the data-plane pump + control task. probe: Arc>, shutdown: Arc, @@ -549,9 +565,9 @@ impl NativeClient { let (host_timing_tx, host_timing_rx) = std::sync::mpsc::sync_channel::(HOST_TIMING_QUEUE); let (input_tx, input_rx) = tokio::sync::mpsc::unbounded_channel::(); - let (mic_tx, mic_rx) = tokio::sync::mpsc::unbounded_channel::<(u32, u64, Vec)>(); + let (mic_tx, mic_rx) = tokio::sync::mpsc::channel::<(u32, u64, Vec)>(MIC_QUEUE); let (rich_input_tx, rich_input_rx) = tokio::sync::mpsc::unbounded_channel::(); - let (ctrl_tx, ctrl_rx) = tokio::sync::mpsc::unbounded_channel::(); + let (ctrl_tx, ctrl_rx) = tokio::sync::mpsc::channel::(CTRL_QUEUE); let (ready_tx, ready_rx) = std::sync::mpsc::channel::>(); let shutdown = Arc::new(AtomicBool::new(false)); let quit = Arc::new(AtomicBool::new(false)); @@ -825,7 +841,7 @@ impl NativeClient { /// reflects it. A rejected request leaves the session unchanged. pub fn request_mode(&self, mode: Mode) -> Result<()> { self.ctrl_tx - .send(CtrlRequest::Mode(mode)) + .try_send(CtrlRequest::Mode(mode)) .map_err(|_| PunktfunkError::Closed) } @@ -835,7 +851,7 @@ impl NativeClient { /// lands, so requesting on every frame would flood the control stream). pub fn request_keyframe(&self) -> Result<()> { self.ctrl_tx - .send(CtrlRequest::Keyframe) + .try_send(CtrlRequest::Keyframe) .map_err(|_| PunktfunkError::Closed) } @@ -915,7 +931,7 @@ impl NativeClient { ..Default::default() }; self.ctrl_tx - .send(CtrlRequest::Probe(ProbeRequest { + .try_send(CtrlRequest::Probe(ProbeRequest { target_kbps, duration_ms, })) @@ -1061,9 +1077,17 @@ impl NativeClient { /// uses them only for diagnostics). The host decodes it into a virtual microphone source. /// Best-effort — like every datagram, it's dropped under loss; no retransmit. pub fn send_mic(&self, seq: u32, pts_ns: u64, opus: Vec) -> Result<()> { - self.mic_tx - .send((seq, pts_ns, opus)) - .map_err(|_| PunktfunkError::Closed) + use tokio::sync::mpsc::error::TrySendError; + match self.mic_tx.try_send((seq, pts_ns, opus)) { + Ok(()) => Ok(()), + Err(TrySendError::Full(_)) => { + // Bounded queue full = the worker stalled for ~MIC_QUEUE x 5 ms. Shed this + // frame (mic is best-effort end to end) instead of queueing latency/memory. + tracing::debug!("mic uplink queue full — dropping frame"); + Ok(()) + } + Err(TrySendError::Closed(_)) => Err(PunktfunkError::Closed), + } } /// Queue one rich input event (DualSense touchpad contact or motion sample) for delivery as a @@ -1115,10 +1139,10 @@ struct WorkerArgs { hdr_meta_tx: SyncSender, host_timing_tx: SyncSender, input_rx: tokio::sync::mpsc::UnboundedReceiver, - mic_rx: tokio::sync::mpsc::UnboundedReceiver<(u32, u64, Vec)>, + mic_rx: tokio::sync::mpsc::Receiver<(u32, u64, Vec)>, rich_input_rx: tokio::sync::mpsc::UnboundedReceiver, - ctrl_rx: tokio::sync::mpsc::UnboundedReceiver, - ctrl_tx: tokio::sync::mpsc::UnboundedSender, + ctrl_rx: tokio::sync::mpsc::Receiver, + ctrl_tx: tokio::sync::mpsc::Sender, ready_tx: std::sync::mpsc::Sender>, shutdown: Arc, /// Deliberate-quit flag (see [`NativeClient::quit`]): the worker closes with the quit code if set. @@ -1472,6 +1496,7 @@ async fn worker_main(args: WorkerArgs) { p.host_send_dropped = result.send_dropped; p.host_duration_ms = result.duration_ms; p.done = true; + p.active = false; // burst over — the pump stops mirroring counters tracing::info!( host_goodput_bytes = result.bytes_sent, wire_packets_sent = result.wire_packets_sent, @@ -1664,7 +1689,7 @@ async fn worker_main(args: WorkerArgs) { // the mid-stream re-sync now (once — the 60 s periodic covers everything else). if resync_wanted { resync_wanted = false; - let _ = ctrl_tx.send(CtrlRequest::ClockResync); + let _ = ctrl_tx.try_send(CtrlRequest::ClockResync); } let window_dropped = st.frames_dropped.wrapping_sub(last_dropped); let loss_ppm = window_loss_ppm( @@ -1672,7 +1697,7 @@ async fn worker_main(args: WorkerArgs) { st.packets_received.wrapping_sub(last_received), window_dropped, ); - let _ = ctrl_tx.send(CtrlRequest::Loss(LossReport { loss_ppm })); + let _ = ctrl_tx.try_send(CtrlRequest::Loss(LossReport { loss_ppm })); // Adaptive bitrate: drain any host ack first (its clamp is authoritative), then // feed the controller this window's congestion signals; a decision becomes a // SetBitrate on the control stream. @@ -1690,7 +1715,7 @@ async fn worker_main(args: WorkerArgs) { flush_in_window, ) { tracing::info!(kbps, "adaptive bitrate: requesting encoder re-target"); - let _ = ctrl_tx.send(CtrlRequest::SetBitrate(kbps)); + let _ = ctrl_tx.try_send(CtrlRequest::SetBitrate(kbps)); } flush_in_window = false; last_report = Instant::now(); @@ -1762,7 +1787,7 @@ async fn worker_main(args: WorkerArgs) { flush_in_window = true; // strongest "link can't hold the rate" signal let flushed = session.flush_backlog().unwrap_or(0); let dropped = frames.clear(); - let _ = ctrl_tx.send(CtrlRequest::Keyframe); + let _ = ctrl_tx.try_send(CtrlRequest::Keyframe); tracing::warn!( behind_ms = if clock_behind { lat_ns / 1_000_000 } else { -1 }, queue_depth = depth, diff --git a/crates/punktfunk-core/src/quic/msgs.rs b/crates/punktfunk-core/src/quic/msgs.rs index d6fc47b1..f5821b37 100644 --- a/crates/punktfunk-core/src/quic/msgs.rs +++ b/crates/punktfunk-core/src/quic/msgs.rs @@ -902,6 +902,17 @@ impl Welcome { c.encrypt = self.encrypt; c.key = self.key; c.salt = self.salt; + // Client-side reassembler ceiling: p1_defaults' 64 MiB hostile-header memory bound is + // ~10x larger than any real access unit. Derive it from the negotiated rate instead: + // 4x the average frame size at the resolved bitrate (IDR headroom), floored at 8 MiB, + // capped at the old 64 MiB. Purely local — the host never reassembles video and the + // wire is self-describing, so old hosts are unaffected; a host that reports bitrate 0 + // (pre-negotiation) keeps the old bound. + if role == Role::Client && self.bitrate_kbps > 0 { + let per_frame = (self.bitrate_kbps as usize).saturating_mul(125) + / self.mode.refresh_hz.max(1) as usize; + c.max_frame_bytes = per_frame.saturating_mul(4).clamp(8 << 20, 64 << 20); + } c } } diff --git a/crates/punktfunk-core/src/quic/tests.rs b/crates/punktfunk-core/src/quic/tests.rs index 08e541ea..ddad5a34 100644 --- a/crates/punktfunk-core/src/quic/tests.rs +++ b/crates/punktfunk-core/src/quic/tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::config::{CompositorPref, FecConfig, FecScheme, GamepadPref, Mode}; +use crate::config::{CompositorPref, FecConfig, FecScheme, GamepadPref, Mode, Role}; #[test] fn welcome_roundtrip() { @@ -32,6 +32,35 @@ fn welcome_roundtrip() { host_caps: HOST_CAP_GAMEPAD_STATE, }; assert_eq!(Welcome::decode(&w.encode()).unwrap(), w); + + // Client-side reassembler ceiling derives from the negotiated rate: 4x the average frame at + // 50 Mbps/240 Hz is ~104 KB, so the 8 MiB floor governs. The host keeps the p1_defaults + // bound (it never reassembles video), as does a client of a bitrate-0 (older) host. + let cc = w.session_config(Role::Client); + assert_eq!(cc.max_frame_bytes, 8 << 20); + cc.validate().expect("derived client config validates"); + assert_eq!(w.session_config(Role::Host).max_frame_bytes, 64 << 20); + let old_host = Welcome { + bitrate_kbps: 0, + ..w.clone() + }; + assert_eq!( + old_host.session_config(Role::Client).max_frame_bytes, + 64 << 20 + ); + // A high-rate mode scales past the floor: 1.5 Gbps at 60 Hz = 4 x 3.125 MB = 12.5 MB. + let fat = Welcome { + bitrate_kbps: 1_500_000, + mode: Mode { + width: 5120, + height: 1440, + refresh_hz: 60, + }, + ..w.clone() + }; + let derived = fat.session_config(Role::Client).max_frame_bytes; + assert_eq!(derived, 4 * 1_500_000 * 125 / 60); + assert!(derived > (8 << 20) && derived < (64 << 20)); } #[test] diff --git a/include/punktfunk_core.h b/include/punktfunk_core.h index db721f68..5d31dd21 100644 --- a/include/punktfunk_core.h +++ b/include/punktfunk_core.h @@ -250,6 +250,58 @@ // `shard_payload` so `HEADER_LEN + shard_payload + CRYPTO_OVERHEAD ≤ MAX_DATAGRAM_BYTES`. #define MAX_DATAGRAM_BYTES 2048 +#if defined(PUNKTFUNK_FEATURE_QUIC) +// Rounds per batch — matches the connect-time [`clock_sync`]. +#define ClockResync_ROUNDS 8 +#endif + +#if defined(PUNKTFUNK_FEATURE_QUIC) +// Datagram wire tags. Video rides UDP; everything low-rate rides QUIC datagrams, +// demultiplexed by the first byte: input = [`crate::input::INPUT_MAGIC`] (0xC8, client→host), +// audio = [`AUDIO_MAGIC`] (0xC9, host→client), rumble = [`RUMBLE_MAGIC`] (0xCA, host→client), +// mic = [`MIC_MAGIC`] (0xCB, client→host), rich-input = [`RICH_INPUT_MAGIC`] (0xCC, client→host), +// HID-output = [`HIDOUT_MAGIC`] (0xCD, host→client), HDR metadata = [`HDR_META_MAGIC`] +// (0xCE, host→client). +#define PUNKTFUNK_AUDIO_MAGIC 201 +#endif + +#if defined(PUNKTFUNK_FEATURE_QUIC) +#define PUNKTFUNK_RUMBLE_MAGIC 202 +#endif + +#if defined(PUNKTFUNK_FEATURE_QUIC) +// Microphone uplink: the client's mic, Opus-encoded, client → host (the inverse of +// [`AUDIO_MAGIC`]). The host feeds it into a virtual PipeWire source so its apps can record it. +#define MIC_MAGIC 203 +#endif + +#if defined(PUNKTFUNK_FEATURE_QUIC) +// Rich client→host input: events too big for the fixed 18-byte [`InputEvent`] +// (crate::input::InputEvent) — the DualSense touchpad and motion sensors. Variable-length, +// kind-tagged (see [`RichInput`]). +#define RICH_INPUT_MAGIC 204 +#endif + +#if defined(PUNKTFUNK_FEATURE_QUIC) +// HID output, host → client: DualSense feedback a game wrote to the host's virtual controller +// (lightbar, player LEDs, adaptive triggers) — the rich analog of [`RUMBLE_MAGIC`]. See +// [`HidOutput`]. +#define HIDOUT_MAGIC 205 +#endif + +#if defined(PUNKTFUNK_FEATURE_QUIC) +// HDR static-metadata datagram tag, host → client (the static analog of the per-frame VUI; +// see [`HdrMeta`]). Next tag after [`HIDOUT_MAGIC`]. +#define HDR_META_MAGIC 206 +#endif + +#if defined(PUNKTFUNK_FEATURE_QUIC) +// Per-AU host-timing datagram tag, host → client (see [`HostTiming`]). Next tag after +// [`HDR_META_MAGIC`]. Emitted once per access unit, right after its last packet left the host's +// socket, and only when the client advertised [`VIDEO_CAP_HOST_TIMING`]. +#define HOST_TIMING_MAGIC 207 +#endif + #if defined(PUNKTFUNK_FEATURE_QUIC) // [`Hello::video_caps`] bit: the client can decode a 10-bit (Main10) HEVC stream. #define VIDEO_CAP_10BIT 1 @@ -419,53 +471,6 @@ #define MSG_PAIR_RESULT 19 #endif -#if defined(PUNKTFUNK_FEATURE_QUIC) -// Datagram wire tags. Video rides UDP; everything low-rate rides QUIC datagrams, -// demultiplexed by the first byte: input = [`crate::input::INPUT_MAGIC`] (0xC8, client→host), -// audio = [`AUDIO_MAGIC`] (0xC9, host→client), rumble = [`RUMBLE_MAGIC`] (0xCA, host→client), -// mic = [`MIC_MAGIC`] (0xCB, client→host), rich-input = [`RICH_INPUT_MAGIC`] (0xCC, client→host), -// HID-output = [`HIDOUT_MAGIC`] (0xCD, host→client), HDR metadata = [`HDR_META_MAGIC`] -// (0xCE, host→client). -#define PUNKTFUNK_AUDIO_MAGIC 201 -#endif - -#if defined(PUNKTFUNK_FEATURE_QUIC) -#define PUNKTFUNK_RUMBLE_MAGIC 202 -#endif - -#if defined(PUNKTFUNK_FEATURE_QUIC) -// Microphone uplink: the client's mic, Opus-encoded, client → host (the inverse of -// [`AUDIO_MAGIC`]). The host feeds it into a virtual PipeWire source so its apps can record it. -#define MIC_MAGIC 203 -#endif - -#if defined(PUNKTFUNK_FEATURE_QUIC) -// Rich client→host input: events too big for the fixed 18-byte [`InputEvent`] -// (crate::input::InputEvent) — the DualSense touchpad and motion sensors. Variable-length, -// kind-tagged (see [`RichInput`]). -#define RICH_INPUT_MAGIC 204 -#endif - -#if defined(PUNKTFUNK_FEATURE_QUIC) -// HID output, host → client: DualSense feedback a game wrote to the host's virtual controller -// (lightbar, player LEDs, adaptive triggers) — the rich analog of [`RUMBLE_MAGIC`]. See -// [`HidOutput`]. -#define HIDOUT_MAGIC 205 -#endif - -#if defined(PUNKTFUNK_FEATURE_QUIC) -// HDR static-metadata datagram tag, host → client (the static analog of the per-frame VUI; -// see [`HdrMeta`]). Next tag after [`HIDOUT_MAGIC`]. -#define HDR_META_MAGIC 206 -#endif - -#if defined(PUNKTFUNK_FEATURE_QUIC) -// Per-AU host-timing datagram tag, host → client (see [`HostTiming`]). Next tag after -// [`HDR_META_MAGIC`]. Emitted once per access unit, right after its last packet left the host's -// socket, and only when the client advertised [`VIDEO_CAP_HOST_TIMING`]. -#define HOST_TIMING_MAGIC 207 -#endif - #if defined(PUNKTFUNK_FEATURE_QUIC) // CICP colour-primaries code point: BT.709. #define ColorInfo_CP_BT709 1 @@ -502,11 +507,6 @@ #define ColorInfo_MC_BT2020_NCL 9 #endif -#if defined(PUNKTFUNK_FEATURE_QUIC) -// Rounds per batch — matches the connect-time [`clock_sync`]. -#define ClockResync_ROUNDS 8 -#endif - // Stable C ABI status codes. `Ok` is 0; all errors are negative so callers can // test `rc < 0`. Do not renumber existing variants — only append. enum PunktfunkStatus