fix(encode/nvenc): AV1 stops shipping half a frame
Every 4K AV1 frame this host encoded reached the wire truncated to its first tile, and had since AV1 was wired up. Measured on .21 (RTX 5070 Ti, 4K60, split AUTO): each access unit carried a frame header declaring two tile rows and a single Tile Group OBU with tg_start = tg_end = 0, so libdav1d rejected 835 of 836 AUs with "Error parsing frame header". NVIDIA's hardware decoder accepts the truncated stream, which is why native Vulkan Video looked healthy at 60 fps while both conformant software decoders — rav1d in-tree and libdav1d out-of-tree — refused every frame and clients fell to a black screen. The two halves of sub-frame readback are armed by different conditions. build_init_params arms the WRITER (enableSubFrameWrite + reportSliceOffsets) from subframe_on alone; the chunked READER additionally requires slices >= 2, and resolve_slices returns 1 for AV1 unconditionally — before the PUNKTFUNK_NVENC_SLICES override is even read, because AV1 partitions via tiles rather than slices. So an AV1 session asked the driver to publish its output tile by tile and then took only the first tile with one blocking lock_bitstream. resolve_split_subframe — the one arbitration point both direct-SDK backends already call — now disarms sub-frame for AV1 and returns split_mode untouched, so AV1 keeps every engine split encode gives it. Arming the reader instead is not a drop-in alternative: poll_chunk cuts at bitstreamSizeInBytes on the reasoning that "slices are contiguous Annex-B", which AV1's OBUs are not. With sub-frame disarmed and split still AUTO, the same session decodes 654/654 frames clean through libdav1d. The test that pinned this as correct (av1_untouched, "both features are legal together") is replaced by one that pins the disarm, and by one that checks the reader's gate against the writer's — the comparison nothing made. The Linux latch comment claiming the two "can't disagree" is corrected; that claim is what made this invisible.
This commit is contained in:
@@ -1668,10 +1668,19 @@ impl NvencCudaEncoder {
|
||||
// Sub-frame chunked poll (§7 LN1 Phase 1; default-on since Phase 3): armed iff this
|
||||
// session was CONFIGURED multi-slice + sub-frame readback (`self.slices` /
|
||||
// `self.subframe_on` were resolved once in `query_caps` and consumed by
|
||||
// `build_config` / `build_init_params`, so the latch can't disagree with the session
|
||||
// config) and the retrieve is sync — chunked poll is a depth-1 sync feature; a
|
||||
// pipelined session's non-blocking poll owns the bitstream from the retrieve thread
|
||||
// instead (the sub-frame write itself stays armed there; it's harmless).
|
||||
// `build_config` / `build_init_params`) and the retrieve is sync — chunked poll is a
|
||||
// depth-1 sync feature; a pipelined session's non-blocking poll owns the bitstream
|
||||
// from the retrieve thread instead (sub-frame write is not armed there at all —
|
||||
// `build_init_params` gates it on `!enable_async`).
|
||||
//
|
||||
// ⚠ THIS LATCH CAN DISAGREE WITH THE WRITER, and an earlier revision of this comment
|
||||
// claimed it could not ("so the latch can't disagree with the session config"). It
|
||||
// can: `build_init_params` arms `enableSubFrameWrite` from `subframe_on` ALONE,
|
||||
// while this line additionally demands `slices >= 2`. AV1 resolves to 1 slice by
|
||||
// construction, so it armed the writer with nothing to read the chunks and every
|
||||
// frame reached the wire truncated to its first tile. `resolve_split_subframe` now
|
||||
// disarms sub-frame for AV1 so the two agree; that function's docs carry the
|
||||
// measurement.
|
||||
self.subframe_chunks = self.slices >= 2 && self.subframe_on && self.async_rt.is_none();
|
||||
if self.subframe_chunks {
|
||||
tracing::info!(
|
||||
|
||||
Reference in New Issue
Block a user