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
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:
@@ -1328,6 +1328,14 @@ impl AmfEncoder {
|
||||
!ltr_disabled() && matches!(self.codec, Codec::H264 | Codec::H265)
|
||||
}
|
||||
|
||||
/// The VBV/HRD buffer (bits) at `bps`: ~1 frame interval, `PUNKTFUNK_VBV_FRAMES`-scaled — the
|
||||
/// same shape every backend ships. Shared by [`apply_static_props`](Self::apply_static_props)
|
||||
/// and [`Encoder::reconfigure_bitrate`] so a dynamic retarget rescales the buffer it opened with.
|
||||
fn vbv_bits(&self, bps: u64) -> i64 {
|
||||
((bps as f64 / self.fps.max(1) as f64) * crate::encode::vbv_frames_env())
|
||||
.clamp(1.0, i32::MAX as f64) as i64
|
||||
}
|
||||
|
||||
/// Apply the static encoder configuration (design §3.4 — the native mirror of the ffmpeg
|
||||
/// opts block in `open_win_encoder`). Called before `Init`, and again on a `reset()`
|
||||
/// re-`Init` (Terminate does not guarantee property retention across every driver).
|
||||
@@ -1357,14 +1365,12 @@ impl AmfEncoder {
|
||||
true,
|
||||
)?;
|
||||
// ~1-frame VBV (PUNKTFUNK_VBV_FRAMES override, same knob as the ffmpeg path).
|
||||
let vbv_frames = std::env::var("PUNKTFUNK_VBV_FRAMES")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<f32>().ok())
|
||||
.filter(|v| v.is_finite() && *v > 0.0)
|
||||
.unwrap_or(1.0);
|
||||
let vbv_bits = ((self.bitrate_bps as f64 / self.fps.max(1) as f64) * vbv_frames as f64)
|
||||
.clamp(1.0, i32::MAX as f64) as i64;
|
||||
set_prop(comp, p.vbv_size, AmfVariant::from_i64(vbv_bits), false)?;
|
||||
set_prop(
|
||||
comp,
|
||||
p.vbv_size,
|
||||
AmfVariant::from_i64(self.vbv_bits(self.bitrate_bps)),
|
||||
false,
|
||||
)?;
|
||||
set_prop(comp, p.enforce_hrd, AmfVariant::from_bool(true), false)?;
|
||||
set_prop(comp, p.filler_data, AmfVariant::from_bool(false), false)?;
|
||||
// Latency-first quality; low-latency submission mode (optional — newer VCN/drivers).
|
||||
@@ -2499,6 +2505,47 @@ impl Encoder for AmfEncoder {
|
||||
true
|
||||
}
|
||||
|
||||
fn reconfigure_bitrate(&mut self, bps: u64) -> bool {
|
||||
let bps_i = bps.min(i64::MAX as u64) as i64;
|
||||
let vbv = self.vbv_bits(bps);
|
||||
let Some(inner) = self.inner.as_ref() else {
|
||||
// Nothing live yet — the lazy open applies the new rate via `apply_static_props`.
|
||||
self.bitrate_bps = bps;
|
||||
return true;
|
||||
};
|
||||
// `TargetBitrate`/`PeakBitrate`/`VBVBufferSize` are DYNAMIC AMF properties (runtime-
|
||||
// changeable on AVC/HEVC/AV1 alike): a SetProperty on the live component retargets the
|
||||
// rate controller with no Terminate/re-Init — the reference chain, LTR slots and
|
||||
// in-flight frames all survive (no IDR).
|
||||
// SAFETY: `inner.comp.0` is the live component, used only on this thread with no AMF
|
||||
// call in flight (the session loop is synchronous); `set_prop` is a prefix-vtable call.
|
||||
let applied = unsafe {
|
||||
let p = &self.props;
|
||||
let comp = inner.comp.0;
|
||||
let ok = set_prop(comp, p.target_bitrate, AmfVariant::from_i64(bps_i), false)
|
||||
.unwrap_or(false)
|
||||
&& set_prop(comp, p.peak_bitrate, AmfVariant::from_i64(bps_i), false)
|
||||
.unwrap_or(false);
|
||||
if ok {
|
||||
// Rescale the VBV with the rate. Optional, like at open — a driver that declines
|
||||
// keeps the old buffer (a size mismatch the HRD absorbs), not worth a rebuild.
|
||||
let _ = set_prop(comp, p.vbv_size, AmfVariant::from_i64(vbv), false);
|
||||
}
|
||||
ok
|
||||
};
|
||||
if !applied {
|
||||
// A half-applied pair doesn't matter: the caller's rebuild fallback re-authors
|
||||
// everything from scratch.
|
||||
tracing::warn!(
|
||||
mbps = bps / 1_000_000,
|
||||
"AMF declined the dynamic bitrate retarget — falling back to a rebuild"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
self.bitrate_bps = bps; // future reset()/re-Init paths re-apply the new rate
|
||||
true
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<()> {
|
||||
let Some(inner) = self.inner.as_mut() else {
|
||||
return Ok(());
|
||||
|
||||
Reference in New Issue
Block a user