fix(client): the native rung now follows the stream's colour and reports true decode latency
The round-4 residuals, closed after the WP-D hardware verdict: - VUI colour plumbing (the one silent-wrong): the picture's ACTIVE SPS's colour signalling (H.273 code points + range, with E.2.1's 'unspecified' inference where the VUI is silent — the vendored parser's defaults ARE the inferred values, verified) rides PicturePlan -> DecodedVkFrame -> NativeVkFrame per frame, never latched: the Windows host switches an HDR desktop to PQ/BT.2020 IN-BAND while the Welcome still says SDR. Before this, the native path would have painted PQ washed out, silently. - Native decode-latency stat: the deliberately-deferred NativeVk arm of the pump's sampled once-per-stats-window decode measurement now feeds - the frame's (semaphore, semaphore_value) is the decode-done signal, resolved through the shipped ledger before a bounded, pure-measurement vkWaitSemaphores (VkH264Decoder::wait_decoded). - The renegotiation-teardown window is settled as NO HOLE: rebuild_state now documents the full safety argument (graveyarded pools stay intact under presenter holds, tokens route strictly by generation, session objects die only post-drain with the generation gate INSIDE read_status), and the two backend comments that wrongly claimed stale pools were 'gone' are fixed. - VK_KHR_unified_image_layouts stays deferred (fleet drivers lack it). Adversarial review round 6: 3 minor findings (2 doc fixes applied; the SPS-replaced-without-PPS-resend divergence stays a documented envelope assumption - hosts re-send both at every keyframe, and a hardening PlanWarning could cost real frames on a false positive). Gates: fmt clean; clippy -D warnings zero (mac + pf-lxcheck2 container, incl. pf-client-core/pf-presenter); tests 45+30+53 mac, 30+121+53 container.
This commit is contained in:
@@ -37,6 +37,7 @@ use std::collections::VecDeque;
|
||||
use ash::vk;
|
||||
use ash::vk::native as hh;
|
||||
use pf_bitstream::h264::AuPlan;
|
||||
use pf_bitstream::h264::ColourDescription;
|
||||
use pf_bitstream::h264::DisplayCrop;
|
||||
use pf_bitstream::h264::DpbUpdate;
|
||||
use pf_bitstream::h264::H264Planner;
|
||||
@@ -125,6 +126,10 @@ pub struct DecodedVkFrame {
|
||||
pub coded_height: u32,
|
||||
/// Conformance-window crop: the region to display.
|
||||
pub crop: DisplayCrop,
|
||||
/// Colour signalling from the picture's ACTIVE SPS (pf-bitstream applies
|
||||
/// E.2.1's "unspecified" inference where the VUI is silent). Per frame, like
|
||||
/// [`Self::crop`]: the host switches HDR in-band with a new SPS mid-stream.
|
||||
pub colour: ColourDescription,
|
||||
/// Timeline pair: pixels ready at `semaphore >= value`; the sampling
|
||||
/// consumer signals `value + 1` (see the type docs).
|
||||
pub semaphore: vk::Semaphore,
|
||||
@@ -409,6 +414,7 @@ struct PendingPic {
|
||||
/// The image's timeline value the decode signalled (frame readiness).
|
||||
timeline_value: u64,
|
||||
crop: DisplayCrop,
|
||||
colour: ColourDescription,
|
||||
poc: i32,
|
||||
is_idr: bool,
|
||||
}
|
||||
@@ -733,6 +739,7 @@ impl VkH264Decoder {
|
||||
query_slot: query_index,
|
||||
timeline_value: signal_value,
|
||||
crop: plan.picture.display_crop,
|
||||
colour: plan.picture.colour,
|
||||
poc: plan.picture.pic_order_cnt,
|
||||
is_idr: plan.picture.is_idr,
|
||||
},
|
||||
@@ -981,6 +988,30 @@ impl VkH264Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait — bounded by `timeout_ns` — for a delivered frame's decode-complete
|
||||
/// signal ([`DecodedVkFrame::semaphore`] reaching [`DecodedVkFrame::value`]).
|
||||
/// Pure measurement (the integration layer's sampled decode-latency stat):
|
||||
/// touches no decoder state, so a timeout or error only degrades the stat —
|
||||
/// the consumer's own GPU wait is what gates sampling, never this. `frame`
|
||||
/// must be unreleased (`release_frame` still owed), which pins its pool — and
|
||||
/// with it the semaphore — alive, graveyarded generations included; a
|
||||
/// stale-generation frame declines rather than block on a verdict the
|
||||
/// rebuild's drain already implied.
|
||||
pub fn wait_decoded(&self, frame: &DecodedVkFrame, timeout_ns: u64) -> bool {
|
||||
if frame.generation != self.generation {
|
||||
return false;
|
||||
}
|
||||
let semaphores = [frame.semaphore];
|
||||
let values = [frame.value];
|
||||
let info = vk::SemaphoreWaitInfo::default()
|
||||
.semaphores(&semaphores)
|
||||
.values(&values);
|
||||
// SAFETY: live device (constructor contract); the semaphore is a pool
|
||||
// semaphore the unreleased frame keeps alive (fn docs); the info arrays
|
||||
// are locals outliving the call.
|
||||
unsafe { self.dev.ash().wait_semaphores(&info, timeout_ns) }.is_ok()
|
||||
}
|
||||
|
||||
/// Drain the planner (teardown / stream discontinuity): every buffered
|
||||
/// picture becomes display-ready via [`Self::take_ready`] (zero-copy — the
|
||||
/// images already hold the content), all DPB slots free, and any picture
|
||||
@@ -1050,6 +1081,22 @@ impl VkH264Decoder {
|
||||
/// retiring its picture pool to the graveyard when the consumer still holds
|
||||
/// images) and build a fresh one shaped by `plan`, bumping
|
||||
/// [`Self::generation`] so frames of the old one route to the graveyard.
|
||||
///
|
||||
/// Why a mid-stream rebuild is safe against presenter-held frames (the
|
||||
/// renegotiation-teardown question, settled):
|
||||
/// - **Images**: a pool with consumer holds retires to the graveyard INTACT —
|
||||
/// images, views and semaphores stay live until `release_frame` takes its
|
||||
/// last token, and a token is sent only after the presenter's sampling
|
||||
/// submission's fence was waited (its `value+1` write-back included), so no
|
||||
/// pool image is ever destroyed under in-flight GPU reads.
|
||||
/// - **Tokens**: every frame and its token carry the generation they were
|
||||
/// born under, and `release_frame` routes strictly by it (current pool vs
|
||||
/// graveyard entry), so releases cannot alias across generations.
|
||||
/// - **Session objects**: the session/ring/ops (query pool included) DO die
|
||||
/// right here — but only after [`Self::drain_gpu`], and no consumer-facing
|
||||
/// handle points at them: [`DecodedVkFrame`] borrows pool resources only,
|
||||
/// and `poll_status` generation-gates before it would touch the NEW
|
||||
/// generation's query pool with an old frame's slot.
|
||||
fn rebuild_state(&mut self, plan: &AuPlan) -> Result<(), VkDecodeError> {
|
||||
self.drain_gpu()?;
|
||||
if let Some(state) = self.state.take() {
|
||||
@@ -1251,6 +1298,7 @@ fn build_frame(state: &mut SessionState, entry: &PendingPic, generation: u64) ->
|
||||
coded_width: state.image_extent.width,
|
||||
coded_height: state.image_extent.height,
|
||||
crop: entry.crop,
|
||||
colour: entry.colour,
|
||||
semaphore: picture.semaphore,
|
||||
value: entry.timeline_value,
|
||||
poc: entry.poc,
|
||||
|
||||
Reference in New Issue
Block a user