Commit Graph
8 Commits
Author SHA1 Message Date
dailz 75ad4bba78 style: apply rustfmt to establish clean baseline before refactor
Pre-refactor baseline state:
- 79 lib tests + 3 integration tests pass (1 integration test #[ignore])
- cargo clippy --all-targets -- -D warnings clean
- cargo build --release clean

No semantic changes; only rustfmt drift correction across 6 files.
2026-07-13 15:40:54 +08:00
dailz 2ac37a1dd1 fix(stats): wire PipeWire drops, expand Display, purge dead residue
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.
2026-06-28 14:15:55 +08:00
dailz 30f8fe51f2 chore: clear clippy errors, document all unsafe blocks, deny new SAFETY debt
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.
2026-06-28 13:44:27 +08:00
dailz a06a41f5f2 feat(stats): expose duplicate_frames_skipped counter (closes #20)
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
2026-06-21 10:04:41 +08:00
dailz b4b9990efe feat(stats): populate frame_age metric for WebRTC path (partial #20)
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.
2026-06-20 23:20:36 +08:00
dailz 3e60258627 feat(portal): BWE-driven resolution adaptation + duplicate frame skipping
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)
2026-06-13 22:46:33 +08:00
dailz 503e4dbc22 feat(portal): independent WebRTC thread + channel tuning for 60fps mouse latency
- 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
2026-06-07 18:30:09 +08:00
dailz 029fe13e37 feat(stats): add --stats flag and PipelineStats windowed diagnostics
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
2026-06-07 16:54:45 +08:00