From c64cdc4ef7a76121ca57834dcda5d50ee124b07d Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 7 Aug 2026 19:26:32 +0200 Subject: [PATCH] =?UTF-8?q?docs(encode):=20close=20out=20the=20tile-aware?= =?UTF-8?q?=20AV1=20sub-frame=20reader=20=E2=80=94=20measured,=20not=20wor?= =?UTF-8?q?th=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #95 disarmed sub-frame readback for AV1, which means AV1 forgoes the latency win HEVC gets from shipping slice 1 while slice 2 encodes. The follow-up was to teach the reader AV1's units: cut on OBU boundaries rather than byte counts and arm from the driver's reported unit count. Measured on .21 (RTX 5070 Ti, av1_nvenc) before writing any of it, and the measurement closes it rather than scoping it. Reading the frame headers av1_nvenc actually emits at 4K: width_in_sbs_minus_1[0] = 59 one tile column, the full 3840 height_in_sbs_minus_1[0..1] = 16, 16 two tile rows tile_start_and_end_present_flag = 0 BOTH TILES IN ONE TILE GROUP OBU That last flag is the finding. "Cut on OBU boundaries" presumes the tiles are separate OBUs and they are not — there is no boundary between them to cut on. Shipping tile 1 early would need the HOST to re-author AV1 syntax per chunk, synthesising a fresh Tile Group OBU header with tile_start_and_end_present_flag = 1 and its own tg_start/tg_end. That is bitstream surgery on the encode path, not the reader change it was assumed to be. And the prize would be small even then, because split encode already spent it. The two tile rows go to two split-encode engines that run CONCURRENTLY, so they complete at nearly the same moment — the win is bounded by the skew between engines, not by half a frame. Whole-frame encode measures 3.3-3.6 ms at 4K60 against a 16.7 ms p50 end-to-end, so even the sequential-tiles fantasy caps near 1.7 ms and the real number is a fraction of it. HEVC's win is bigger for a structural reason that does not transfer: forced split and sub-frame are mutually unsupported, so HEVC's slices genuinely are produced one after another. 1080p settles it further: tile_cols_log2 = tile_rows_log2 = 0, a single tile, so there is nothing to pipeline at the commonest streaming resolution at all. Recorded next to the disarm with the reopen condition named — NVENC emitting one OBU per tile, or setting tile_start_and_end_present_flag = 1 — so this is closed on evidence rather than left as an open maybe. Documentation only — no behaviour change. --- crates/pf-encode/src/enc/nvenc_core.rs | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/crates/pf-encode/src/enc/nvenc_core.rs b/crates/pf-encode/src/enc/nvenc_core.rs index a20cd47d..242230f9 100644 --- a/crates/pf-encode/src/enc/nvenc_core.rs +++ b/crates/pf-encode/src/enc/nvenc_core.rs @@ -137,6 +137,36 @@ pub(super) fn subframe_env_forced() -> bool { /// drop-in alternative — `poll_chunk` cuts at `bitstreamSizeInBytes` on the reasoning that /// "slices are contiguous Annex-B", which AV1's OBUs are not. /// +/// # A tile-aware chunk reader was considered and is CLOSED, not deferred +/// +/// The obvious follow-up is to teach the reader AV1's units — cut on OBU boundaries instead +/// of byte counts and arm from the driver's reported unit count — so AV1 gets the sub-frame +/// latency win HEVC gets (ship tile 1 while tile 2 encodes). Measured on `.21` (RTX 5070 Ti, +/// `av1_nvenc`, 2026-08-07) before writing any of it, and the measurement closes it: +/// +/// * **4K carries two tiles, and they share ONE Tile Group OBU.** The frame header reads +/// `width_in_sbs_minus_1[0] = 59` (one tile column, the full 3840) and +/// `height_in_sbs_minus_1[] = {16, 16}` (two tile rows) — but +/// `tile_start_and_end_present_flag = 0`, which puts both tiles in a single Tile Group +/// OBU. There is no OBU boundary between them to cut on. Shipping tile 1 early would mean +/// the HOST re-authoring AV1 syntax per chunk — synthesising a fresh Tile Group OBU header +/// with `tile_start_and_end_present_flag = 1` and its own `tg_start`/`tg_end` — which is +/// bitstream surgery on the encode path, not a reader change. +/// * **1080p carries one tile** (`tile_cols_log2 = tile_rows_log2 = 0`), so there is nothing +/// to pipeline at the commonest streaming resolution regardless. +/// * **The prize is small even at 4K, because split encode already spent it.** The two tile +/// rows go to two split-encode engines that run CONCURRENTLY, so they finish at nearly the +/// same moment: the win is bounded by the skew between engines, not by half the frame. +/// Whole-frame encode measures 3.3–3.6 ms at 4K60 against a 16.7 ms p50 end-to-end, so +/// even the sequential-tiles fantasy caps out near 1.7 ms and the real figure is a +/// fraction of that. HEVC's sub-frame win is larger for a structural reason that does not +/// transfer: forced split and sub-frame are mutually unsupported (below), so HEVC's slices +/// really are produced one after another. +/// +/// Reopen only if NVENC starts emitting one OBU per tile, or sets +/// `tile_start_and_end_present_flag = 1` — at that point the cut points exist and the reader +/// change becomes the small piece it was assumed to be. +/// /// Returns the `(split_mode, subframe)` to ACTUALLY configure. The caller must store BOTH back /// (the chunked-poll latch and `CeilingKey` key on them) — a silent in-params drop would leave /// `poll_chunk` busy-polling its full budget every AU (`numSlices` stays 0 without