Commit Graph
23 Commits
Author SHA1 Message Date
dailz f38adf70f9 fix(state_portal): gate handle_pw_frame on WebRTC paused state (closes #19)
Root cause (verified via code reading + Oracle design review):
- webrtc_paused IS correctly initialized to true in StatePortal::new()
- encode_cpu_frame DOES respect paused via early return
- BUT encode_thread_loop unconditionally sent timing on every Ok(()),
  causing stats.record_encode_thread to tick encoded_frames even for
  paused-dropped frames -> phantom encoded_fps=29.7 during idle
- Real waste: handle_pw_frame imports DMA-BUF + VAAPI scale + NV12 clone
  + crossbeam send at 60fps even when no WebRTC client is connected

Fix (Option B - surgical bugfix):
- Add EncodeOutcome enum (Encoded/SkippedPaused/SkippedDisconnected/
  SkippedDuplicate) to encode_cpu_frame return type
- encode_thread_loop only reports timing on Ok(Encoded), not on skips
  -> encoded_fps naturally stays at 0 during idle (also helps #20)
- handle_pw_frame entry: early return on paused, skipping ALL frame
  processing (DMA-BUF import, VAAPI scale, NV12 clone, channel send)
- MP4 mode unchanged (webrtc_paused is None, gate is no-op)
- Side benefit: last_fillable_frame stays None during initial idle,
  so maybe_send_filler_frame also early-returns -> no filler waste
  during the pre-connect idle window (partial mitigation for #18)

Out of scope (TODO comment added at state_portal.rs:178):
- Encoder still initializes on first PipeWire frame (one-time ~50ms
  SwEncEncode::new_webrtc cost). Full deferral (Option A) requires
  splitting WebRTC signaling lifecycle from media lifecycle - deferred
  until startup cost becomes user-perceptible
- Bitrate formula unchanged (5*W*H*fps/100) - tracked by #21

Verification (34s idle + 60s connected session):
- 0 'skipping duplicate frame' events during idle (was ~30/sec before)
- 0 BWE bitrate updates during idle
- 0 IDR production during idle (was 180 frames into the void)
- First IDR produced 178ms after connect (ForceKeyframe -> IDR in 10ms)
- cargo test transform/fps_limit/backend_detect: 34 passed
- SAFETY comments preserved verbatim
2026-06-20 19:57:15 +08:00
dailz 0aba0e651e fix(stats): real encode thread timing via SwEncodeTiming + take_timing (#17)
encode_thread_loop hardcoded sws_us=0 and output_bytes=0, making the
stats dashboard show zeros for those columns. The previous encode_us
was measured around the entire encode_cpu_frame call, mixing sws +
encode work into one bucket.

Add SwEncodeTiming { sws_us, encode_us, output_bytes } to SwEncEncode.
- drain_encoder returns Result<usize> (total encoded bytes), accumulated
  before the Muxer/Channel match so branch duplication and multi-packet
  drain are both handled correctly. output_bytes = bytes produced by
  libavcodec even if downstream delivery drops them.
- encode_cpu_frame resets last_timing to Default at entry (so early
  returns from disconnect/pause/dedup never report stale prior values),
  measures sws_us around sws_scale, measures encode_us around
  avcodec_send_frame + drain, then stores the complete snapshot.
- take_timing() uses mem::take to return and clear in one step.
- flush() ignores the byte count from drain_encoder.
- state_portal encode_thread_loop calls take_timing() for real values.
2026-06-14 08:50:47 +08:00
dailz 36cee9d9dd fix(webrtc): force IDR on KeyframeRequest via forced-idr=1 + pict_type (closes #16)
WebRTC KeyframeRequest 响应延迟 2.1s(152 P 帧丢弃)→ 端到端 5-20ms。

- BitrateCommand::ForceKeyframe variant over existing bitrate_tx channel
- SwEncEncode.force_keyframe_pending: bypass dedup, set AV_PICTURE_TYPE_I,
  clear only after avcodec_send_frame succeeds
- libx264 forced-idr=1 via av_opt_set (FFmpeg-level option, not x264-native)
- pict_type reset to NONE per frame (reused AVFrame leak guard)
- WebRtcInner.force_keyframe_to_encode + take_force_keyframe() drain API
- recreate_encoder sets force_keyframe_pending (resolution change → IDR)

Verification: skipping non-IDR frame 152→0, ForceKeyframe→IDR 4-5ms stable.
2026-06-14 08:20:06 +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 826f544569 feat(portal): async encode pipeline - decouple capture from encoding
Split synchronous encode pipeline so sws_scale + libx264 runs on a
dedicated thread, leaving only VAAPI import + GPU scale + GPU→CPU
transfer on the main capture thread.

Problem: encode_p95 occasionally hit 74ms, blocking the entire capture
pipeline and causing capture_gap_max=356ms stutter.

Solution:
- avhw.rs: Split SwEncState into SwEncImport (main thread: VAAPI import,
  filter_graph scale, GPU→CPU transfer) and SwEncEncode (encode thread:
  sws_scale NV12→YUV420P, libx264 encode). New CpuNv12Frame struct
  carries owned pixel data across threads via crossbeam channel.
  SwEncState wraps both for backward compat (MP4/sync path untouched).
- state_portal.rs: WebRTC portal path spawns 'wl-webrtc-encode' thread
  with bounded(2) input channel (drop-newest backpressure) and separate
  timing channel. Graceful shutdown: drop webrtc_rx → drop input_tx →
  join encode thread → flush sync encoder.
- stats.rs: Add record_import() + record_encode_thread() for async timing.

Results: encode_p95 stable at 2.9-4.2ms (was 11-74ms), capture_fps
stable 59-60fps, cap_gap_p95 17-19ms. Remaining capture stalls traced
to PipeWire compositor frame delivery (external, not our code).
2026-06-07 16:55:28 +08:00
dailz 36f07c92e9 fix(state_portal): prevent shutdown deadlock on full bounded channel (closes #8)
shutdown() calls enc.flush() → drain_encoder() → tx.send() on a
crossbeam bounded(32) channel.  If the channel is full and the
receiver (webrtc_rx) is alive but not being drained, send() blocks
forever — a self-deadlock since both ends belong to the same struct.

Two-layer fix:
- avhw.rs: replace tx.send() with tx.try_send(); handle Full (drop
  frame) and Disconnected (set flag) separately.
- state_portal.rs: drop webrtc_rx before flushing in shutdown() so
  try_send returns Disconnected immediately.

Regression tests added for the channel semantics.
2026-06-06 20:02:09 +08:00
dailz 7c1c9b2e19 fix(avhw): add SAFETY comments to all undocumented unsafe blocks
Close #7

- Add // SAFETY: comments to 19 undocumented unsafe blocks and impls
- Add nb_streams/null guard on stream array dereference (drain_encoder)
- Add clippy undocumented_unsafe_blocks = warn lint to prevent regression

avhw.rs now has 0 clippy unsafe documentation warnings.
2026-06-06 15:54:09 +08:00
dailz 226768c3e3 fix(avhw): handle tx.send() failure and pause encoding on WebRTC disconnect (closes #6)
- Replace 'let _ = tx.send()' with proper error handling: log warning,
  set webrtc_disconnected flag, and break drain loop on SendError
- Add Arc<AtomicBool> webrtc_paused shared between State/StatePortal
  and SwEncState, synced from wrtc.is_connected() in poll_webrtc()
- Skip encoding in encode_filtered_frame() when paused or disconnected
- Drain and discard stale channel frames on disconnect
- Resume encoding automatically on WebRTC reconnection
2026-06-06 15:12:49 +08:00
dailz fd170b66d9 fix(unsafe): add SAFETY comment and runtime guards for from_raw_parts in drain_encoder
Issue: #5

- Read AVPacket fields into local variable to avoid repeated pointer deref
- Guard against size <= 0 (prevents c_int negative wrap to huge usize)
- Guard against null data pointer (from_raw_parts(null, 0) is UB in Rust)
- Add SAFETY comment matching existing codebase convention (30+ instances)
2026-06-06 11:56:47 +08:00
dailz b0ed6548a6 feat: add WebRTC streaming via str0m + portal session persistence
- Add src/webrtc.rs: HTTP signaling server + str0m Sans-IO WebRTC transport
  with H.264 Annex-B → RTP packetization and key-frame request handling
- avhw: introduce FrameOutput enum (Muxer | Channel) so SwEncState can
  output to either MP4 muxer or crossbeam channel for WebRTC
- cap_portal: support portal session restore tokens (PersistMode::ExplicitlyRevoked)
  to skip re-authorization dialog; add --no-persist flag to force fresh dialog
- args: make --output optional when --port is used for WebRTC mode
- state_portal: integrate WebRTC pipeline (encoder channel → RTP forwarding)
  with shorter GOP for WebRTC (fps/2, min 10)
- main: redirect tracing to stderr; validate --output or --port required
- Add dependencies: str0m 0.20, serde_json 1, dirs 6
2026-06-04 20:54:16 +08:00
dailz 74f4dc826d perf(portal): achieve 58-60fps PipeWire screen capture
- Force PipeWire quantum=512 via NODE_FORCE_QUANTUM (48000/512=93Hz scheduling)
- Switch to libx264 ultrafast/zerolatency with 6 threads
- Use two-phase poll_and_encode: blocking recv_timeout for first frame,
  non-blocking try_recv drain for subsequent frames
- Remove fps_limit from portal path (PW already rate-limits via quantum/KWin;
  fps_limit's min_interval was silently dropping ~10% of valid frames)
- Remove diagnostic instrumentation (TIMING/PIPEWIRE logs, timing fields,
  pw_stats counters)
- Add lightweight production stats: per-10s fps log + shutdown summary
- Prefer libx264 over libopenh264 (better quality at same speed)
2026-05-30 08:44:15 +08:00
dailz d80b34f44f feat: GPU-downscale + software H.264 encode pipeline (WIP)
Add SwEncState in avhw.rs: GPU pipeline using scale_vaapi to downscale
4K BGRA -> 2K NV12 on AMD iGPU, then software encode with libopenh264.

- import_dma_buf_to_vaapi: av_hwframe_map based DMA-BUF import
- SwEncState: GPU filter graph (scale_vaapi) + NV12->YUV420P + libopenh264
- state_portal.rs: integrated SwEncState, auto DRM device detection
- vaapi_import_bench.rs: CPU vs GPU pipeline benchmark
- sw_encode_bench.rs: software encode benchmark

Benchmark results: GPU pipeline ~91 FPS theoretical (10.95ms/frame)
vs CPU pipeline ~33 FPS (30.21ms/frame).

Known issue: only 1 frame encoded in production recording,
diagnostic STATS logging added to debug frame flow.
2026-05-29 22:04:12 +08:00
dailz 5100d78aa8 fix: resolve SHM hang, DRM device mismatch, and duplicate VAAPI context
BUG-2 (HIGH): SHM Buffer event caused permanent hang
  In the ZwlrScreencopyFrameV1 dispatcher, receiving a SHM Buffer event
  left in_flight_surface stuck at AllocQueued forever, preventing
  queue_alloc_frame() from requesting new frames.
  Fix: treat Buffer as a metadata offer (v3 protocol), wait for
  BufferDone to decide failure, and add AllocQueued state guard to
  LinuxDmabuf handler.

BUG-3 (MEDIUM): Portal backend picked wrong GPU on multi-GPU systems
  state_portal.rs hardcoded /dev/dri/renderD128 then renderD129, which
  selects the wrong GPU when PipeWire uses a different device.
  Fix: extract find_drm_render_nodes() as shared utility; defer DRM
  device selection to first PipeWire frame; test each candidate with
  av_hwframe_transfer_data to find the GPU that can actually import
  the DMA-BUF frame.

BUG-4 (LOW): VAAPI device context created twice unnecessarily
  try_finalize_output() created an AvHwDevCtx stored in EverythingButFmt,
  but negotiate_format() discarded it (_hw_device_ctx) and EncState::new
  created a new one.
  Fix: thread the existing hw_device_ctx through negotiate_format() and
  create_encoder() to EncState::new() which reuses it when provided.
2026-05-25 14:32:58 +08:00
dailz d7fbb5256c feat: add KWin/KDE Plasma screen capture via xdg-desktop-portal ScreenCast + PipeWire
Add a second capture backend for compositors without wlr-screencopy
(KWin, GNOME, etc.) using the xdg-desktop-portal ScreenCast interface
and PipeWire DMA-BUF streaming.

New files:
- src/backend_detect.rs: auto-detect wlr-screencopy vs portal backend
- src/cap_portal.rs: Portal session setup + PipeWire DMA-BUF thread
- src/state_portal.rs: StatePortal encoder pipeline (DMA-BUF → VAAPI)

Changes:
- Cargo.toml: add ashpd 0.13, tokio 1, pipewire 0.9, libspa 0.9,
  crossbeam-channel 0.5
- src/args.rs: add --backend CLI flag
- src/avhw.rs: extract create_encoder() from inline State code
- src/main.rs: route to portal or wlr-screencopy based on backend
- src/state.rs: fix params.destroy() on dup failure, cleanup
  in_flight_surface on copy fail, use create_encoder()
- tests/integration_test.rs: add --backend flag tests
2026-05-11 08:49:08 +08:00
dailz 2972216a02 fix(avhw): specify format=nv12 output for scale_vaapi filter
scale_vaapi defaults to the input sw_format (RGBZ) when no output format
is specified. h264_vaapi encoder only supports NV12/YUV formats.
Adding format=nv12 ensures the filter outputs the correct color format
for hardware encoding.
2026-04-15 15:01:06 +08:00
dailz 1065364261 fix(avhw): add missing mut on video_filter for buffersink access 2026-04-15 14:41:58 +08:00
dailz 833cbdfed3 chore(avhw): clean up debug tracing in VA-API pipeline 2026-04-15 14:01:22 +08:00
dailz c77838235a fix(avhw): derive encoder hw_frames_ctx from filter graph buffersink 2026-04-15 13:57:34 +08:00
dailz 506e5ea30e fix(state,avhw): bind initial globals manually + fix filter graph crash on niri
- registry_queue_init consumes registry events during its internal
  roundtrip without forwarding them to Dispatch<WlRegistry>. Added
  bind_initial_globals() to manually iterate GlobalList and bind all
  initial globals (wl_output, xdg_output_manager, dmabuf, screencopy,
  wlr_output_manager) at State::new time.
- Fix av_freep segfault in build_filter_graph: av_buffersrc_parameters_alloc
  returns a plain pointer, use av_free instead of av_freep (which expects
  pointer-to-pointer).
- Fix filter graph format negotiation: remove software format filter that
  broke scale_vaapi hardware pipeline. Chain is now src -> scale -> sink.
- Downgrade repeat_pps error to warning (not available in FFmpeg 6.x).
2026-04-14 20:42:05 +08:00
dailzandSisyphus ecd78492ee feat(avhw): integrate transform into VA-API filter graph
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-14 17:02:54 +08:00
dailz 1e8d00126b fix(bsf): remove non-functional BSF pipeline, use encoder repeat_pps option 2026-04-06 09:35:37 +08:00
dailz 6d49222de8 feat: Phase 1 MVP with audit fixes — Wayland screen capture + VAAPI encoding
Phase 1 MVP implementation of wl-webrtc: Wayland screen capture tool
with hardware-accelerated VAAPI H.264 encoding and WebTransport output.

Includes all 9 runtime bug fixes from code audit (fix-audit-issues plan):

CRITICAL:
- C2: h264_metadata BSF with repeat_sps/repeat_pps in encode pipeline
- C4: FpsLimit wired as timing gate in on_copy_complete

HIGH:
- C3+A2: DRM device discovery via dmabuf feedback MainDevice event,
  unified resolve_drm_path() helper (CLI > compositor > auto > fallback)
- H2: Separate physical_size (mm) from mode_size (pixels) in wl_output
- H1+A3: Multi-output warning + named-output-not-found error

MEDIUM:
- M5: tv_sec u32->u64 to avoid Y2106 timestamp truncation
- M4: Guard against SHM Buffer event (DMA-BUF only)

Key components:
- src/avhw.rs: FFmpeg VAAPI encoder + filter graph + BSF pipeline
- src/state.rs: Wayland event loop + output negotiation + screencopy
- src/cap_wlr_screencopy.rs: wlr-screencopy capture source
- src/fps_limit.rs: Frame rate limiting with configurable target
- src/transform.rs: Frame format conversion utilities
2026-04-05 23:35:00 +08:00