feat(stats): wire real scale/transfer/encode timing from EncState
Oracle step 4 (option A) — give the scale_*, transfer_*, encode_* stats
fields real producers instead of misleading zeros. The fields existed in
FrameTimings and PipelineStats already; producers just weren't passing
non-zero values.
- avhw.rs: new EncodeStages { scale_us, transfer_us, encode_us } struct.
EncState::encode_frame (HW VAAPI path) now times the filter graph
separately from avcodec_send_frame, returning EncodeStages. transfer_us
is honestly 0 because the HW path never reads back to CPU.
SwEncState::encode_frame (SW fallback path) returns EncodeStages too;
there import_and_scale bundles GPU scale + GPU→CPU readback into one
call, so scale_us includes transfer for SW. Documented inline.
- state.rs: StreamingEncoder::encode_frame return type bumps from
Result<()> to Result<EncodeStages>; wlr-screencopy path now feeds
real per-stage timings into FrameTimings instead of just total_us.
- state_portal.rs: HW portal path (enc.encode_frame) now extracts
stages.scale_us / stages.transfer_us / stages.encode_us into
FrameTimings. Removed the now-unused t_encode_start binding.
Deferred (documented):
- state_portal.rs SW portal path (line 525) calls import_and_scale +
enc_thread separately and bypasses SwEncState::encode_frame. To wire
scale/transfer timing there too, either route through SwEncState or
thread timing out of import_and_scale. Out of scope for this commit.
- SW path lumps transfer into scale_us. Splitting requires extending
import_and_scale's return type — left as a follow-up if operational
need arises (current default is HW VAAPI).
Oracle audit 2026-06-28 step 4 (option A: integrate, not delete).
All 79 unit tests + 3 integration tests pass. clippy: 0 errors.
This commit is contained in:
+51
-4
@@ -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<EncodeStages> {
|
||||
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<EncodeStages> {
|
||||
// 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<()> {
|
||||
|
||||
+12
-5
@@ -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<crate::avhw::EncodeStages> {
|
||||
match self {
|
||||
StreamingEncoder::Mp4(enc) => enc.encode_frame(hw_frame),
|
||||
StreamingEncoder::WebRtc(enc) => enc.encode_frame(hw_frame),
|
||||
@@ -626,16 +626,23 @@ impl<S: CaptureSource> State<S> {
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.stats_frames += 1;
|
||||
if let Some(last) = self.stats_last_time {
|
||||
if last.elapsed() >= std::time::Duration::from_secs(10) {
|
||||
|
||||
+5
-4
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user