Oracle-driven P1 fix plan. Resolves the StatsSnapshot 'computed but never
consumed' debt that was silently zeroing two real diagnostic fields and
leaving a dozen more unreported.
Bug fix (Oracle step 2):
- state_portal.rs: set_pipewire_dropped(0, 0) and set_queue_depths(0, 0)
were hardcoded, silently discarding real PipeWire diagnostics. Now wires
to self.cap.dropped_count() (with pw_dropped_prev delta tracking) and
self.cap.capture_queue_depth(). The encoded side stays 0 because the
encoder thread exposes no queue-depth API.
Display expansion (Oracle step 1):
- stats.rs: StatsSnapshot::Display now reports 12 previously-silent fields
paired with their existing p95/max counterparts — capture/encoded/sent
frame counts, elapsed_secs, *_avg_ms gap timing, frame_age_avg_ms,
per-stage import/sws/encode/total avg_ms, output_frame_bytes_p95. Each
line of the format string maps to one operational question (cadence,
drops, queue pressure, latency, bandwidth); layout note added.
Dead residue purge (Oracle steps 5 + 6):
- stats.rs: removed record_over_budget method + over_budget_count field
(no caller; total_p95_ms answers the useful question without an
arbitrary budget threshold).
- state.rs: removed InFlightSurface::Allocd variant (never constructed)
and CaptureSource::alloc_frame trait method (prototype leftover; the
sole impl in cap_wlr_screencopy.rs returned None unconditionally).
- cap_wlr_screencopy.rs: removed the alloc_frame stub; updated the
unit-type Frame doc to reference the asynchronicity rationale without
the deleted method.
- cap_portal.rs: removed redundant 'let dropped = dropped;' shadowing
flagged by clippy::redundant_locals (line 849).
Deferred (Oracle step 4 — needs product decision):
- scale_avg/scale_p95/transfer_avg/transfer_p95/send_wait_p95 fields
still appear in Display but producers in the live encode path don't
record them, so they often show misleading zeros. Either add real
EncState timing for scale/transfer stages, or remove the fields from
Display until then.
All 79 unit tests + 3 integration tests still pass. clippy: 0 errors.
Warning count: multiple_fields_never_read on StatsSnapshot,
method_never_used on record_over_budget/dropped_count/capture_queue_depth/
alloc_frame, variant_never_constructed on Allocd, redundant_locals on
dropped — all gone.
Audit-driven cleanup pass. End state:
- cargo clippy --release --all-targets: 0 errors (was 4)
- undocumented_unsafe_blocks warnings: 0 (was 67)
- Cargo.toml: undocumented_unsafe_blocks escalated warn -> deny
Clippy correctness errors fixed:
- src/bin/{sw_encode_bench,vaapi_import_bench}.rs: receive_first_frame
rewritten per Oracle plan with total 10s deadline + 200ms wait slice +
while-let drain of all control events. The previous loop body always
exited on first iteration (never_loop); the new version actually retries
and matches production's repeated-poll semantics in state_portal.rs.
- src/avhw.rs: hash_sampled_y_plane tests now use a row_range(row, stride,
width) helper instead of inline stride * N. Preserves the row-index
intent across all sibling tests without tripping erasing_op (row==0) or
identity_op (row==1).
Machine-applicable clippy autofixes applied via 'cargo clippy --fix':
- unnecessary_cast, manual_is_multiple_of, needless_borrows_for_generic_args
- manual_abs_diff, derivable_impls, new_without_default
- unnecessary_map_or, unneeded_struct_pattern, redundant_locals
webrtc_gop_formula test rewritten to wrap the (fps * 2).max(20) formula in
a runtime lambda. The previous clippy --fix pass had constant-folded the
5fps case into assert_eq!(20, 20), silently stripping the floor-case
coverage. The lambda blocks the fold while keeping the formula exercisable.
67 SAFETY comments added across 7 files (cap_portal.rs 26, sw_encode_bench
21, state_portal.rs 7, vaapi_import_bench.rs 6, avhw.rs 5, state.rs 1,
main.rs 1). Two sites carry load-bearing invariant documentation:
- cap_portal.rs:806 process callback documents the PipeWire raw_buf
ownership contract across all 10 exit paths (audited: every path
correctly requeues; fd ownership via dup() is independent and also
exactly-once closed).
- avhw.rs:341 unsafe impl Send for EncState documents the single-thread
exclusivity assumption referenced by AGENTS.md.
All 97 unit tests + 3 integration tests still pass; cargo build --release
finishes clean. Lint escalation to deny freezes the SAFETY baseline: any
future patch adding an unsafe block without a // SAFETY: comment will fail
clippy at compile time.
Final piece of #20. The EncodeOutcome::SkippedDuplicate variant was
introduced in #19 but its count was invisible — silent Ok(_) arm in
encode_thread_loop. Now exposed as a stat.
Changes:
- stats.rs: PipelineStats gains duplicate_frames_skipped (window delta)
and prev_duplicate_frames_skipped (running total). Snapshot field
added. Display format places it after over_budget (both are counters).
Reset clears window delta but preserves running total (same pattern
as pipewire_dropped).
- state_portal.rs: EncodeThread struct gains duplicate_count:
Arc<AtomicU64>. Cloned for encode_thread_loop, stored for main-thread
reads. encode_thread_loop now explicitly matches SkippedDuplicate and
increments with Ordering::Relaxed. Stats snapshot code reads atomic
and calls set_duplicate_frames_skipped after timing drain.
What this enables:
Diagnosing encoded_fps health. Examples:
- capture_fps=60 encoded_fps=30 duplicate_frames_skipped=30
→ healthy: encoder at 30fps target, 30 frames were true duplicates
- capture_fps=60 encoded_fps=5 duplicate_frames_skipped=0
→ problem: frames not being dedup'd but encoder can't keep up
- capture_fps=1.7 encoded_fps=1.7 duplicate_frames_skipped=0
→ healthy static: low fps because KWin damage-driven delivery
Original #20 issues status:
- 'encoded_fps stuck at ~30': FIXED via #19 (EncodeOutcome enum), now
tracks capture_fps when below 30
- 'filler masking real fps': FIXED via #15/#18 (filler deleted)
- 'duplicate count invisible': FIXED via this commit
- 'unique_encoded_fps / delivered_fps': not implemented, deemed
unnecessary now that the core metrics are trustworthy
Tests:
- cargo build --release: 0 new warnings (19 baseline preserved)
- cargo test: 96 lib + 3 integration, 0 failed
- SAFETY comments preserved verbatim
- 2 files changed, +45/-6 lines
Add quantitative capture-to-send latency measurement so we can diagnose
remaining latency sources after #24/#25 PTS fixes.
Previously frame_age_p95 was always 0.0ms because the WebRTC code path
never propagated capture timestamps, even though stats.rs already
supported the metric. The infrastructure existed but was disconnected.
Changes:
- avhw.rs: Add capture_time: Instant field to CpuNv12Frame (set when
PipeWire delivers frame) and EncodedH264Frame (propagated through
encode thread via new last_capture_time side-channel on SwEncEncode).
- state_portal.rs: Change sent_gap channel type from Sender<f64> to
Sender<(f64, Option<f64>)> so WebRTC thread can send pre-computed
age_ms = capture_time.elapsed() at the exact send moment (not at
stats drain time, which would inflate the measurement by ~1s).
- stats.rs: record_send_from_thread now accepts Option<f64> age_ms
and pushes to frame_age_ms Vec when Some.
After this commit:
- stats: log lines show real frame_age_p95 / frame_age_max in ms
- Expected range: 5-30ms (import + scale + encode + channel send)
- If much higher: server pipeline has queueing issue
- If low but user still sees latency: confirms bottleneck is network
or browser-side (jitter buffer, decode queue)
Scope notes:
- Only Portal/PipeWire path is instrumented. wlr-screencopy path uses
different code path (EncState, not SwEncState) and will continue to
report frame_age=0.0ms. Adding wlr instrumentation is separate scope.
- This is diagnostic only — does NOT change user-visible behavior.
No encoding, sending, or stats output format changes.
Tests:
- cargo build --release: 0 new warnings (19 baseline preserved)
- cargo test: 96 lib + 3 integration, 0 failed
- SAFETY comments preserved verbatim
- 3 files changed, +39/-10 lines
Refs #20.
WebRTC client bandwidth estimate now drives both encoder bitrate and
resolution tier selection, replacing the previous static-target encoder.
- webrtc.rs: enable str0m BWE (seeded at 5 Mbps), surface
EgressBitrateEstimate + KeyframeRequest events, expose
get_bwe_estimate() / set_need_keyframe()
- state_portal.rs: wire bitrate/resolution channels between the WebRTC
thread and the encode thread; tier ladder [1440p, 1080p, 720p] with
downscale at 60% budget and upscale hysteresis (120% sustained 10s)
- avhw.rs: SwEncImport::poll_resolution_commands() rebuilds the import
filter graph on UpdateResolution; SwEncEncode::recreate_encoder()
rebuilds sws/enc_video/yuv_frame atomically; hash_sampled_y_plane()
skips duplicate frames; VBV x264opts cap IDR bursts; H.264 level 4.0
(muxer) / 4.2 (WebRTC)
- state.rs: sync wlr-screencopy GOP to fps*2 max 20 for parity
- fix: drain bitrate_rx + resolution_rx BEFORE the stride check in
encode_cpu_frame() so the new (smaller-stride) frame produced after
a resolution change does not hit the stale (larger) enc_width and
crash the encode thread
- WebRTC GOP widened to fps*2 max 20 (was fps/2 max 10)
- Move WebRTC send to dedicated wl-webrtc-webrtc thread (was inline in main loop)
- Reduce frame_rx 16→1, input_tx 2→1 (drop-on-full), webrtc_tx 32→2
- Recv_timeout 10ms→2ms to reduce pipeline latency
- Fix sent_gap_p95 stats bug: compute gap at actual send time in WebRTC
thread instead of batch-draining at snapshot time (was always 0.0ms)
- High profile via AVCodecContext.profile, veryfast preset, 5x bitrate
- Stats drain via sent_gap channel with record_send_from_thread()
- Shutdown: drop input_tx → join encode → drop webrtc_tx → join webrtc
Add lightweight per-second pipeline statistics for stutter diagnosis:
- --stats CLI flag enables structured stats logging
- PipelineStats tracks capture/encode/send timing with p95/pmax
- FrameTimings records import/scale/transfer/sws/encode per-frame
- StatsSnapshot produces one structured log line per second