feat(host): in-place encoder rate reconfigure — ABR steps no longer cost an IDR
ci / web (push) Successful in 56s
ci / docs-site (push) Successful in 1m8s
ci / bench (push) Successful in 5m25s
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 9s
arch / build-publish (push) Successful in 11m11s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 41s
android / android (push) Successful in 14m53s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6m24s
deb / build-publish (push) Successful in 11m50s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m5s
ci / rust (push) Successful in 23m4s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m54s
docker / deploy-docs (push) Successful in 22s
apple / swift (push) Successful in 4m31s
windows-host / package (push) Successful in 14m46s
apple / screenshots (push) Successful in 18m26s

Every adaptive-bitrate step used to tear the encoder down and rebuild it,
opening on a full IDR (a 20-40x frame-size spike, in-flight AU forfeit and
an IDR-cooldown anchor) — exactly when the Automatic controller is climbing.
Encoder::reconfigure_bitrate(bps) retargets the LIVE encoder instead
(default false, so libavcodec/software paths keep the rebuild fallback,
which also still owns the bitrate clamping):

- Linux + Windows direct NVENC: nvEncReconfigureEncoder (added to the
  hand-rolled runtime EncodeApi tables) with resetEncoder=0 / forceIDR=0;
  the same init/config is re-authored via the new shared build_config/
  build_init_params with only avg/max bitrate + VBV (PUNKTFUNK_VBV_FRAMES)
  moved. On-hardware test: 20→60→10 Mbps in place, zero IDRs (RTX 5070 Ti).
- Native AMF: TargetBitrate/PeakBitrate/VBVBufferSize are dynamic
  properties — SetProperty on the live component, no Terminate/re-Init.
- Vulkan Video (HEVC + AV1): stage the rate and emit an
  ENCODE_RATE_CONTROL control command on the next recorded frame (begin
  keeps declaring the session's current state, as the spec requires).

The session glue tries the in-place retarget first and skips the rebuild/
inflight-clear/IDR-cooldown bookkeeping when it succeeds — the reference
chain and the wire-index prediction survive, so RFI keeps working across
rate steps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 19:53:18 +02:00
parent 46b7ffc001
commit a1af916e38
6 changed files with 478 additions and 93 deletions
+56 -36
View File
@@ -4274,47 +4274,67 @@ fn virtual_stream(ctx: SessionContext) -> Result<()> {
}
}
// Adaptive bitrate: drain to the NEWEST requested rate (the client's controller may step
// several times while we stream) and rebuild the ENCODER ONLY in place — the mode didn't
// change, so capture and the virtual output are untouched and the switch costs exactly the
// IDR the fresh encoder opens with (the same resync discipline as a mode switch, minus the
// pipeline churn). Rates arrive pre-clamped by the control task (`resolve_bitrate_kbps`).
// several times while we stream) and retarget the ENCODER ONLY — the mode didn't change,
// so capture and the virtual output are untouched. Preferred lever: an IN-PLACE
// `reconfigure_bitrate` (Phase 3.2 — NVENC nvEncReconfigureEncoder / AMF dynamic props /
// Vulkan RC control), which keeps the encoder, its reference chain and the in-flight AUs,
// so the step costs NOTHING on the wire (no IDR, no forfeit — exactly what the Automatic
// controller's doubling climb wants). A backend that can't (libavcodec paths) or a driver
// rejection falls back to the full rebuild, which costs the IDR the fresh encoder opens
// with (the same resync discipline as a mode switch, minus the pipeline churn) and owns
// the bitrate clamping. Rates arrive pre-clamped by the control task
// (`resolve_bitrate_kbps`).
let mut want_kbps = None;
while let Ok(k) = bitrate_rx.try_recv() {
want_kbps = Some(k);
}
if let Some(new_kbps) = want_kbps.filter(|&k| k != bitrate_kbps) {
// `interval` was built as 1/effective_hz, so the round-trip recovers the integer rate.
let hz = interval_hz(interval);
match crate::encode::open_video(
plan.codec,
frame.format,
frame.width,
frame.height,
hz,
new_kbps as u64 * 1000,
frame.is_cuda(),
bit_depth,
plan.chroma,
) {
Ok(new_enc) => {
tracing::info!(
from_kbps = bitrate_kbps,
to_kbps = new_kbps,
"encoder rebuilt at new bitrate (adaptive bitrate)"
);
enc = new_enc;
bitrate_kbps = new_kbps;
live_bitrate.store(new_kbps, Ordering::Relaxed);
// The owed AUs died with the old encoder — same bookkeeping as a mode-switch
// rebuild; the fresh encoder opens on an IDR, so anchor the IDR cooldown too.
inflight.clear();
last_au_at = std::time::Instant::now();
encoder_resets = 0;
last_forced_idr = Some(std::time::Instant::now());
}
Err(e) => {
tracing::error!(error = %format!("{e:#}"), to_kbps = new_kbps,
"bitrate-change encoder rebuild failed — keeping the current rate");
if enc.reconfigure_bitrate(new_kbps as u64 * 1000) {
tracing::info!(
from_kbps = bitrate_kbps,
to_kbps = new_kbps,
"encoder bitrate reconfigured in place (adaptive bitrate — no IDR)"
);
bitrate_kbps = new_kbps;
live_bitrate.store(new_kbps, Ordering::Relaxed);
// Same encoder, same stream: the in-flight AUs and the wire-index prediction
// stay valid — no inflight forfeit, no IDR-cooldown anchor.
} else {
// `interval` was built as 1/effective_hz, so the round-trip recovers the integer
// rate.
let hz = interval_hz(interval);
match crate::encode::open_video(
plan.codec,
frame.format,
frame.width,
frame.height,
hz,
new_kbps as u64 * 1000,
frame.is_cuda(),
bit_depth,
plan.chroma,
) {
Ok(new_enc) => {
tracing::info!(
from_kbps = bitrate_kbps,
to_kbps = new_kbps,
"encoder rebuilt at new bitrate (adaptive bitrate)"
);
enc = new_enc;
bitrate_kbps = new_kbps;
live_bitrate.store(new_kbps, Ordering::Relaxed);
// The owed AUs died with the old encoder — same bookkeeping as a
// mode-switch rebuild; the fresh encoder opens on an IDR, so anchor the
// IDR cooldown too.
inflight.clear();
last_au_at = std::time::Instant::now();
encoder_resets = 0;
last_forced_idr = Some(std::time::Instant::now());
}
Err(e) => {
tracing::error!(error = %format!("{e:#}"), to_kbps = new_kbps,
"bitrate-change encoder rebuild failed — keeping the current rate");
}
}
}
}