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.
|
/// 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<()> {
|
pub fn test_dma_buf_import(drm_device: &Path, frame: &PwDmaBufFrame) -> Result<()> {
|
||||||
let hw_dev = AvHwDevCtx::new_vaapi(drm_device)?;
|
let hw_dev = AvHwDevCtx::new_vaapi(drm_device)?;
|
||||||
@@ -577,7 +589,7 @@ impl EncState {
|
|||||||
&self.frames_rgb
|
&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
|
let mut filter_src_ctx = self
|
||||||
.video_filter
|
.video_filter
|
||||||
.get("in")
|
.get("in")
|
||||||
@@ -589,11 +601,18 @@ impl EncState {
|
|||||||
.ok_or_else(|| anyhow::anyhow!("filter 'out' not found"))?;
|
.ok_or_else(|| anyhow::anyhow!("filter 'out' not found"))?;
|
||||||
let mut filter_sink = filter_sink_ctx.sink();
|
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.
|
// SAFETY: hw_frame is a valid VAAPI hardware frame from capture.
|
||||||
filter_src
|
filter_src
|
||||||
.add(hw_frame)
|
.add(hw_frame)
|
||||||
.map_err(|e| anyhow::anyhow!("Filter source add failed: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("Filter source add failed: {e}"))?;
|
||||||
|
|
||||||
|
let mut scale_us = 0u64;
|
||||||
|
let mut encode_us = 0u64;
|
||||||
loop {
|
loop {
|
||||||
let mut filtered = ff::frame::Video::empty();
|
let mut filtered = ff::frame::Video::empty();
|
||||||
match filter_sink.frame(&mut filtered) {
|
match filter_sink.frame(&mut filtered) {
|
||||||
@@ -605,6 +624,11 @@ impl EncState {
|
|||||||
Err(ff::Error::Other { errno }) if errno == ffi::EAGAIN => break,
|
Err(ff::Error::Other { errno }) if errno == ffi::EAGAIN => break,
|
||||||
Err(e) => bail!("Filter sink get frame failed: {e}"),
|
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);
|
let pts = filtered.pts().unwrap_or(0);
|
||||||
if self.starting_timestamp.is_none() {
|
if self.starting_timestamp.is_none() {
|
||||||
@@ -612,6 +636,7 @@ impl EncState {
|
|||||||
}
|
}
|
||||||
let start_ts = self.starting_timestamp.unwrap();
|
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.
|
// SAFETY: avcodec_send_frame sends a valid NV12 VAAPI surface to the encoder.
|
||||||
let ret =
|
let ret =
|
||||||
unsafe { ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), filtered.as_ptr()) };
|
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));
|
bail!("avcodec_send_frame failed: {}", ff_err(ret));
|
||||||
}
|
}
|
||||||
self.drain_encoder(start_ts)?;
|
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<()> {
|
pub fn flush(&mut self) -> Result<()> {
|
||||||
@@ -1562,9 +1593,25 @@ impl SwEncState {
|
|||||||
self.import.frames_rgb()
|
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)?;
|
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<()> {
|
pub fn flush(&mut self) -> Result<()> {
|
||||||
|
|||||||
+16
-9
@@ -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 {
|
match self {
|
||||||
StreamingEncoder::Mp4(enc) => enc.encode_frame(hw_frame),
|
StreamingEncoder::Mp4(enc) => enc.encode_frame(hw_frame),
|
||||||
StreamingEncoder::WebRtc(enc) => enc.encode_frame(hw_frame),
|
StreamingEncoder::WebRtc(enc) => enc.encode_frame(hw_frame),
|
||||||
@@ -626,15 +626,22 @@ impl<S: CaptureSource> State<S> {
|
|||||||
};
|
};
|
||||||
if should_encode {
|
if should_encode {
|
||||||
let encode_start = Instant::now();
|
let encode_start = Instant::now();
|
||||||
if let Err(e) = enc.encode_frame(&surface) {
|
match enc.encode_frame(&surface) {
|
||||||
tracing::error!("encode_frame failed: {}", e);
|
Ok(stages) => {
|
||||||
self.errored = true;
|
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;
|
self.stats_frames += 1;
|
||||||
if let Some(last) = self.stats_last_time {
|
if let Some(last) = self.stats_last_time {
|
||||||
|
|||||||
+5
-4
@@ -494,7 +494,6 @@ impl StatePortal {
|
|||||||
}?;
|
}?;
|
||||||
|
|
||||||
let import_us = t_import_start.elapsed().as_micros() as u64;
|
let import_us = t_import_start.elapsed().as_micros() as u64;
|
||||||
let t_encode_start = Instant::now();
|
|
||||||
|
|
||||||
// 设置帧的显示时间戳(PTS),基于已编码帧序号
|
// 设置帧的显示时间戳(PTS),基于已编码帧序号
|
||||||
// SAFETY: vaapi_frame is the freshly-imported valid AVFrame returned by
|
// SAFETY: vaapi_frame is the freshly-imported valid AVFrame returned by
|
||||||
@@ -504,15 +503,17 @@ impl StatePortal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 送入编码器完成:缩放 → 回读 → 格式转换 → H.264 编码
|
// 送入编码器完成:缩放 → 回读 → 格式转换 → 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 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;
|
self.frames_encoded += 1;
|
||||||
|
|
||||||
// 记录帧计时到管道统计(import + encode 内部各阶段暂不可分离,用 total 覆盖)
|
// 记录帧计时到管道统计(scale 来自 filter graph;transfer 在 HW 路径恒为 0)
|
||||||
let timings = FrameTimings {
|
let timings = FrameTimings {
|
||||||
import_us,
|
import_us,
|
||||||
|
scale_us: stages.scale_us,
|
||||||
|
transfer_us: stages.transfer_us,
|
||||||
encode_us,
|
encode_us,
|
||||||
total_us,
|
total_us,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
Reference in New Issue
Block a user