The controller learns stillness from the host, and motion stops reading as congestion #407
@@ -191,6 +191,7 @@ include = ["PunktfunkEndReason"]
|
||||
"HOST_CAP_GAMEPAD_STATE" = "PUNKTFUNK_HOST_CAP_GAMEPAD_STATE"
|
||||
"HOST_CAP_PEN" = "PUNKTFUNK_HOST_CAP_PEN"
|
||||
"HOST_CAP_TEXT_INPUT" = "PUNKTFUNK_HOST_CAP_TEXT_INPUT"
|
||||
"HOST_CAP2_REPEAT_MARK" = "PUNKTFUNK_HOST_CAP2_REPEAT_MARK"
|
||||
"HOST_TIMING_MAGIC" = "PUNKTFUNK_HOST_TIMING_MAGIC"
|
||||
"INBOUND_REQ_FLAG" = "PUNKTFUNK_INBOUND_REQ_FLAG"
|
||||
"INPUT_MAGIC" = "PUNKTFUNK_INPUT_MAGIC"
|
||||
@@ -267,6 +268,7 @@ include = ["PunktfunkEndReason"]
|
||||
"USER_FLAG_CHUNK_ALIGNED" = "PUNKTFUNK_USER_FLAG_CHUNK_ALIGNED"
|
||||
"USER_FLAG_RECOVERY_ANCHOR" = "PUNKTFUNK_USER_FLAG_RECOVERY_ANCHOR"
|
||||
"USER_FLAG_RECOVERY_POINT" = "PUNKTFUNK_USER_FLAG_RECOVERY_POINT"
|
||||
"USER_FLAG_REPEAT" = "PUNKTFUNK_USER_FLAG_REPEAT"
|
||||
"USER_FLAG_SLICE_STREAM" = "PUNKTFUNK_USER_FLAG_SLICE_STREAM"
|
||||
"VIDEO_CAP_10BIT" = "PUNKTFUNK_VIDEO_CAP_10BIT"
|
||||
"VIDEO_CAP_444" = "PUNKTFUNK_VIDEO_CAP_444"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -96,6 +96,10 @@ pub(crate) struct Negotiated {
|
||||
/// [`crate::quic::HOST_CAP_GAMEPAD_STATE`], [`crate::quic::HOST_CAP_CLIPBOARD`]. Exposed to the
|
||||
/// embedder via [`NativeClient::host_caps`] so a native client greys out unsupported toggles.
|
||||
pub(crate) host_caps: u8,
|
||||
/// The second capability byte ([`crate::quic::Welcome::host_caps2`]):
|
||||
/// [`crate::quic::HOST_CAP2_REPEAT_MARK`] tells the ABR that unflagged AUs are genuinely
|
||||
/// new content. `0` from an older host.
|
||||
pub(crate) host_caps2: u8,
|
||||
/// The host's management-API port ([`crate::quic::Welcome::mgmt_port`]), `0` when it did not
|
||||
/// advertise one. Surfaced to the embedder via [`crate::NativeClient::mgmt_port`] so a client
|
||||
/// can reach the game library without ever having seen an mDNS advert.
|
||||
|
||||
@@ -86,6 +86,10 @@ pub(super) async fn run_pump(args: WorkerArgs) {
|
||||
let clock_rtt_ns = negotiated.clock_rtt_ns;
|
||||
let resolved_bitrate_kbps = negotiated.bitrate_kbps;
|
||||
let negotiated_codec = negotiated.codec;
|
||||
// Whether the host marks its idle-keepalive repeats (USER_FLAG_REPEAT — RFC §4.1). Only
|
||||
// then can the ABR trust an unflagged AU as new content; an older host leaves the window
|
||||
// arithmetic in its legacy form.
|
||||
let marks_repeats = negotiated.host_caps2 & crate::quic::HOST_CAP2_REPEAT_MARK != 0;
|
||||
// Session constants a mode switch does not change — the pump recomputes the stream-shape
|
||||
// cap from them for the switched geometry (review §2.1).
|
||||
let bit_depth = negotiated.bit_depth;
|
||||
@@ -290,6 +294,7 @@ pub(super) async fn run_pump(args: WorkerArgs) {
|
||||
negotiated_codec,
|
||||
bit_depth,
|
||||
chroma_format,
|
||||
marks_repeats,
|
||||
stream_cap_kbps,
|
||||
refresh_hz,
|
||||
mode_slot: mode_slot_pump,
|
||||
|
||||
@@ -48,6 +48,11 @@ pub(super) struct DataPump {
|
||||
/// (review §2.1).
|
||||
pub(super) bit_depth: u8,
|
||||
pub(super) chroma_format: u8,
|
||||
/// The host marks idle-keepalive repeats with `USER_FLAG_REPEAT`
|
||||
/// ([`crate::quic::HOST_CAP2_REPEAT_MARK`] in the Welcome — RFC §4.1). Only then do the
|
||||
/// per-window active/repeat counts below mean anything; against an older host the ABR is
|
||||
/// handed `None` and keeps its legacy window arithmetic.
|
||||
pub(super) marks_repeats: bool,
|
||||
/// What this session's mode + codec could plausibly use (see
|
||||
/// [`crate::abr::stream_ceiling_kbps`]) — the bound the probe-measured link ceiling is held
|
||||
/// to. Computed where the negotiated geometry lives; recomputed here on an accepted mode
|
||||
@@ -85,6 +90,7 @@ impl DataPump {
|
||||
negotiated_codec,
|
||||
bit_depth,
|
||||
chroma_format,
|
||||
marks_repeats,
|
||||
stream_cap_kbps,
|
||||
refresh_hz,
|
||||
mode_slot: pump_mode_slot,
|
||||
@@ -212,6 +218,9 @@ impl DataPump {
|
||||
let mut discard_abr_window = false;
|
||||
let mut probe_watchdog: Option<Instant> = None;
|
||||
let (mut owd_sum_ns, mut owd_frames) = (0i128, 0u32);
|
||||
// Per-window AU activity split (RFC §4.1): total completed video AUs and how many the
|
||||
// host flagged as idle-keepalive repeats. Meaningful only on a `marks_repeats` host.
|
||||
let (mut au_frames, mut au_repeats) = (0u32, 0u32);
|
||||
let mut flush_in_window = false;
|
||||
// Jump-to-live state (see the guard in the loop below): when the clock-based over-bound
|
||||
// run began (`stale_since`, armed only when the skew handshake succeeded so the clocks
|
||||
@@ -586,6 +595,15 @@ impl DataPump {
|
||||
let owd_mean_us =
|
||||
(owd_frames > 0).then(|| (owd_sum_ns / owd_frames as i128 / 1000) as i64);
|
||||
(owd_sum_ns, owd_frames) = (0, 0);
|
||||
// Active = new content the source actually produced this window. `None` toward
|
||||
// an older host that doesn't mark repeats — the ABR then keeps its legacy
|
||||
// window arithmetic rather than misreading "no flags" as "all active".
|
||||
let active_frames = if marks_repeats {
|
||||
Some(au_frames.saturating_sub(au_repeats))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(au_frames, au_repeats) = (0, 0);
|
||||
// Drain the embedder's decode-latency window (always, so it stays bounded even when
|
||||
// the controller is disabled) → the mean feeds the decode signal; `None` when the
|
||||
// embedder reported nothing this window (old embedder / no decoded frames).
|
||||
@@ -635,6 +653,7 @@ impl DataPump {
|
||||
actual_kbps,
|
||||
flush_in_window,
|
||||
recovery_kf_reqs,
|
||||
active_frames,
|
||||
)
|
||||
};
|
||||
if let Some(kbps) = verdict {
|
||||
@@ -713,6 +732,14 @@ impl DataPump {
|
||||
// signals, so only the delivery that completes an AU feeds them (parts
|
||||
// would bias OWD low and constantly reset the staleness run).
|
||||
let is_au = frame.complete;
|
||||
if is_au {
|
||||
// The window's activity split (RFC §4.1) — repeats are the host's
|
||||
// idle keepalive, not new content.
|
||||
au_frames = au_frames.saturating_add(1);
|
||||
if frame.flags & crate::packet::USER_FLAG_REPEAT != 0 {
|
||||
au_repeats = au_repeats.saturating_add(1);
|
||||
}
|
||||
}
|
||||
if pump_perf_on && is_au {
|
||||
let now = Instant::now();
|
||||
if let Some(prev) = last_arrival.replace(now) {
|
||||
@@ -1097,6 +1124,7 @@ mod tests {
|
||||
negotiated_codec: crate::quic::CODEC_HEVC,
|
||||
bit_depth: 8,
|
||||
chroma_format: 0,
|
||||
marks_repeats: false,
|
||||
stream_cap_kbps: 100_000,
|
||||
refresh_hz: 60,
|
||||
mode_slot: Arc::new(Mutex::new(crate::config::Mode {
|
||||
|
||||
@@ -275,6 +275,7 @@ pub(super) async fn connect_and_handshake(args: &WorkerArgs) -> Result<Handshake
|
||||
codec: welcome.codec,
|
||||
shard_payload: welcome.shard_payload,
|
||||
host_caps: welcome.host_caps,
|
||||
host_caps2: welcome.host_caps2,
|
||||
mgmt_port: welcome.mgmt_port,
|
||||
grants: welcome.grants,
|
||||
expires_in_secs: welcome.expires_in_secs,
|
||||
|
||||
@@ -55,6 +55,18 @@ pub const USER_FLAG_CHUNK_ALIGNED: u32 = 0x40;
|
||||
/// know this contract.
|
||||
pub const USER_FLAG_SLICE_STREAM: u32 = 0x80;
|
||||
|
||||
/// `user_flags` bit: this AU is a host-side **repeat** — the encode loop re-encoded a held
|
||||
/// frame because the source produced nothing new (the idle keepalive), so it carries no new
|
||||
/// content. The client's ABR uses it to tell an idle window from an active one (ABR overhaul
|
||||
/// RFC §4.1): repeat-heavy windows must neither train the latency baselines nor read as "the
|
||||
/// target wasn't utilized" — that is how a static stretch used to poison the controller
|
||||
/// against the first moment of motion. Purely informational (no wire-shape change), so it is
|
||||
/// set unconditionally; receivers that predate it ignore the bit, and a client only TRUSTS
|
||||
/// its absence as "active frame" when the host advertised
|
||||
/// [`HOST_CAP2_REPEAT_MARK`](crate::quic::HOST_CAP2_REPEAT_MARK) — against an older host,
|
||||
/// zero repeat flags means "unknown", not "all active".
|
||||
pub const USER_FLAG_REPEAT: u32 = 0x100;
|
||||
|
||||
/// Widest lost-frame range (frames, wrapping `last - first`) a reference-frame-invalidation
|
||||
/// recovery may be asked to repair; anything wider goes straight to the keyframe path on BOTH
|
||||
/// ends. RFI can only re-reference history the encoder still holds — NVENC keeps a 5-frame DPB,
|
||||
|
||||
@@ -210,6 +210,18 @@ pub const CLIENT_CAP_AUDIO_HIRES: u8 = 0x10;
|
||||
/// `0x01`/`0x02` are gamepad-state / clipboard.
|
||||
pub const HOST_CAP_AUDIO_HIRES: u8 = 0x80;
|
||||
|
||||
/// [`Welcome::host_caps2`](crate::quic::Welcome::host_caps2) bit — the second capability byte
|
||||
/// the `0x80` wall above predicted, delivered as a trailing Welcome field (absent → `0`, the
|
||||
/// same append discipline as every field since `compositor`; no ABI bump needed).
|
||||
///
|
||||
/// The host marks its idle-keepalive re-encodes with
|
||||
/// [`USER_FLAG_REPEAT`](crate::packet::USER_FLAG_REPEAT), so the client's ABR can tell an
|
||||
/// idle window from an active one (ABR overhaul RFC §4.1). The bit is what makes the flag's
|
||||
/// ABSENCE meaningful: against a host that advertises it, an unflagged AU is genuinely new
|
||||
/// content; against an older host the client must treat activity as unknown and keep the
|
||||
/// legacy window arithmetic.
|
||||
pub const HOST_CAP2_REPEAT_MARK: u8 = 0x01;
|
||||
|
||||
/// [`Hello::video_codecs`] bit: the client can decode H.264 / AVC. The GPU-less **software**
|
||||
/// encode path (openh264) emits H.264, so a client that wants to stream from a software host MUST
|
||||
/// advertise this.
|
||||
@@ -386,6 +398,7 @@ mod tests {
|
||||
audio_rate_hz: SAMPLE_RATE_HZ,
|
||||
audio_bits: BITS_16,
|
||||
audio_frame_us: 0,
|
||||
host_caps2: 0,
|
||||
};
|
||||
let got = Welcome::decode(&w.encode()).unwrap();
|
||||
assert_eq!(got.host_caps & HOST_CAP_CLIPBOARD, HOST_CAP_CLIPBOARD);
|
||||
|
||||
@@ -371,8 +371,16 @@ pub struct Welcome {
|
||||
/// settled or it sizes the whole session against the conservative initial value
|
||||
/// (`design/hi-res-audio.md` §4.2).
|
||||
///
|
||||
/// 2 LE bytes at 85..87 (or 117..119) — the last field of the message. Absent → `0`.
|
||||
/// 2 LE bytes at 85..87 (or 117..119). Absent → `0`.
|
||||
pub audio_frame_us: u16,
|
||||
/// The second host-capability byte — the `0x80` wall in `host_caps` predicted it; see
|
||||
/// [`HOST_CAP2_REPEAT_MARK`](super::HOST_CAP2_REPEAT_MARK) (currently its only bit).
|
||||
/// Emitting it (nonzero) extends the placeholder chain one link past the audio block, so
|
||||
/// an Opus session then carries the audio block as its own defaults — every placeholder
|
||||
/// decodes to exactly what its absence meant, the discipline every link of the chain keeps.
|
||||
///
|
||||
/// One byte at 87 (or 119) — the last field of the message. Absent (an older host) → `0`.
|
||||
pub host_caps2: u8,
|
||||
}
|
||||
|
||||
/// `client → host`: data plane is bound, begin streaming.
|
||||
@@ -707,27 +715,40 @@ impl Welcome {
|
||||
// and the client would open the wrong plane. Conversely an Opus session never emits the
|
||||
// block whatever the other three say, so today's Welcome stays byte-identical — the
|
||||
// interop guarantee the cipher byte bought and every link since has had to keep.
|
||||
// …and `host_caps2` extends the chain one link past the audio block (same rule: its
|
||||
// presence forces every earlier placeholder, each of which encodes exactly what its
|
||||
// absence decodes to — an Opus session forced to emit the audio block writes
|
||||
// `AUDIO_CODEC_OPUS` + zeros, which every shipped client reads as the defaults).
|
||||
let mgmt_present = self.mgmt_port != 0;
|
||||
let access_present = self.grants != super::access::GRANT_ALL || self.expires_in_secs != 0;
|
||||
let audio_present = self.audio_codec != AUDIO_CODEC_OPUS;
|
||||
if self.cipher != CIPHER_AES_128_GCM || mgmt_present || access_present || audio_present {
|
||||
let caps2_present = self.host_caps2 != 0;
|
||||
if self.cipher != CIPHER_AES_128_GCM
|
||||
|| mgmt_present
|
||||
|| access_present
|
||||
|| audio_present
|
||||
|| caps2_present
|
||||
{
|
||||
b.push(self.cipher);
|
||||
if let Some(k) = &self.key_chacha {
|
||||
b.extend_from_slice(k);
|
||||
}
|
||||
if mgmt_present || access_present || audio_present {
|
||||
if mgmt_present || access_present || audio_present || caps2_present {
|
||||
b.extend_from_slice(&self.mgmt_port.to_le_bytes());
|
||||
}
|
||||
if access_present || audio_present {
|
||||
if access_present || audio_present || caps2_present {
|
||||
b.extend_from_slice(&self.grants.to_le_bytes());
|
||||
b.extend_from_slice(&self.expires_in_secs.to_le_bytes());
|
||||
}
|
||||
if audio_present {
|
||||
if audio_present || caps2_present {
|
||||
b.push(self.audio_codec);
|
||||
b.extend_from_slice(&self.audio_rate_hz.to_le_bytes());
|
||||
b.push(self.audio_bits);
|
||||
b.extend_from_slice(&self.audio_frame_us.to_le_bytes());
|
||||
}
|
||||
if caps2_present {
|
||||
b.push(self.host_caps2);
|
||||
}
|
||||
}
|
||||
b
|
||||
}
|
||||
@@ -834,6 +855,10 @@ impl Welcome {
|
||||
.get(audio_off + 6..audio_off + 8)
|
||||
.map(|s| u16::from_le_bytes(s.try_into().unwrap()))
|
||||
.unwrap_or(0);
|
||||
// The second capability byte trails the audio block — 87 for AES, 119 behind a ChaCha
|
||||
// key. Absent (an older host) → 0: no repeat marking, and the client's ABR keeps its
|
||||
// legacy window arithmetic.
|
||||
let host_caps2 = b.get(audio_off + 8).copied().unwrap_or(0);
|
||||
Ok(Welcome {
|
||||
abi_version: u32at(4),
|
||||
udp_port: u16at(8),
|
||||
@@ -910,6 +935,7 @@ impl Welcome {
|
||||
audio_rate_hz,
|
||||
audio_bits,
|
||||
audio_frame_us,
|
||||
host_caps2,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1005,6 +1031,7 @@ mod tests {
|
||||
audio_rate_hz: SAMPLE_RATE_HZ,
|
||||
audio_bits: BITS_16,
|
||||
audio_frame_us: 0,
|
||||
host_caps2: 0,
|
||||
};
|
||||
assert_eq!(Welcome::decode(&w.encode()).unwrap(), w);
|
||||
|
||||
@@ -1077,6 +1104,7 @@ mod tests {
|
||||
audio_rate_hz: SAMPLE_RATE_HZ,
|
||||
audio_bits: BITS_16,
|
||||
audio_frame_us: 0,
|
||||
host_caps2: 0,
|
||||
};
|
||||
// An AES session's Welcome is byte-identical to the pre-cipher wire form (68 bytes) —
|
||||
// the old-client × new-host interop guarantee.
|
||||
@@ -1334,6 +1362,7 @@ mod tests {
|
||||
audio_rate_hz: SAMPLE_RATE_HZ,
|
||||
audio_bits: BITS_16,
|
||||
audio_frame_us: 0,
|
||||
host_caps2: 0,
|
||||
}
|
||||
.encode(),
|
||||
)
|
||||
@@ -1417,6 +1446,7 @@ mod tests {
|
||||
audio_rate_hz: SAMPLE_RATE_HZ,
|
||||
audio_bits: BITS_16,
|
||||
audio_frame_us: 0,
|
||||
host_caps2: 0,
|
||||
}
|
||||
.encode(),
|
||||
)
|
||||
@@ -1539,6 +1569,7 @@ mod tests {
|
||||
audio_rate_hz: SAMPLE_RATE_HZ,
|
||||
audio_bits: BITS_16,
|
||||
audio_frame_us: 0,
|
||||
host_caps2: 0,
|
||||
};
|
||||
let wenc = w.encode();
|
||||
assert_eq!(wenc.len(), 68); // 60 base + 4 colour + chroma + audio-channels + codec + host-caps
|
||||
@@ -2013,6 +2044,7 @@ mod tests {
|
||||
audio_rate_hz: SAMPLE_RATE_HZ,
|
||||
audio_bits: BITS_16,
|
||||
audio_frame_us: 0,
|
||||
host_caps2: 0,
|
||||
};
|
||||
// ── The interop floor: a plain Opus session is byte-identical to the pre-hi-res wire ──
|
||||
//
|
||||
@@ -2174,6 +2206,83 @@ mod tests {
|
||||
assert_eq!(Welcome::decode(&odd_rate).unwrap().audio_rate_hz, 44_100);
|
||||
}
|
||||
|
||||
/// `host_caps2` — the link past the audio block (ABR overhaul RFC §4.1): its presence
|
||||
/// forces every earlier placeholder, each of which must decode to exactly what its
|
||||
/// absence meant; absent (an older host) → 0.
|
||||
#[test]
|
||||
fn welcome_host_caps2_wire_under_both_ciphers() {
|
||||
let base = Welcome {
|
||||
abi_version: 1,
|
||||
udp_port: 1,
|
||||
mode: Mode {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
refresh_hz: 60,
|
||||
},
|
||||
fec: FecConfig {
|
||||
scheme: FecScheme::Gf16,
|
||||
fec_percent: 10,
|
||||
max_data_per_block: 4096,
|
||||
},
|
||||
shard_payload: 1408,
|
||||
encrypt: true,
|
||||
key: [7; 16],
|
||||
salt: [3; 4],
|
||||
frames: 0,
|
||||
compositor: CompositorPref::Auto,
|
||||
gamepad: GamepadPref::Auto,
|
||||
bitrate_kbps: 20_000,
|
||||
bit_depth: 8,
|
||||
color: ColorInfo::SDR_BT709,
|
||||
chroma_format: CHROMA_IDC_420,
|
||||
audio_channels: 2,
|
||||
codec: CODEC_HEVC,
|
||||
host_caps: HOST_CAP_GAMEPAD_STATE,
|
||||
mgmt_port: 0,
|
||||
grants: super::super::access::GRANT_ALL,
|
||||
expires_in_secs: 0,
|
||||
cipher: CIPHER_AES_128_GCM,
|
||||
key_chacha: None,
|
||||
audio_codec: AUDIO_CODEC_OPUS,
|
||||
audio_rate_hz: SAMPLE_RATE_HZ,
|
||||
audio_bits: BITS_16,
|
||||
audio_frame_us: 0,
|
||||
host_caps2: 0,
|
||||
};
|
||||
// Zero = not emitted: the plain Welcome keeps the 68-byte interop floor.
|
||||
assert_eq!(base.encode().len(), 68);
|
||||
|
||||
// Set, over AES: the whole chain is forced as placeholders and the byte lands at 87.
|
||||
let marked = Welcome {
|
||||
host_caps2: HOST_CAP2_REPEAT_MARK,
|
||||
..base
|
||||
};
|
||||
let enc = marked.encode();
|
||||
assert_eq!(enc.len(), 88);
|
||||
let got = Welcome::decode(&enc).unwrap();
|
||||
assert_eq!(got, marked);
|
||||
// The forced placeholders decode to exactly what absence meant — an Opus session with
|
||||
// caps2 must not read as hi-res, granted-nothing, or a mgmt port.
|
||||
assert_eq!(got.audio_codec, AUDIO_CODEC_OPUS);
|
||||
assert_eq!(got.audio_rate_hz, SAMPLE_RATE_HZ);
|
||||
assert_eq!(got.grants, super::super::access::GRANT_ALL);
|
||||
assert_eq!(got.mgmt_port, 0);
|
||||
|
||||
// Behind a ChaCha key the byte shifts with the rest of the tail (119, total 120).
|
||||
let chacha = Welcome {
|
||||
cipher: CIPHER_CHACHA20_POLY1305,
|
||||
key_chacha: Some([9; 32]),
|
||||
host_caps2: HOST_CAP2_REPEAT_MARK,
|
||||
..base
|
||||
};
|
||||
let enc = chacha.encode();
|
||||
assert_eq!(enc.len(), 120);
|
||||
assert_eq!(Welcome::decode(&enc).unwrap(), chacha);
|
||||
|
||||
// An older host's shorter wire decodes to 0 — "no repeat marking", never an error.
|
||||
assert_eq!(Welcome::decode(&base.encode()).unwrap().host_caps2, 0);
|
||||
}
|
||||
|
||||
/// The hi-res audio request on `Hello` (`design/hi-res-audio.md` §7) — the placeholder
|
||||
/// discipline, the post-`display_hdr` byte budget, and the legacy defaults in both
|
||||
/// directions.
|
||||
|
||||
@@ -1073,6 +1073,10 @@ pub(super) async fn negotiate(
|
||||
audio_rate_hz: audio_plane.rate_hz,
|
||||
audio_bits: audio_plane.bits,
|
||||
audio_frame_us: audio_plane.frame_us,
|
||||
// This host marks its idle-keepalive re-encodes with USER_FLAG_REPEAT (ABR overhaul
|
||||
// RFC §4.1) — the bit is what lets the client's ABR trust an unflagged AU as new
|
||||
// content.
|
||||
host_caps2: punktfunk_core::quic::HOST_CAP2_REPEAT_MARK,
|
||||
};
|
||||
io::write_msg(send, &welcome.encode()).await?;
|
||||
bringup.mark("welcome");
|
||||
|
||||
@@ -735,6 +735,12 @@ fn handle_chunk(
|
||||
punktfunk_core::packet::USER_FLAG_SLICE_STREAM
|
||||
} else {
|
||||
0
|
||||
}
|
||||
// Same repeat marking as the whole-frame path (RFC §4.1).
|
||||
| if c.repeat {
|
||||
punktfunk_core::packet::USER_FLAG_REPEAT
|
||||
} else {
|
||||
0
|
||||
};
|
||||
*open = Some(StreamedOpen {
|
||||
au: session
|
||||
@@ -991,7 +997,16 @@ fn send_loop(
|
||||
&mut session,
|
||||
&msg.data,
|
||||
msg.capture_ns,
|
||||
msg.flags,
|
||||
// Repeats are marked on the wire (USER_FLAG_REPEAT, RFC §4.1) so the
|
||||
// client's ABR can tell an idle window from an active one — the
|
||||
// Welcome advertised HOST_CAP2_REPEAT_MARK, which is what makes the
|
||||
// bit's absence mean "new content".
|
||||
msg.flags
|
||||
| if msg.repeat {
|
||||
punktfunk_core::packet::USER_FLAG_REPEAT
|
||||
} else {
|
||||
0
|
||||
},
|
||||
msg.frame_index,
|
||||
msg.deadline,
|
||||
burst_cap,
|
||||
|
||||
@@ -46,9 +46,11 @@ Mac you type one in, and Apple TV's rate rides with the Stream mode preset.
|
||||
|
||||
**Bitrate** — *default: Automatic.* For H.264, HEVC and AV1, Automatic means the host's default,
|
||||
**20 Mbps**, plus two things an explicit rate switches off: adaptive bitrate, and a link-capacity
|
||||
probe about two seconds in that lets the rate climb past 20 Mbps. An explicit rate is fixed for the
|
||||
session, clamped to **500 kbps – 8 Gbps**. A host card's menu has **Test network speed…** to suggest
|
||||
a value.
|
||||
probe about two seconds in that lets the rate climb past 20 Mbps. Automatic never descends below
|
||||
**2 Mbps** — on a link that thin the picture goes visibly soft rather than lossy, and the client
|
||||
logs a one-line warning the first time it happens. An explicit rate is fixed for the session,
|
||||
clamped to **500 kbps – 8 Gbps**. A host card's menu has **Test network speed…** to suggest a
|
||||
value.
|
||||
|
||||
PyroWave is **always Automatic**: the rate is a fixed per-pixel budget for the negotiated mode
|
||||
(hundreds of Mbps), with adaptive bitrate and the probe off for the whole session. A fixed kbps
|
||||
|
||||
@@ -730,6 +730,18 @@
|
||||
// know this contract.
|
||||
#define PUNKTFUNK_USER_FLAG_SLICE_STREAM 128
|
||||
|
||||
// `user_flags` bit: this AU is a host-side **repeat** — the encode loop re-encoded a held
|
||||
// frame because the source produced nothing new (the idle keepalive), so it carries no new
|
||||
// content. The client's ABR uses it to tell an idle window from an active one (ABR overhaul
|
||||
// RFC §4.1): repeat-heavy windows must neither train the latency baselines nor read as "the
|
||||
// target wasn't utilized" — that is how a static stretch used to poison the controller
|
||||
// against the first moment of motion. Purely informational (no wire-shape change), so it is
|
||||
// set unconditionally; receivers that predate it ignore the bit, and a client only TRUSTS
|
||||
// its absence as "active frame" when the host advertised
|
||||
// [`HOST_CAP2_REPEAT_MARK`](crate::quic::HOST_CAP2_REPEAT_MARK) — against an older host,
|
||||
// zero repeat flags means "unknown", not "all active".
|
||||
#define PUNKTFUNK_USER_FLAG_REPEAT 256
|
||||
|
||||
// Widest lost-frame range (frames, wrapping `last - first`) a reference-frame-invalidation
|
||||
// recovery may be asked to repair; anything wider goes straight to the keyframe path on BOTH
|
||||
// ends. RFI can only re-reference history the encoder still holds — NVENC keeps a 5-frame DPB,
|
||||
@@ -1082,6 +1094,20 @@
|
||||
#define PUNKTFUNK_HOST_CAP_AUDIO_HIRES 128
|
||||
#endif
|
||||
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// [`Welcome::host_caps2`](crate::quic::Welcome::host_caps2) bit — the second capability byte
|
||||
// the `0x80` wall above predicted, delivered as a trailing Welcome field (absent → `0`, the
|
||||
// same append discipline as every field since `compositor`; no ABI bump needed).
|
||||
//
|
||||
// The host marks its idle-keepalive re-encodes with
|
||||
// [`USER_FLAG_REPEAT`](crate::packet::USER_FLAG_REPEAT), so the client's ABR can tell an
|
||||
// idle window from an active one (ABR overhaul RFC §4.1). The bit is what makes the flag's
|
||||
// ABSENCE meaningful: against a host that advertises it, an unflagged AU is genuinely new
|
||||
// content; against an older host the client must treat activity as unknown and keep the
|
||||
// legacy window arithmetic.
|
||||
#define PUNKTFUNK_HOST_CAP2_REPEAT_MARK 1
|
||||
#endif
|
||||
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// [`Hello::video_codecs`] bit: the client can decode H.264 / AVC. The GPU-less **software**
|
||||
// encode path (openh264) emits H.264, so a client that wants to stream from a software host MUST
|
||||
|
||||
@@ -142,6 +142,7 @@ PUNKTFUNK_HOST_CAP_AUDIO_RED
|
||||
PUNKTFUNK_HOST_CAP_CLIPBOARD
|
||||
PUNKTFUNK_HOST_CAP_CURSOR
|
||||
PUNKTFUNK_HOST_CAP_GAMEPAD_STATE
|
||||
PUNKTFUNK_HOST_CAP2_REPEAT_MARK
|
||||
PUNKTFUNK_HOST_CAP_PEN
|
||||
PUNKTFUNK_HOST_CAP_TEXT_INPUT
|
||||
PUNKTFUNK_HOST_TIMING_MAGIC
|
||||
@@ -300,6 +301,7 @@ PUNKTFUNK_USBIP_TRACE
|
||||
PUNKTFUNK_USER_FLAG_CHUNK_ALIGNED
|
||||
PUNKTFUNK_USER_FLAG_RECOVERY_ANCHOR
|
||||
PUNKTFUNK_USER_FLAG_RECOVERY_POINT
|
||||
PUNKTFUNK_USER_FLAG_REPEAT
|
||||
PUNKTFUNK_USER_FLAG_SLICE_STREAM
|
||||
PUNKTFUNK_VAAPI_ASYNC_DEPTH
|
||||
PUNKTFUNK_VBV_FRAMES
|
||||
|
||||
Reference in New Issue
Block a user