fix(encode): bound NVENC async pipelining by the capturer's texture ring
The Windows direct-NVENC backend registers and encodes the capturer's textures IN PLACE (no CopyResource), so how deep it may pipeline is a property of the CAPTURER, not of the encoder. It was bounded only by `async_inflight_cap()` — `PUNKTFUNK_NVENC_ASYNC_DEPTH`, default 4, clamped to the output-bitstream pool — which consults nothing about the capturer, while the comment at the backpressure loop claimed it "keep[s] in-flight depth within the capturer's texture ring". It never did. The IDD-push capturer rotates `OUT_RING = 3` per delivered frame with no regard for encode completion (its own invariant note says OUT_RING(3) > max pipeline_depth(2)). With the default async depth of 4 the encoder can therefore still be reading a texture the capturer has already handed out again and overwritten: torn or mixed frames. It is visual corruption rather than UB, so it fails silently and intermittently — the worst shape to diagnose from a field report. Adds `Encoder::set_input_ring_depth`, reported from `Capturer::pipeline_depth`, and bounds the async backpressure loop by `min(async_inflight_cap(), depth)`. For IDD-push that yields 2, matching the capturer's stated contract; backends that copy their input, or are synchronous, ignore it. Wired at ALL THREE encoder-creation sites (initial open, stall/resize rebuild, ABR rebuild) and forwarded through `TrackedEncoder` — this crate has a documented trap where an unforwarded defaulted trait method silently no-ops through that wrapper, which has already bitten the direct-NVENC work once and the wire-chunking probe once. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1477,6 +1477,9 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
|
||||
if let Some(c) = plan.wire_chunk {
|
||||
new_enc.set_wire_chunking(c);
|
||||
}
|
||||
// (`max_depth` is computed later in the iteration — read the capturer
|
||||
// directly so an ABR rebuild re-establishes the bound immediately.)
|
||||
new_enc.set_input_ring_depth(capturer.pipeline_depth().max(1));
|
||||
enc = new_enc;
|
||||
bitrate_kbps = new_kbps;
|
||||
live_bitrate.store(new_kbps, Ordering::Relaxed);
|
||||
@@ -2265,6 +2268,9 @@ fn try_inplace_resize(
|
||||
if let Some(c) = plan.wire_chunk {
|
||||
new_enc.set_wire_chunking(c);
|
||||
}
|
||||
// Re-report the capturer's ring depth: in-place backends bound async pipelining by it, and a
|
||||
// rebuilt encoder starts with it unset.
|
||||
new_enc.set_input_ring_depth(capturer.pipeline_depth().max(1));
|
||||
*enc = new_enc;
|
||||
*frame = new_frame;
|
||||
*interval = std::time::Duration::from_secs_f64(1.0 / effective_hz.max(1) as f64);
|
||||
@@ -2579,6 +2585,10 @@ fn build_pipeline(
|
||||
if let Some(c) = plan.wire_chunk {
|
||||
enc.set_wire_chunking(c);
|
||||
}
|
||||
// Tell in-place backends (Windows direct-NVENC) how deep they may pipeline against the
|
||||
// capturer's texture ring — without it they use only the env/pool cap and can encode a texture
|
||||
// the capturer has already rotated and overwritten.
|
||||
enc.set_input_ring_depth(capturer.pipeline_depth().max(1));
|
||||
// Post-open cross-check: the Welcome already committed `chroma_format` from the pre-open probe, so
|
||||
// warn loudly if the encoder actually opened a different chroma than negotiated (the in-band SPS is
|
||||
// authoritative for the decoder, but a mismatch means the probe and the live open disagreed).
|
||||
|
||||
Reference in New Issue
Block a user