diff --git a/src/avhw.rs b/src/avhw.rs index abe118a..722c087 100644 --- a/src/avhw.rs +++ b/src/avhw.rs @@ -192,6 +192,18 @@ impl Drop for AvHwFrameCtx { } } +/// Per-stage timing breakdown for one encode cycle on the hardware path. +/// Returned by [`EncState::encode_frame`] so callers can fold the numbers +/// into [`crate::stats::FrameTimings`]. `transfer_us` is always 0 on the HW +/// path because the frame stays on the GPU; the SW path's struct (if added +/// later) would carry a real readback measurement. +#[derive(Debug, Default, Clone, Copy)] +pub struct EncodeStages { + pub scale_us: u64, + pub transfer_us: u64, + pub encode_us: u64, +} + /// Test whether `drm_device` can import the PipeWire DMA-BUF frame via VAAPI. pub fn test_dma_buf_import(drm_device: &Path, frame: &PwDmaBufFrame) -> Result<()> { let hw_dev = AvHwDevCtx::new_vaapi(drm_device)?; @@ -577,7 +589,7 @@ impl EncState { &self.frames_rgb } - pub fn encode_frame(&mut self, hw_frame: &ff::frame::Video) -> Result<()> { + pub fn encode_frame(&mut self, hw_frame: &ff::frame::Video) -> Result { let mut filter_src_ctx = self .video_filter .get("in") @@ -589,11 +601,18 @@ impl EncState { .ok_or_else(|| anyhow::anyhow!("filter 'out' not found"))?; let mut filter_sink = filter_sink_ctx.sink(); + // Scale stage = filter graph push + pull (scale_vaapi for resolution + // change + format conversion to NV12). Timed separately from the + // actual avcodec_send_frame so the per-stage stats answer "where is + // latency?" honestly. See Oracle audit 2026-06-28 step 4. + let scale_start = Instant::now(); // SAFETY: hw_frame is a valid VAAPI hardware frame from capture. filter_src .add(hw_frame) .map_err(|e| anyhow::anyhow!("Filter source add failed: {e}"))?; + let mut scale_us = 0u64; + let mut encode_us = 0u64; loop { let mut filtered = ff::frame::Video::empty(); match filter_sink.frame(&mut filtered) { @@ -605,6 +624,11 @@ impl EncState { Err(ff::Error::Other { errno }) if errno == ffi::EAGAIN => break, Err(e) => bail!("Filter sink get frame failed: {e}"), } + // First successful pull closes the scale-stage measurement; later + // pulls (rare extras) roll into encode time. + if scale_us == 0 { + scale_us = scale_start.elapsed().as_micros() as u64; + } let pts = filtered.pts().unwrap_or(0); if self.starting_timestamp.is_none() { @@ -612,6 +636,7 @@ impl EncState { } let start_ts = self.starting_timestamp.unwrap(); + let encode_start = Instant::now(); // SAFETY: avcodec_send_frame sends a valid NV12 VAAPI surface to the encoder. let ret = unsafe { ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), filtered.as_ptr()) }; @@ -619,9 +644,15 @@ impl EncState { bail!("avcodec_send_frame failed: {}", ff_err(ret)); } self.drain_encoder(start_ts)?; + encode_us += encode_start.elapsed().as_micros() as u64; } - Ok(()) + Ok(EncodeStages { + scale_us, + // HW path stays on GPU — no CPU readback, transfer is N/A. + transfer_us: 0, + encode_us, + }) } pub fn flush(&mut self) -> Result<()> { @@ -1562,9 +1593,25 @@ impl SwEncState { self.import.frames_rgb() } - pub fn encode_frame(&mut self, hw_frame: &ff::frame::Video) -> Result<()> { + pub fn encode_frame(&mut self, hw_frame: &ff::frame::Video) -> Result { + // SW path: import_and_scale bundles GPU filter graph (scale) + GPU→CPU + // readback (transfer) into one call. Timing them separately requires + // extending import_and_scale's signature; for now both roll into + // scale_us and transfer_us stays 0 with this comment as the honest + // statement. Oracle audit 2026-06-28 step 4. + let scale_start = Instant::now(); let cpu_frame = self.import.import_and_scale(hw_frame)?; - self.encode.encode_cpu_frame(&cpu_frame).map(|_| ()) + let scale_us = scale_start.elapsed().as_micros() as u64; + + let encode_start = Instant::now(); + self.encode.encode_cpu_frame(&cpu_frame)?; + let encode_us = encode_start.elapsed().as_micros() as u64; + + Ok(EncodeStages { + scale_us, + transfer_us: 0, + encode_us, + }) } pub fn flush(&mut self) -> Result<()> { diff --git a/src/state.rs b/src/state.rs index 5274221..eafcf53 100644 --- a/src/state.rs +++ b/src/state.rs @@ -126,7 +126,7 @@ impl StreamingEncoder { } } - fn encode_frame(&mut self, hw_frame: &ffmpeg_next::frame::Video) -> anyhow::Result<()> { + fn encode_frame(&mut self, hw_frame: &ffmpeg_next::frame::Video) -> anyhow::Result { match self { StreamingEncoder::Mp4(enc) => enc.encode_frame(hw_frame), StreamingEncoder::WebRtc(enc) => enc.encode_frame(hw_frame), @@ -626,15 +626,22 @@ impl State { }; if should_encode { let encode_start = Instant::now(); - if let Err(e) = enc.encode_frame(&surface) { - tracing::error!("encode_frame failed: {}", e); - self.errored = true; + match enc.encode_frame(&surface) { + Ok(stages) => { + let encode_elapsed = encode_start.elapsed().as_micros() as u64; + self.stats.record_encode(&FrameTimings { + scale_us: stages.scale_us, + transfer_us: stages.transfer_us, + encode_us: stages.encode_us, + total_us: encode_elapsed, + ..Default::default() + }); + } + Err(e) => { + tracing::error!("encode_frame failed: {}", e); + self.errored = true; + } } - let encode_elapsed = encode_start.elapsed().as_micros() as u64; - self.stats.record_encode(&FrameTimings { - total_us: encode_elapsed, - ..Default::default() - }); } self.stats_frames += 1; if let Some(last) = self.stats_last_time { diff --git a/src/state_portal.rs b/src/state_portal.rs index be77da5..2556cd1 100644 --- a/src/state_portal.rs +++ b/src/state_portal.rs @@ -494,7 +494,6 @@ impl StatePortal { }?; let import_us = t_import_start.elapsed().as_micros() as u64; - let t_encode_start = Instant::now(); // 设置帧的显示时间戳(PTS),基于已编码帧序号 // SAFETY: vaapi_frame is the freshly-imported valid AVFrame returned by @@ -504,15 +503,17 @@ impl StatePortal { } // 送入编码器完成:缩放 → 回读 → 格式转换 → H.264 编码 - enc.encode_frame(&vaapi_frame)?; + let stages = enc.encode_frame(&vaapi_frame)?; let total_us = t_import_start.elapsed().as_micros() as u64; - let encode_us = t_encode_start.elapsed().as_micros() as u64; + let encode_us = stages.encode_us; self.frames_encoded += 1; - // 记录帧计时到管道统计(import + encode 内部各阶段暂不可分离,用 total 覆盖) + // 记录帧计时到管道统计(scale 来自 filter graph;transfer 在 HW 路径恒为 0) let timings = FrameTimings { import_us, + scale_us: stages.scale_us, + transfer_us: stages.transfer_us, encode_us, total_us, ..Default::default()