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:
dailz
2026-06-28 14:22:45 +08:00
parent 2ac37a1dd1
commit a6560cff6c
3 changed files with 72 additions and 17 deletions
+16 -9
View File
@@ -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,15 +626,22 @@ 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;
}
}
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 {