fix(host): forward reconfigure_bitrate through TrackedEncoder + probe --rebitrate validator
ci / web (push) Successful in 56s
ci / docs-site (push) Successful in 1m5s
decky / build-publish (push) Successful in 21s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
arch / build-publish (push) Successful in 13m33s
android / android (push) Successful in 14m55s
ci / bench (push) Successful in 5m19s
ci / rust (push) Successful in 16m59s
flatpak / build-publish (push) Failing after 8m7s
apple / swift (push) Successful in 4m30s
deb / build-publish (push) Successful in 11m24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m31s
docker / deploy-docs (push) Successful in 25s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m8s
windows-host / package (push) Successful in 8m14s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m3s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m46s
release / apple (push) Successful in 24m38s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m12s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m56s
apple / screenshots (push) Successful in 21m24s
ci / web (push) Successful in 56s
ci / docs-site (push) Successful in 1m5s
decky / build-publish (push) Successful in 21s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
arch / build-publish (push) Successful in 13m33s
android / android (push) Successful in 14m55s
ci / bench (push) Successful in 5m19s
ci / rust (push) Successful in 16m59s
flatpak / build-publish (push) Failing after 8m7s
apple / swift (push) Successful in 4m30s
deb / build-publish (push) Successful in 11m24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m31s
docker / deploy-docs (push) Successful in 25s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m8s
windows-host / package (push) Successful in 8m14s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m3s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m46s
release / apple (push) Successful in 24m38s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m12s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m56s
apple / screenshots (push) Successful in 21m24s
The gpu-session TrackedEncoder wrapper delegates every Encoder method by hand, so the new reconfigure_bitrate fell through to the trait's false default and EVERY bitrate change silently took the rebuild+IDR path — the live .21 A/B caught it (host log said 'rebuilt', never 'in place'). Also: - punktfunk-probe --rebitrate KBPS:SECS — headless mid-stream SetBitrate validator (cursor-wiggles so a damage-driven idle desktop keeps publishing frames through the switch). Live-verified on .21: one NVENC session open, then 'encoder bitrate reconfigured in place (adaptive bitrate — no IDR)' at 20→60 Mbps. - on-hardware nvenc_cuda reconfigure smoke test (20→60→10 Mbps in place, zero IDRs — green on the RTX 5070 Ti). - BitrateChanged doc no longer claims the switch costs an IDR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,7 @@
|
|||||||
//! exits without connecting.
|
//! exits without connecting.
|
||||||
//!
|
//!
|
||||||
//! Usage: `punktfunk-probe [--connect HOST:PORT] [--mode WxHxFPS] [--remode WxHxFPS:SECS]
|
//! Usage: `punktfunk-probe [--connect HOST:PORT] [--mode WxHxFPS] [--remode WxHxFPS:SECS]
|
||||||
|
//! [--rebitrate KBPS:SECS]
|
||||||
//! [--out FILE] [--bitrate KBPS] [--codec auto|h264|hevc|av1] [--audio-channels 2|6|8]
|
//! [--out FILE] [--bitrate KBPS] [--codec auto|h264|hevc|av1] [--audio-channels 2|6|8]
|
||||||
//! [--launch APP] [--name NAME] [--speed-test KBPS:MS]
|
//! [--launch APP] [--name NAME] [--speed-test KBPS:MS]
|
||||||
//! [--input-test | --mic-test [--mic-burst] | --touch-test | --rich-input-test]
|
//! [--input-test | --mic-test [--mic-burst] | --touch-test | --rich-input-test]
|
||||||
@@ -51,8 +52,8 @@ use punktfunk_core::config::Role;
|
|||||||
use punktfunk_core::input::{InputEvent, InputKind};
|
use punktfunk_core::input::{InputEvent, InputKind};
|
||||||
use punktfunk_core::packet::FLAG_PROBE;
|
use punktfunk_core::packet::FLAG_PROBE;
|
||||||
use punktfunk_core::quic::{
|
use punktfunk_core::quic::{
|
||||||
endpoint, io, window_loss_ppm, Hello, LossReport, ProbeRequest, ProbeResult, Reconfigure,
|
endpoint, io, window_loss_ppm, BitrateChanged, Hello, LossReport, ProbeRequest, ProbeResult,
|
||||||
Reconfigured, RequestKeyframe, Start, Welcome,
|
Reconfigure, Reconfigured, RequestKeyframe, SetBitrate, Start, Welcome,
|
||||||
};
|
};
|
||||||
use punktfunk_core::transport::UdpTransport;
|
use punktfunk_core::transport::UdpTransport;
|
||||||
use punktfunk_core::{CompositorPref, Mode, PunktfunkError, Session};
|
use punktfunk_core::{CompositorPref, Mode, PunktfunkError, Session};
|
||||||
@@ -84,6 +85,11 @@ struct Args {
|
|||||||
pin: Option<[u8; 32]>,
|
pin: Option<[u8; 32]>,
|
||||||
/// `--remode WxHxFPS:SECS` — request this mode SECS seconds into the stream.
|
/// `--remode WxHxFPS:SECS` — request this mode SECS seconds into the stream.
|
||||||
remode: Option<(Mode, u32)>,
|
remode: Option<(Mode, u32)>,
|
||||||
|
/// `--rebitrate KBPS:SECS` — send a mid-stream [`SetBitrate`] (the adaptive-bitrate control
|
||||||
|
/// message) SECS seconds into the stream: the headless validator for the host's in-place
|
||||||
|
/// encoder rate retarget (Phase 3.2) / rebuild fallback. Wiggles the cursor around the switch
|
||||||
|
/// so a damage-driven idle desktop actually publishes frames through it.
|
||||||
|
rebitrate: Option<(u32, u32)>,
|
||||||
/// `--pair PIN` — run the pairing ceremony instead of a session.
|
/// `--pair PIN` — run the pairing ceremony instead of a session.
|
||||||
pair: Option<String>,
|
pair: Option<String>,
|
||||||
/// `--name LABEL` — how the host labels this client when pairing.
|
/// `--name LABEL` — how the host labels this client when pairing.
|
||||||
@@ -201,6 +207,10 @@ fn parse_args() -> Args {
|
|||||||
let (m, secs) = s.split_once(':')?;
|
let (m, secs) = s.split_once(':')?;
|
||||||
Some((parse_mode(m)?, secs.parse().ok()?))
|
Some((parse_mode(m)?, secs.parse().ok()?))
|
||||||
});
|
});
|
||||||
|
let rebitrate = get("--rebitrate").and_then(|s| {
|
||||||
|
let (kbps, secs) = s.split_once(':')?;
|
||||||
|
Some((kbps.parse().ok()?, secs.parse().ok()?))
|
||||||
|
});
|
||||||
// A present-but-malformed --pin must abort, not silently downgrade to trust-on-first-use
|
// A present-but-malformed --pin must abort, not silently downgrade to trust-on-first-use
|
||||||
// (the user asked for verification; fail closed).
|
// (the user asked for verification; fail closed).
|
||||||
let pin = match get("--pin") {
|
let pin = match get("--pin") {
|
||||||
@@ -252,6 +262,7 @@ fn parse_args() -> Args {
|
|||||||
seconds: get("--seconds").and_then(|s| s.parse().ok()),
|
seconds: get("--seconds").and_then(|s| s.parse().ok()),
|
||||||
pin,
|
pin,
|
||||||
remode,
|
remode,
|
||||||
|
rebitrate,
|
||||||
pair: get("--pair").map(String::from),
|
pair: get("--pair").map(String::from),
|
||||||
name: get("--name").unwrap_or("punktfunk-probe").to_string(),
|
name: get("--name").unwrap_or("punktfunk-probe").to_string(),
|
||||||
compositor,
|
compositor,
|
||||||
@@ -629,6 +640,64 @@ async fn session(args: Args) -> Result<()> {
|
|||||||
other => tracing::error!(?other, "bad Reconfigured"),
|
other => tracing::error!(?other, "bad Reconfigured"),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else if let Some((new_kbps, after_secs)) = args.rebitrate {
|
||||||
|
// Mid-stream adaptive-bitrate test: after a delay, send the SetBitrate the Automatic
|
||||||
|
// controller would and await the host's BitrateChanged ack. Host-side this exercises the
|
||||||
|
// in-place `reconfigure_bitrate` (no IDR) or the rebuild fallback — the host log says
|
||||||
|
// which. The cursor wiggle keeps a damage-driven idle desktop publishing frames through
|
||||||
|
// the whole window: the encode loop only drains bitrate requests between frames, and the
|
||||||
|
// post-switch AUs are what prove the stream carried on.
|
||||||
|
let mut rs = send;
|
||||||
|
let mut rr = recv;
|
||||||
|
let conn2 = conn.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let wiggle = |i: u32| InputEvent {
|
||||||
|
kind: InputKind::MouseMove,
|
||||||
|
_pad: [0; 3],
|
||||||
|
code: 0,
|
||||||
|
x: if i % 2 == 0 { 2 } else { -2 },
|
||||||
|
y: 0,
|
||||||
|
flags: 0,
|
||||||
|
};
|
||||||
|
let end =
|
||||||
|
std::time::Instant::now() + std::time::Duration::from_secs(after_secs as u64 + 6);
|
||||||
|
let switch_at =
|
||||||
|
std::time::Instant::now() + std::time::Duration::from_secs(after_secs as u64);
|
||||||
|
let mut sent = false;
|
||||||
|
let mut i = 0u32;
|
||||||
|
while std::time::Instant::now() < end {
|
||||||
|
let _ = conn2.send_datagram(wiggle(i).encode().to_vec().into());
|
||||||
|
i += 1;
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||||
|
if !sent && std::time::Instant::now() >= switch_at {
|
||||||
|
sent = true;
|
||||||
|
tracing::info!(new_kbps, "requesting mid-stream bitrate change");
|
||||||
|
if io::write_msg(
|
||||||
|
&mut rs,
|
||||||
|
&SetBitrate {
|
||||||
|
bitrate_kbps: new_kbps,
|
||||||
|
}
|
||||||
|
.encode(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
tracing::error!("SetBitrate write failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match io::read_msg(&mut rr)
|
||||||
|
.await
|
||||||
|
.map(|b| BitrateChanged::decode(&b))
|
||||||
|
{
|
||||||
|
Ok(Ok(ack)) => tracing::info!(
|
||||||
|
applied_kbps = ack.bitrate_kbps,
|
||||||
|
"BITRATE CHANGE acked by host"
|
||||||
|
),
|
||||||
|
other => tracing::error!(?other, "bad BitrateChanged"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
} else if let Some((target_kbps, duration_ms)) = args.speed_test {
|
} else if let Some((target_kbps, duration_ms)) = args.speed_test {
|
||||||
// Bandwidth probe: after the stream warms up, ask the host to burst FLAG_PROBE filler; measure
|
// Bandwidth probe: after the stream warms up, ask the host to burst FLAG_PROBE filler; measure
|
||||||
// delivered WIRE packets (session-stat delta) vs. what the host reports putting on the wire.
|
// delivered WIRE packets (session-stat delta) vs. what the host reports putting on the wire.
|
||||||
|
|||||||
@@ -415,9 +415,10 @@ pub struct SetBitrate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// `host → client`: answer to [`SetBitrate`] — the bitrate the host actually configured (the
|
/// `host → client`: answer to [`SetBitrate`] — the bitrate the host actually configured (the
|
||||||
/// request clamped to its supported band). The encoder switches on the next frame (an IDR); the
|
/// request clamped to its supported band). The encoder retargets in place where the backend can
|
||||||
/// stream never pauses. Also the controller's liveness signal: no answer ⇒ an old host that
|
/// (no IDR — the stream carries straight on); a backend without in-place reconfigure rebuilds and
|
||||||
/// doesn't renegotiate bitrate.
|
/// switches on the next frame (an IDR). The stream never pauses either way. Also the controller's
|
||||||
|
/// liveness signal: no answer ⇒ an old host that doesn't renegotiate bitrate.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub struct BitrateChanged {
|
pub struct BitrateChanged {
|
||||||
pub bitrate_kbps: u32,
|
pub bitrate_kbps: u32,
|
||||||
|
|||||||
@@ -476,6 +476,9 @@ impl Encoder for TrackedEncoder {
|
|||||||
fn reset(&mut self) -> bool {
|
fn reset(&mut self) -> bool {
|
||||||
self.inner.reset()
|
self.inner.reset()
|
||||||
}
|
}
|
||||||
|
fn reconfigure_bitrate(&mut self, bps: u64) -> bool {
|
||||||
|
self.inner.reconfigure_bitrate(bps)
|
||||||
|
}
|
||||||
fn flush(&mut self) -> Result<()> {
|
fn flush(&mut self) -> Result<()> {
|
||||||
self.inner.flush()
|
self.inner.flush()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user