fix(stats): decode stage measures GPU completion; Alt+Enter fullscreen alias
The Vulkan path's receive_frame returns at SUBMISSION (~0.1 ms) — the hardware decodes asynchronously, so the decode stat was truthful but measured the wrong boundary. The pump now ships the frame to the presenter FIRST, then waits the frame's timeline fence (vkWaitSemaphores resolved through the shared device's proc chain) and stamps received→decode-COMPLETE — true NVDEC time at zero pipeline cost, since the presenter's own GPU wait is what actually gates sampling. Software/ VAAPI keep their synchronous stamps. Also: Alt+Enter joins F11 as the fullscreen toggle (some keyboards' Fn layer sends a media key for plain F11 — observed on glass as 'F11 only works with shift'); the shell's shortcuts panel lists both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -74,7 +74,10 @@ pub struct Stats {
|
||||
/// `host + network`. An old host never emits 0xCF, so this stays false and the
|
||||
/// combined stage renders unchanged.
|
||||
pub split: bool,
|
||||
/// p50 `decode` stage: received → decoded, single-clock client-local (ms).
|
||||
/// p50 `decode` stage: received → decode COMPLETE, single-clock client-local (ms).
|
||||
/// Hardware paths measure GPU completion via the frame's timeline fence (an async
|
||||
/// decoder's submission returning in ~0.1 ms is not "decoded"); software measures
|
||||
/// the synchronous CPU decode.
|
||||
pub decode_ms: f32,
|
||||
/// Unrecoverable network frame drops this window, and their share of
|
||||
/// received+lost (%). The OSD renders the counter line only when nonzero.
|
||||
@@ -343,13 +346,33 @@ fn pump(
|
||||
}
|
||||
pending_split.push_back((frame.pts_ns, hn / 1000));
|
||||
}
|
||||
// `decode` stage: received→decoded, single clock, no skew.
|
||||
decode_us.push(decoded_ns.saturating_sub(received_ns) / 1000);
|
||||
// Ship the frame FIRST, then settle the decode stat: on the
|
||||
// Vulkan path receive_frame returns at SUBMISSION (~0.1 ms) and
|
||||
// the hardware decodes asynchronously — waiting the frame's
|
||||
// timeline fence here (after the presenter already has the
|
||||
// frame) measures true received→decode-complete at zero
|
||||
// pipeline cost. Software/VAAPI keep the synchronous stamp.
|
||||
let hw_fence = match &image {
|
||||
DecodedImage::VkFrame(v) => {
|
||||
Some((v.timeline_sem, v.decode_done_value))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let _ = frame_tx.force_send(DecodedFrame {
|
||||
pts_ns: frame.pts_ns,
|
||||
decoded_ns,
|
||||
image,
|
||||
});
|
||||
// `decode` stage: received→decode COMPLETE, single clock.
|
||||
let decode_done_ns = match hw_fence {
|
||||
Some((sem, value))
|
||||
if decoder.wait_hw_decoded(sem, value, 50_000_000) =>
|
||||
{
|
||||
now_ns()
|
||||
}
|
||||
_ => decoded_ns,
|
||||
};
|
||||
decode_us.push(decode_done_ns.saturating_sub(received_ns) / 1000);
|
||||
}
|
||||
Ok(None) => no_output_streak += 1,
|
||||
// Survivable (loss until the next IDR/RFI recovery) — keep feeding.
|
||||
|
||||
@@ -68,6 +68,12 @@ pub struct VkVideoFrame {
|
||||
/// The frame pool's VkFormat (`AVVulkanFramesContext.format[0]`, raw i32) — the
|
||||
/// multiplanar format the presenter builds its per-plane views against.
|
||||
pub vk_format: i32,
|
||||
/// The frame's timeline semaphore (raw VkSemaphore; creation-constant) and the
|
||||
/// value FFmpeg's decode submission signals on completion — the pump waits this
|
||||
/// pair AFTER shipping the frame to measure true GPU decode time (zero pipeline
|
||||
/// cost: the presenter already waits the same pair on the GPU).
|
||||
pub timeline_sem: u64,
|
||||
pub decode_done_value: u64,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub color: ColorDesc,
|
||||
@@ -313,6 +319,15 @@ impl Decoder {
|
||||
done(Backend::Software(SoftwareDecoder::new(codec_id)?))
|
||||
}
|
||||
|
||||
/// Wait for a Vulkan-Video frame's GPU decode to complete (timeline semaphore) —
|
||||
/// the pump's decode-stat measurement. `false` = not the Vulkan backend, or timeout.
|
||||
pub fn wait_hw_decoded(&self, timeline_sem: u64, value: u64, timeout_ns: u64) -> bool {
|
||||
match &self.backend {
|
||||
Backend::Vulkan(v) => v.wait_timeline(timeline_sem, value, timeout_ns),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Drain the "please ask the host for an IDR" flag — the pump calls this each iteration
|
||||
/// (throttled) so a demoted/erroring decoder can resynchronize under the infinite GOP.
|
||||
pub fn take_keyframe_request(&mut self) -> bool {
|
||||
@@ -807,6 +822,10 @@ struct VulkanDecoder {
|
||||
hw_device: *mut ffmpeg::ffi::AVBufferRef,
|
||||
packet: *mut ffmpeg::ffi::AVPacket,
|
||||
frame: *mut ffmpeg::ffi::AVFrame,
|
||||
/// `vkWaitSemaphores` on the shared device — the decode-complete measurement
|
||||
/// (resolved through the same get_proc_addr chain FFmpeg uses).
|
||||
wait_semaphores: pf_ffvk::PFN_vkWaitSemaphores,
|
||||
vk_device: pf_ffvk::VkDevice,
|
||||
/// Storage `AVVulkanDeviceContext` points into (extension string arrays + the
|
||||
/// feature chain) — FFmpeg reads the extension lists past init (frames-context
|
||||
/// setup keys code paths off them), so this lives exactly as long as `hw_device`.
|
||||
@@ -926,6 +945,26 @@ impl VulkanDecoder {
|
||||
return Err(averr("av_hwdevice_ctx_init(VULKAN)", r));
|
||||
}
|
||||
|
||||
// vkWaitSemaphores for the pump's decode-complete stat: loader →
|
||||
// vkGetDeviceProcAddr → device fn (core 1.2, guaranteed by our gate).
|
||||
let gipa = (*hwctx)
|
||||
.get_proc_addr
|
||||
.expect("get_proc_addr was just set above");
|
||||
let gdpa: pf_ffvk::PFN_vkGetDeviceProcAddr = std::mem::transmute(gipa(
|
||||
(*hwctx).inst,
|
||||
c"vkGetDeviceProcAddr".as_ptr(),
|
||||
));
|
||||
let wait_semaphores: pf_ffvk::PFN_vkWaitSemaphores = std::mem::transmute(gdpa
|
||||
.expect("vkGetDeviceProcAddr resolvable")(
|
||||
(*hwctx).act_dev,
|
||||
c"vkWaitSemaphores".as_ptr(),
|
||||
));
|
||||
if wait_semaphores.is_none() {
|
||||
ffi::av_buffer_unref(&mut hw_device);
|
||||
bail!("vkWaitSemaphores unresolvable on this device");
|
||||
}
|
||||
let vk_device = (*hwctx).act_dev;
|
||||
|
||||
let codec = ffi::avcodec_find_decoder(codec_id.into());
|
||||
if codec.is_null() {
|
||||
ffi::av_buffer_unref(&mut hw_device);
|
||||
@@ -951,6 +990,8 @@ impl VulkanDecoder {
|
||||
hw_device,
|
||||
packet: ffi::av_packet_alloc(),
|
||||
frame: ffi::av_frame_alloc(),
|
||||
wait_semaphores,
|
||||
vk_device,
|
||||
_ctx_storage: store,
|
||||
})
|
||||
}
|
||||
@@ -985,6 +1026,27 @@ impl VulkanDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Block until the timeline semaphore reaches `value` (GPU decode complete) or the
|
||||
/// timeout passes. Pure measurement — the presenter's own GPU wait is what gates
|
||||
/// sampling, so a timeout here only degrades the stat, never the picture.
|
||||
fn wait_timeline(&self, sem: u64, value: u64, timeout_ns: u64) -> bool {
|
||||
let sems = [sem as pf_ffvk::VkSemaphore];
|
||||
let values = [value];
|
||||
let info = pf_ffvk::VkSemaphoreWaitInfo {
|
||||
sType: pf_ffvk::VkStructureType_VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO,
|
||||
pNext: std::ptr::null(),
|
||||
flags: 0,
|
||||
semaphoreCount: 1,
|
||||
pSemaphores: sems.as_ptr(),
|
||||
pValues: values.as_ptr(),
|
||||
};
|
||||
// SAFETY: resolved from this device at init; handles outlive the decoder.
|
||||
let r = unsafe {
|
||||
self.wait_semaphores.expect("checked at init")(self.vk_device, &info, timeout_ns)
|
||||
};
|
||||
r == 0 // VK_SUCCESS (VK_TIMEOUT = 2)
|
||||
}
|
||||
|
||||
/// Lift the decoded `AVVkFrame` into a [`VkVideoFrame`]: clone the AVFrame (the
|
||||
/// guard — keeps the image + frames context alive through present) and ship the
|
||||
/// POINTERS; the presenter reads the live sync state under the frames-context lock
|
||||
@@ -1024,12 +1086,18 @@ impl VulkanDecoder {
|
||||
ffi::av_frame_free(&mut clone);
|
||||
bail!("multi-image Vulkan frames unsupported (disjoint pool)");
|
||||
}
|
||||
// Safe without the frames lock: the handle is creation-constant and
|
||||
// sem_value was last written by the decode submission on THIS thread.
|
||||
let timeline_sem = (*vkf).sem[0] as u64;
|
||||
let decode_done_value = (*vkf).sem_value[0];
|
||||
Ok(VkVideoFrame {
|
||||
vkframe: vkf as usize,
|
||||
frames_ctx: fc as usize,
|
||||
lock_frame,
|
||||
unlock_frame,
|
||||
vk_format,
|
||||
timeline_sem,
|
||||
decode_done_value,
|
||||
width: (*self.frame).width as u32,
|
||||
height: (*self.frame).height as u32,
|
||||
color: ColorDesc::from_raw(self.frame),
|
||||
|
||||
Reference in New Issue
Block a user