chore: design cleanup, dead-code purge, README/doc refresh
Audit-driven follow-up after the SAFETY-debt commit (Oracle steps 6-7).
End state: cargo clippy --release --all-targets still 0 errors; private_interfaces
and type_complexity warnings cleared.
Design cleanups (Oracle step 6):
- cap_portal.rs: introduce PortalFormatInfo struct to replace the
Rc<Cell<Option<(u32,u32,u32,u64)>>> cross-callback hand-off. Self-
documenting struct fields replace positional tuple access at the
format-change and process callbacks.
- avhw.rs: import_dma_buf_to_vaapi signature collapses from 8 args
(fd/width/height/drm_format/modifier/stride/offset) to
(*mut AVBufferRef, &PwDmaBufFrame). Callers in avhw.rs,
state_portal.rs, and vaapi_import_bench.rs now pass the frame by
reference instead of unpacking 7 fields just to repack them. Drops
the unused width parameter and the too_many_arguments(8/7) warning.
- state.rs: visibility hygiene. EncConstructionStage and WlrHeadInfo
downgrade pub -> pub(crate); State.stage field downgrades to
pub(crate). These are internal state-machine types not exposed
across the crate boundary; making them pub(crate) clears all
private_interfaces warnings without leaking more types.
Dead-code purge (Oracle step 7):
- transform.rs: remove unused Rect struct, transform_basis,
screen_to_frame, fit_inside_bounds helpers and their 18 dedicated
tests. Transform enum and transpose_if_transform_transposed remain
(both are actively used by state.rs and avhw.rs). File shrinks
from 409 -> 109 lines.
Repository housekeeping (Oracle step 7):
- .gitignore: add review.json (stray review-tool output that
regenerates per run).
- README.md: refresh CLI table to match src/args.rs (now lists
--backend, --no-persist, --port-as-WebRTC-signaling, --max-bitrate,
--stats). Add capture-backend explainer + 4 new usage examples.
Note in README points readers at src/args.rs as the authoritative
source. Remove stale 'WebTransport, unused in MVP' description.
avhw.rs: AsRawFd import annotated with a rustc-quirk explanation — the
import triggers a false 'unused_imports' warning but E0599 if removed.
Left as-is with explanatory comment rather than chasing the lint.
All 79 remaining unit tests + 3 integration tests still pass. Cargo
build --release clean.
This commit is contained in:
@@ -21,3 +21,6 @@ Thumbs.db
|
|||||||
.playwright-mcp/
|
.playwright-mcp/
|
||||||
wl-webrtc.log
|
wl-webrtc.log
|
||||||
webrtc-p0-success.png
|
webrtc-p0-success.png
|
||||||
|
|
||||||
|
# Stray review-tool output (regenerated per review run)
|
||||||
|
review.json
|
||||||
|
|||||||
@@ -38,19 +38,46 @@ wl-webrtc --output output.mp4 --drm-device /dev/dri/renderD128
|
|||||||
|
|
||||||
# Verbose mode
|
# Verbose mode
|
||||||
wl-webrtc --output output.mp4 -v
|
wl-webrtc --output output.mp4 -v
|
||||||
|
|
||||||
|
# WebRTC streaming mode (HTTP signaling server)
|
||||||
|
wl-webrtc --port 8080 -v
|
||||||
|
|
||||||
|
# Force a fresh portal authorization dialog (ignore saved restore token)
|
||||||
|
wl-webrtc --output output.mp4 --no-persist
|
||||||
|
|
||||||
|
# Pin the capture backend instead of auto-detecting
|
||||||
|
wl-webrtc --output output.mp4 --backend portal # or: --backend screencopy
|
||||||
```
|
```
|
||||||
|
|
||||||
## CLI Arguments
|
## CLI Arguments
|
||||||
|
|
||||||
|
> `src/args.rs` is the authoritative source. Run `wl-webrtc --help` for the live list.
|
||||||
|
|
||||||
| Argument | Default | Description |
|
| Argument | Default | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `-o`, `--output` | (required) | Output file path (e.g., output.mp4) |
|
| `-o`, `--output` | (optional) | Output file path (e.g. output.mp4). Optional when using `--port` for WebRTC mode. |
|
||||||
| `--output-name` | auto | Wayland output name to capture |
|
| `--output-name` | auto | Wayland output name to capture |
|
||||||
| `--fps` | 30 | Target frames per second |
|
| `--fps` | 30 | Target frames per second |
|
||||||
| `--codec` | h264 | Video codec (h264 only for MVP) |
|
| `--codec` | h264 | Video codec (h264 only for MVP) |
|
||||||
| `--hw-accel` | vaapi | Hardware acceleration method |
|
| `--hw-accel` | vaapi | Hardware acceleration method |
|
||||||
| `--drm-device` | auto | DRM render device path |
|
| `--drm-device` | auto | DRM render device path |
|
||||||
| `--bitrate` | auto | Target bitrate in bps |
|
| `--bitrate` | auto | Target bitrate in bps |
|
||||||
|
| `--max-bitrate` | 8000000 | Max bitrate cap for WebRTC mode (caps BWE escalation; no effect in MP4 mode) |
|
||||||
| `--gop-size` | auto | Group of Pictures size |
|
| `--gop-size` | auto | Group of Pictures size |
|
||||||
| `-v`, `--verbose` | false | Enable verbose logging |
|
| `-v`, `--verbose` | false | Enable verbose logging |
|
||||||
| `--port` | 0 | WebTransport server port (unused in MVP) |
|
| `--backend` | auto | Capture backend: `screencopy` (wlroots) or `portal` (KWin/KDE). Auto-detected if omitted. |
|
||||||
|
| `--port` | 0 | WebRTC HTTP signaling server port. `0` keeps MP4 file output mode. |
|
||||||
|
| `--no-persist` | false | Force re-authorization (ignore saved portal restore token) |
|
||||||
|
| `--stats` | false | Print per-second pipeline statistics for stutter diagnosis |
|
||||||
|
|
||||||
|
## Capture backends
|
||||||
|
|
||||||
|
The tool supports two Wayland capture backends, auto-detected by default:
|
||||||
|
|
||||||
|
- **wlr-screencopy** (preferred when `zwlr_screencopy_manager_v1` is advertised):
|
||||||
|
works on wlroots-based compositors (Sway, Hyprland, etc.).
|
||||||
|
- **XDG Portal / PipeWire** (fallback when D-Bus ScreenCast is available):
|
||||||
|
works on KWin/KDE and any compositor that implements the XDG Desktop Portal
|
||||||
|
screen-cast protocol. The first run shows an authorization dialog; a restore
|
||||||
|
token is cached under `wl-webrtc/portal-restore-token` so subsequent runs
|
||||||
|
don't re-prompt (use `--no-persist` to force a fresh authorization).
|
||||||
|
|||||||
+26
-30
@@ -1,6 +1,9 @@
|
|||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use std::os::fd::{AsRawFd, RawFd};
|
// AsRawFd is required by `frame.fd.as_raw_fd()` below but rustc emits a false
|
||||||
|
// "unused_imports" warning because OwnedFd also has an inherent `as_raw_fd`.
|
||||||
|
// E0599 if removed → must stay; warning is a known rustc quirk.
|
||||||
|
use std::os::fd::AsRawFd;
|
||||||
use std::os::raw::c_void;
|
use std::os::raw::c_void;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::ptr;
|
use std::ptr;
|
||||||
@@ -197,16 +200,7 @@ pub fn test_dma_buf_import(drm_device: &Path, frame: &PwDmaBufFrame) -> Result<(
|
|||||||
|
|
||||||
// SAFETY: frames is a live VAAPI frames context; frame carries valid DMA-BUF metadata.
|
// SAFETY: frames is a live VAAPI frames context; frame carries valid DMA-BUF metadata.
|
||||||
unsafe {
|
unsafe {
|
||||||
import_dma_buf_to_vaapi(
|
import_dma_buf_to_vaapi(frames.as_ptr(), frame)
|
||||||
frames.as_ptr(),
|
|
||||||
frame.fd.as_raw_fd(),
|
|
||||||
frame.width,
|
|
||||||
frame.height,
|
|
||||||
frame.format,
|
|
||||||
frame.modifier,
|
|
||||||
frame.stride,
|
|
||||||
frame.offset,
|
|
||||||
)
|
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -215,19 +209,22 @@ pub fn test_dma_buf_import(drm_device: &Path, frame: &PwDmaBufFrame) -> Result<(
|
|||||||
/// Import a DMA-BUF into a VAAPI hardware frame via zero-copy `av_hwframe_map`.
|
/// Import a DMA-BUF into a VAAPI hardware frame via zero-copy `av_hwframe_map`.
|
||||||
///
|
///
|
||||||
/// # Safety
|
/// # Safety
|
||||||
|
/// Imports a DMA-BUF frame into a VAAPI hardware frame pool for GPU-side processing.
|
||||||
|
///
|
||||||
|
/// Takes the negotiated format/geometry from `frame` (a `PwDmaBufFrame` from
|
||||||
|
/// PipeWire capture) plus the target `frames_ctx` (VAAPI frame pool from
|
||||||
|
/// `AvHwFrameCtx`) and returns an `ff::frame::Video` whose data[3] points to
|
||||||
|
/// the hardware frame.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
/// - `frames_ctx` must point to an initialized AVHWCramesContext for VAAPI
|
/// - `frames_ctx` must point to an initialized AVHWCramesContext for VAAPI
|
||||||
/// - `raw_fd` must be a valid DMA-BUF file descriptor
|
/// - `frame.fd` must be a valid DMA-BUF file descriptor
|
||||||
pub unsafe fn import_dma_buf_to_vaapi(
|
pub unsafe fn import_dma_buf_to_vaapi(
|
||||||
frames_ctx: *mut ffi::AVBufferRef,
|
frames_ctx: *mut ffi::AVBufferRef,
|
||||||
raw_fd: RawFd,
|
frame: &PwDmaBufFrame,
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
drm_format: u32,
|
|
||||||
modifier: u64,
|
|
||||||
stride: u32,
|
|
||||||
offset: u64,
|
|
||||||
) -> Result<ff::frame::Video> {
|
) -> Result<ff::frame::Video> {
|
||||||
let duped_fd = libc::dup(raw_fd);
|
let duped_fd = libc::dup(frame.fd.as_raw_fd());
|
||||||
if duped_fd < 0 {
|
if duped_fd < 0 {
|
||||||
bail!("dup(fd) failed: {}", std::io::Error::last_os_error());
|
bail!("dup(fd) failed: {}", std::io::Error::last_os_error());
|
||||||
}
|
}
|
||||||
@@ -235,14 +232,14 @@ pub unsafe fn import_dma_buf_to_vaapi(
|
|||||||
let mut desc: ffi::AVDRMFrameDescriptor = mem::zeroed();
|
let mut desc: ffi::AVDRMFrameDescriptor = mem::zeroed();
|
||||||
desc.nb_objects = 1;
|
desc.nb_objects = 1;
|
||||||
desc.objects[0].fd = duped_fd;
|
desc.objects[0].fd = duped_fd;
|
||||||
desc.objects[0].size = (height as usize) * (stride as usize);
|
desc.objects[0].size = (frame.height as usize) * (frame.stride as usize);
|
||||||
desc.objects[0].format_modifier = modifier;
|
desc.objects[0].format_modifier = frame.modifier;
|
||||||
desc.nb_layers = 1;
|
desc.nb_layers = 1;
|
||||||
desc.layers[0].format = drm_format;
|
desc.layers[0].format = frame.format;
|
||||||
desc.layers[0].nb_planes = 1;
|
desc.layers[0].nb_planes = 1;
|
||||||
desc.layers[0].planes[0].object_index = 0;
|
desc.layers[0].planes[0].object_index = 0;
|
||||||
desc.layers[0].planes[0].offset = offset as isize;
|
desc.layers[0].planes[0].offset = frame.offset as isize;
|
||||||
desc.layers[0].planes[0].pitch = stride as isize;
|
desc.layers[0].planes[0].pitch = frame.stride as isize;
|
||||||
|
|
||||||
let desc_box = Box::new(desc);
|
let desc_box = Box::new(desc);
|
||||||
let desc_ptr = Box::into_raw(desc_box);
|
let desc_ptr = Box::into_raw(desc_box);
|
||||||
@@ -264,8 +261,8 @@ pub unsafe fn import_dma_buf_to_vaapi(
|
|||||||
{
|
{
|
||||||
let sp = src.as_mut_ptr();
|
let sp = src.as_mut_ptr();
|
||||||
(*sp).format = ffi::AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32;
|
(*sp).format = ffi::AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32;
|
||||||
(*sp).width = width as i32;
|
(*sp).width = frame.width as i32;
|
||||||
(*sp).height = height as i32;
|
(*sp).height = frame.height as i32;
|
||||||
(*sp).data[0] = (*buf_ref).data;
|
(*sp).data[0] = (*buf_ref).data;
|
||||||
(*sp).buf[0] = buf_ref;
|
(*sp).buf[0] = buf_ref;
|
||||||
}
|
}
|
||||||
@@ -509,11 +506,10 @@ impl EncState {
|
|||||||
// avformat_query_codec checks codec+format compatibility; both pointers are
|
// avformat_query_codec checks codec+format compatibility; both pointers are
|
||||||
// valid and FF_COMPLIANCE_NORMAL is a constant. All three reads happen in one
|
// valid and FF_COMPLIANCE_NORMAL is a constant. All three reads happen in one
|
||||||
// block so a single SAFETY rationale covers them.
|
// block so a single SAFETY rationale covers them.
|
||||||
let (codec_id, oformat, compat) = unsafe {
|
let compat = unsafe {
|
||||||
let codec_id = (*enc_video.as_ptr()).codec_id;
|
let codec_id = (*enc_video.as_ptr()).codec_id;
|
||||||
let oformat = (*fmt_ctx_ptr).oformat;
|
let oformat = (*fmt_ctx_ptr).oformat;
|
||||||
let compat = ffi::avformat_query_codec(oformat, codec_id, ffi::FF_COMPLIANCE_NORMAL);
|
ffi::avformat_query_codec(oformat, codec_id, ffi::FF_COMPLIANCE_NORMAL)
|
||||||
(codec_id, oformat, compat)
|
|
||||||
};
|
};
|
||||||
if compat < 0 {
|
if compat < 0 {
|
||||||
bail!("H.264 codec not supported by output container format");
|
bail!("H.264 codec not supported by output container format");
|
||||||
|
|||||||
@@ -417,17 +417,10 @@ fn import_frame(
|
|||||||
) -> Result<ff::frame::Video> {
|
) -> Result<ff::frame::Video> {
|
||||||
// SAFETY: frames_ctx is a live VAAPI frames context configured for the capture format; frame
|
// SAFETY: frames_ctx is a live VAAPI frames context configured for the capture format; frame
|
||||||
// carries a valid DMA-BUF fd and metadata from PipeWire for the duration of the call.
|
// carries a valid DMA-BUF fd and metadata from PipeWire for the duration of the call.
|
||||||
|
// SAFETY: frames_ctx is a valid VAAPI frames context; `frame` carries the
|
||||||
|
// DMA-BUF metadata read by the function.
|
||||||
unsafe {
|
unsafe {
|
||||||
import_dma_buf_to_vaapi(
|
import_dma_buf_to_vaapi(frames_ctx.as_ptr(), frame)
|
||||||
frames_ctx.as_ptr(),
|
|
||||||
frame.fd.as_raw_fd(),
|
|
||||||
frame.width,
|
|
||||||
frame.height,
|
|
||||||
frame.format,
|
|
||||||
frame.modifier,
|
|
||||||
frame.stride,
|
|
||||||
frame.offset,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -940,20 +933,11 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
// SAFETY: delegates to avhw::import_dma_buf_to_vaapi (itself an unsafe fn).
|
// SAFETY: delegates to avhw::import_dma_buf_to_vaapi (itself an unsafe fn).
|
||||||
// frames_ctx is a valid AVBufferRef from AvHwFrameCtx::for_capture above;
|
// frames_ctx is a valid AVBufferRef from AvHwFrameCtx::for_capture above;
|
||||||
// first_frame's fd/width/height/format/modifier/stride/offset are all sourced
|
// `first_frame` is the PipeWire-formatted PwDmaBufFrame whose metadata the
|
||||||
// from the PipeWire-formatted PwDmaBufFrame. See that function's own SAFETY
|
// function reads directly. See that function's own SAFETY contract for the
|
||||||
// contract for the full rationale.
|
// full rationale.
|
||||||
let vaapi_frame = unsafe {
|
let vaapi_frame = unsafe {
|
||||||
import_dma_buf_to_vaapi(
|
import_dma_buf_to_vaapi(frames_ctx.as_ptr(), &first_frame)
|
||||||
frames_ctx.as_ptr(),
|
|
||||||
first_frame.fd.as_raw_fd(),
|
|
||||||
first_frame.width,
|
|
||||||
first_frame.height,
|
|
||||||
first_frame.format,
|
|
||||||
first_frame.modifier,
|
|
||||||
first_frame.stride,
|
|
||||||
first_frame.offset,
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match &vaapi_frame {
|
match &vaapi_frame {
|
||||||
|
|||||||
+28
-5
@@ -95,6 +95,23 @@ pub struct PwDmaBufFrame {
|
|||||||
pub pts: i64,
|
pub pts: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// PipeWire-negotiated video format snapshot, stashed in a `Cell` for cross-callback
|
||||||
|
/// sharing (format-change callback writes it; process callback reads it). The four
|
||||||
|
/// fields are the minimal subset of `PwDmaBufFrame`'s metadata that the process
|
||||||
|
/// callback needs to construct the frame once a buffer arrives.
|
||||||
|
///
|
||||||
|
/// `Copy` is required because we store it inside `Cell<Option<PortalFormatInfo>>`;
|
||||||
|
/// `Cell` requires its contents to be `Copy` (no borrowed interior state).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct PortalFormatInfo {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
/// DRM FourCC format code (e.g. `0x34325258` for XR24 / XRGB8888).
|
||||||
|
pub drm_format: u32,
|
||||||
|
/// DRM format modifier describing buffer layout (linear, tiling, etc.).
|
||||||
|
pub modifier: u64,
|
||||||
|
}
|
||||||
|
|
||||||
/// PipeWire 控制事件枚举
|
/// PipeWire 控制事件枚举
|
||||||
///
|
///
|
||||||
/// 从 PipeWire 捕获线程发送给消费者的控制事件。
|
/// 从 PipeWire 捕获线程发送给消费者的控制事件。
|
||||||
@@ -744,7 +761,7 @@ fn pipewire_thread(ctx: PwThreadCtx) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let format_info: Rc<Cell<Option<(u32, u32, u32, u64)>>> = Rc::new(Cell::new(None));
|
let format_info: Rc<Cell<Option<PortalFormatInfo>>> = Rc::new(Cell::new(None));
|
||||||
|
|
||||||
let event_tx_state = event_tx.clone();
|
let event_tx_state = event_tx.clone();
|
||||||
let _listener = stream
|
let _listener = stream
|
||||||
@@ -796,9 +813,14 @@ fn pipewire_thread(ctx: PwThreadCtx) {
|
|||||||
let max_framerate = info.max_framerate();
|
let max_framerate = info.max_framerate();
|
||||||
// 保存协商后的格式信息,供 process 回调读取
|
// 保存协商后的格式信息,供 process 回调读取
|
||||||
let previous_format = format_info.get();
|
let previous_format = format_info.get();
|
||||||
format_info.set(Some((width, height, drm_format, modifier)));
|
format_info.set(Some(PortalFormatInfo {
|
||||||
if let Some((previous_width, previous_height, _, _)) = previous_format {
|
width,
|
||||||
if width != previous_width || height != previous_height {
|
height,
|
||||||
|
drm_format,
|
||||||
|
modifier,
|
||||||
|
}));
|
||||||
|
if let Some(prev) = previous_format {
|
||||||
|
if width != prev.width || height != prev.height {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"PipeWire dimensions changed: {}x{} (format renegotiation)",
|
"PipeWire dimensions changed: {}x{} (format renegotiation)",
|
||||||
width,
|
width,
|
||||||
@@ -927,11 +949,12 @@ fn pipewire_thread(ctx: PwThreadCtx) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 验证格式信息已协商完成,且分辨率和格式有效
|
// 验证格式信息已协商完成,且分辨率和格式有效
|
||||||
let Some((width, height, format, modifier)) = format_info.get() else {
|
let Some(fmt) = format_info.get() else {
|
||||||
// SAFETY: raw_buf still owned, returning it.
|
// SAFETY: raw_buf still owned, returning it.
|
||||||
unsafe { stream.queue_raw_buffer(raw_buf) };
|
unsafe { stream.queue_raw_buffer(raw_buf) };
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let PortalFormatInfo { width, height, drm_format: format, modifier } = fmt;
|
||||||
if width == 0 || height == 0 || format == 0 {
|
if width == 0 || height == 0 || format == 0 {
|
||||||
tracing::trace!("process: invalid dimensions {width}x{height} format={format}");
|
tracing::trace!("process: invalid dimensions {width}x{height} format={format}");
|
||||||
// SAFETY: raw_buf still owned, returning it.
|
// SAFETY: raw_buf still owned, returning it.
|
||||||
|
|||||||
+11
-3
@@ -98,7 +98,9 @@ pub struct PartialOutputInfo {
|
|||||||
|
|
||||||
|
|
||||||
/// Stores head info from wlr-output-management for name-based matching with wl_output.
|
/// Stores head info from wlr-output-management for name-based matching with wl_output.
|
||||||
struct WlrHeadInfo {
|
// `pub(crate)` (not module-private): exposed via `EncConstructionStage::ProbingOutputs.wlr_heads`
|
||||||
|
// which is reached from main.rs during the wlr-screencopy probing loop.
|
||||||
|
pub(crate) struct WlrHeadInfo {
|
||||||
position: Option<(i32, i32)>,
|
position: Option<(i32, i32)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,8 +146,14 @@ impl StreamingEncoder {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// EncConstructionStage
|
// EncConstructionStage
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// `pub(crate)` (not `pub`): this enum leaks the private `WlrHeadInfo` type via
|
||||||
|
// its `wlr_heads` field, and the construction-stage state machine is an
|
||||||
|
// internal implementation detail. Crate-internal consumers (main.rs) get there
|
||||||
|
// via `crate::state::`; there is no need to expose this across the crate
|
||||||
|
// boundary. See Oracle audit 2026-06-28.
|
||||||
|
|
||||||
pub enum EncConstructionStage<S: CaptureSource> {
|
pub(crate) enum EncConstructionStage<S: CaptureSource> {
|
||||||
ProbingOutputs {
|
ProbingOutputs {
|
||||||
outputs: Vec<PartialOutputInfo>,
|
outputs: Vec<PartialOutputInfo>,
|
||||||
bound_outputs: Vec<WlOutput>,
|
bound_outputs: Vec<WlOutput>,
|
||||||
@@ -199,7 +207,7 @@ pub enum InFlightSurface<S: CaptureSource> {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
pub struct State<S: CaptureSource> {
|
pub struct State<S: CaptureSource> {
|
||||||
pub stage: EncConstructionStage<S>,
|
pub(crate) stage: EncConstructionStage<S>,
|
||||||
pub in_flight_surface: InFlightSurface<S>,
|
pub in_flight_surface: InFlightSurface<S>,
|
||||||
pub starting_timestamp: Option<i64>,
|
pub starting_timestamp: Option<i64>,
|
||||||
pub stats_start_time: Option<Instant>,
|
pub stats_start_time: Option<Instant>,
|
||||||
|
|||||||
+6
-24
@@ -479,20 +479,11 @@ impl StatePortal {
|
|||||||
if let Some(enc) = self.enc.as_mut() {
|
if let Some(enc) = self.enc.as_mut() {
|
||||||
// 将 DMA-BUF 帧零拷贝导入 VAAPI 硬件帧池
|
// 将 DMA-BUF 帧零拷贝导入 VAAPI 硬件帧池
|
||||||
// SAFETY: delegates to avhw::import_dma_buf_to_vaapi (itself an unsafe fn);
|
// SAFETY: delegates to avhw::import_dma_buf_to_vaapi (itself an unsafe fn);
|
||||||
// frames_rgb pointer is a valid AVBufferRef owned by enc, and frame's
|
// frames_rgb pointer is a valid AVBufferRef owned by enc, and `frame` is the
|
||||||
// fd/width/height/format/modifier/stride/offset come straight from the
|
// PipeWire-formatted PwDmaBufFrame whose metadata the function reads directly.
|
||||||
// PipeWire-formatted PwDmaBufFrame. See that function's own SAFETY contract.
|
// See that function's own SAFETY contract.
|
||||||
let mut vaapi_frame = unsafe {
|
let mut vaapi_frame = unsafe {
|
||||||
avhw::import_dma_buf_to_vaapi(
|
avhw::import_dma_buf_to_vaapi(enc.frames_rgb().as_ptr(), &frame)
|
||||||
enc.frames_rgb().as_ptr(),
|
|
||||||
frame.fd.as_raw_fd(),
|
|
||||||
frame.width,
|
|
||||||
frame.height,
|
|
||||||
frame.format,
|
|
||||||
frame.modifier,
|
|
||||||
frame.stride,
|
|
||||||
frame.offset,
|
|
||||||
)
|
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
let import_us = t_import_start.elapsed().as_micros() as u64;
|
let import_us = t_import_start.elapsed().as_micros() as u64;
|
||||||
@@ -522,18 +513,9 @@ impl StatePortal {
|
|||||||
self.stats.record_encode(&timings);
|
self.stats.record_encode(&timings);
|
||||||
} else if let Some(import) = self.enc_import.as_mut() {
|
} else if let Some(import) = self.enc_import.as_mut() {
|
||||||
// SAFETY: same contract as the enc branch above — frames_rgb owned by
|
// SAFETY: same contract as the enc branch above — frames_rgb owned by
|
||||||
// import, frame fields come from the PipeWire PwDmaBufFrame.
|
// import, `frame` carries the PipeWire DMA-BUF metadata.
|
||||||
let mut vaapi_frame = unsafe {
|
let mut vaapi_frame = unsafe {
|
||||||
avhw::import_dma_buf_to_vaapi(
|
avhw::import_dma_buf_to_vaapi(import.frames_rgb().as_ptr(), &frame)
|
||||||
import.frames_rgb().as_ptr(),
|
|
||||||
frame.fd.as_raw_fd(),
|
|
||||||
frame.width,
|
|
||||||
frame.height,
|
|
||||||
frame.format,
|
|
||||||
frame.modifier,
|
|
||||||
frame.stride,
|
|
||||||
frame.offset,
|
|
||||||
)
|
|
||||||
}?;
|
}?;
|
||||||
// SAFETY: vaapi_frame is the valid AVFrame returned above; pts is plain i64.
|
// SAFETY: vaapi_frame is the valid AVFrame returned above; pts is plain i64.
|
||||||
unsafe {
|
unsafe {
|
||||||
|
|||||||
+8
-310
@@ -1,8 +1,12 @@
|
|||||||
/// Coordinate transformation module for Wayland output transforms.
|
/// Coordinate transformation module for Wayland output transforms.
|
||||||
///
|
//
|
||||||
/// Handles the 8 `wl_output` transform variants (rotation + reflection)
|
// Historically exposed a family of `Rect`/`screen_to_frame`/`fit_inside_bounds`
|
||||||
/// and ROI clipping for screen capture.
|
// helpers for ROI-based capture clipping. Those were never wired into the
|
||||||
///
|
// capture pipeline (we capture full frames and let FFmpeg's filter graph handle
|
||||||
|
// any scaling/rotation); they have been removed. Only `Transform` and the
|
||||||
|
// `transpose_if_transform_transposed` helper remain — both are actively used by
|
||||||
|
// `state.rs` and `avhw.rs`.
|
||||||
|
|
||||||
/// Wayland output transform enum, matching `wl_output::Transform`.
|
/// Wayland output transform enum, matching `wl_output::Transform`.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum Transform {
|
pub enum Transform {
|
||||||
@@ -16,68 +20,6 @@ pub enum Transform {
|
|||||||
Flipped270,
|
Flipped270,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Axis-aligned rectangle in integer coordinates.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct Rect {
|
|
||||||
pub x: i32,
|
|
||||||
pub y: i32,
|
|
||||||
pub w: i32,
|
|
||||||
pub h: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the 2×2 basis matrix (a, b, c, d) for the given transform.
|
|
||||||
///
|
|
||||||
/// The matrix represents the affine mapping from screen coordinates to
|
|
||||||
/// frame coordinates:
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// [new_x] [a b] [x]
|
|
||||||
/// [new_y] = [c d] [y]
|
|
||||||
/// ```
|
|
||||||
pub fn transform_basis(transform: Transform) -> (i32, i32, i32, i32) {
|
|
||||||
match transform {
|
|
||||||
Transform::Normal => (1, 0, 0, 1),
|
|
||||||
Transform::Normal90 => (0, 1, -1, 0),
|
|
||||||
Transform::Normal180 => (-1, 0, 0, -1),
|
|
||||||
Transform::Normal270 => (0, -1, 1, 0),
|
|
||||||
Transform::Flipped => (-1, 0, 0, 1),
|
|
||||||
Transform::Flipped90 => (0, 1, 1, 0),
|
|
||||||
Transform::Flipped180 => (1, 0, 0, -1),
|
|
||||||
Transform::Flipped270 => (0, -1, -1, 0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transform a rectangle from screen space to frame space.
|
|
||||||
///
|
|
||||||
/// Applies the 2×2 basis matrix and computes offsets so the result
|
|
||||||
/// fits within the frame dimensions `(frame_w, frame_h)`.
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// new_x = a * x + b * y + offset_x
|
|
||||||
/// new_y = c * x + d * y + offset_y
|
|
||||||
/// ```
|
|
||||||
pub fn screen_to_frame(transform: Transform, rect: Rect, frame_w: i32, frame_h: i32) -> Rect {
|
|
||||||
let (a, b, c, d) = transform_basis(transform);
|
|
||||||
|
|
||||||
// Compute the offset so that the transformed origin maps correctly.
|
|
||||||
// For transforms with negative components, we need to shift by the
|
|
||||||
// frame dimension to keep coordinates in [0, frame_w) × [0, frame_h).
|
|
||||||
let offset_x = if a + b < 0 { frame_w } else { 0 };
|
|
||||||
let offset_y = if c + d < 0 { frame_h } else { 0 };
|
|
||||||
|
|
||||||
let new_x = a * rect.x + b * rect.y + offset_x;
|
|
||||||
let new_y = c * rect.x + d * rect.y + offset_y;
|
|
||||||
let new_w = a * rect.w + b * rect.h;
|
|
||||||
let new_h = c * rect.w + d * rect.h;
|
|
||||||
|
|
||||||
Rect {
|
|
||||||
x: new_x,
|
|
||||||
y: new_y,
|
|
||||||
w: new_w.abs(),
|
|
||||||
h: new_h.abs(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Swap width and height for 90° or 270° rotations.
|
/// Swap width and height for 90° or 270° rotations.
|
||||||
///
|
///
|
||||||
/// After a quarter-turn rotation the output dimensions are transposed
|
/// After a quarter-turn rotation the output dimensions are transposed
|
||||||
@@ -93,140 +35,10 @@ pub fn transpose_if_transform_transposed(transform: Transform, w: i32, h: i32) -
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clip a rectangle so it stays inside `(0, 0) .. (bounds_w, bounds_h)`.
|
|
||||||
///
|
|
||||||
/// The resulting rectangle has non-negative origin and its extent does
|
|
||||||
/// not exceed the bounds.
|
|
||||||
pub fn fit_inside_bounds(rect: Rect, bounds_w: i32, bounds_h: i32) -> Rect {
|
|
||||||
let x = rect.x.clamp(0, bounds_w);
|
|
||||||
let y = rect.y.clamp(0, bounds_h);
|
|
||||||
let right = (rect.x + rect.w).min(bounds_w);
|
|
||||||
let bottom = (rect.y + rect.h).min(bounds_h);
|
|
||||||
let w = (right - x).max(0);
|
|
||||||
let h = (bottom - y).max(0);
|
|
||||||
Rect { x, y, w, h }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
// ── transform_basis ───────────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basis_normal_is_identity() {
|
|
||||||
assert_eq!(transform_basis(Transform::Normal), (1, 0, 0, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basis_90_cw_rotation() {
|
|
||||||
assert_eq!(transform_basis(Transform::Normal90), (0, 1, -1, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basis_180_rotation() {
|
|
||||||
assert_eq!(transform_basis(Transform::Normal180), (-1, 0, 0, -1));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basis_270_cw_rotation() {
|
|
||||||
assert_eq!(transform_basis(Transform::Normal270), (0, -1, 1, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basis_flipped_horizontal() {
|
|
||||||
assert_eq!(transform_basis(Transform::Flipped), (-1, 0, 0, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basis_flipped_90() {
|
|
||||||
assert_eq!(transform_basis(Transform::Flipped90), (0, 1, 1, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basis_flipped_180() {
|
|
||||||
assert_eq!(transform_basis(Transform::Flipped180), (1, 0, 0, -1));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basis_flipped_270() {
|
|
||||||
assert_eq!(transform_basis(Transform::Flipped270), (0, -1, -1, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── screen_to_frame ───────────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn screen_to_frame_identity_unchanged() {
|
|
||||||
let rect = Rect {
|
|
||||||
x: 10,
|
|
||||||
y: 20,
|
|
||||||
w: 100,
|
|
||||||
h: 50,
|
|
||||||
};
|
|
||||||
let result = screen_to_frame(Transform::Normal, rect, 1920, 1080);
|
|
||||||
assert_eq!(
|
|
||||||
result,
|
|
||||||
Rect {
|
|
||||||
x: 10,
|
|
||||||
y: 20,
|
|
||||||
w: 100,
|
|
||||||
h: 50
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn screen_to_frame_90_rotates_origin() {
|
|
||||||
// 90° CW: top-left (0,0) in screen should map to bottom-left in frame
|
|
||||||
let rect = Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
w: 100,
|
|
||||||
h: 50,
|
|
||||||
};
|
|
||||||
let result = screen_to_frame(Transform::Normal90, rect, 1080, 1920);
|
|
||||||
// a=0,b=1,c=-1,d=0 => offset_x=0, offset_y=1920 (c+d=-1<0)
|
|
||||||
// new_x = 0*0 + 1*0 + 0 = 0
|
|
||||||
// new_y = -1*0 + 0*0 + 1920 = 1920
|
|
||||||
assert_eq!(result.x, 0);
|
|
||||||
assert_eq!(result.y, 1920);
|
|
||||||
// w' = 0*100 + 1*50 = 50, h' = -1*100 + 0*50 = -100 -> abs=100
|
|
||||||
assert_eq!(result.w, 50);
|
|
||||||
assert_eq!(result.h, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn screen_to_frame_180_rotates() {
|
|
||||||
let rect = Rect {
|
|
||||||
x: 100,
|
|
||||||
y: 200,
|
|
||||||
w: 300,
|
|
||||||
h: 400,
|
|
||||||
};
|
|
||||||
let result = screen_to_frame(Transform::Normal180, rect, 1920, 1080);
|
|
||||||
// a=-1,b=0,c=0,d=-1, offset_x=1920, offset_y=1080
|
|
||||||
assert_eq!(result.x, -100 + 1920);
|
|
||||||
assert_eq!(result.y, -200 + 1080);
|
|
||||||
assert_eq!(result.w, 300);
|
|
||||||
assert_eq!(result.h, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn screen_to_frame_flipped_horizontal() {
|
|
||||||
let rect = Rect {
|
|
||||||
x: 50,
|
|
||||||
y: 30,
|
|
||||||
w: 200,
|
|
||||||
h: 100,
|
|
||||||
};
|
|
||||||
let result = screen_to_frame(Transform::Flipped, rect, 1920, 1080);
|
|
||||||
// a=-1,b=0,c=0,d=1, offset_x=1920, offset_y=0
|
|
||||||
assert_eq!(result.x, -50 + 1920);
|
|
||||||
assert_eq!(result.y, 30);
|
|
||||||
assert_eq!(result.w, 200);
|
|
||||||
assert_eq!(result.h, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── transpose_if_transform_transposed ─────────────────────────
|
// ── transpose_if_transform_transposed ─────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -292,118 +104,4 @@ mod tests {
|
|||||||
(1080, 1920)
|
(1080, 1920)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── fit_inside_bounds ─────────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fit_inside_already_fits() {
|
|
||||||
let rect = Rect {
|
|
||||||
x: 10,
|
|
||||||
y: 20,
|
|
||||||
w: 100,
|
|
||||||
h: 50,
|
|
||||||
};
|
|
||||||
let result = fit_inside_bounds(rect, 1920, 1080);
|
|
||||||
assert_eq!(result, rect);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fit_inside_clips_right_and_bottom() {
|
|
||||||
let rect = Rect {
|
|
||||||
x: 1800,
|
|
||||||
y: 1000,
|
|
||||||
w: 200,
|
|
||||||
h: 200,
|
|
||||||
};
|
|
||||||
let result = fit_inside_bounds(rect, 1920, 1080);
|
|
||||||
assert_eq!(
|
|
||||||
result,
|
|
||||||
Rect {
|
|
||||||
x: 1800,
|
|
||||||
y: 1000,
|
|
||||||
w: 120,
|
|
||||||
h: 80
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fit_inside_clips_negative_origin() {
|
|
||||||
let rect = Rect {
|
|
||||||
x: -50,
|
|
||||||
y: -30,
|
|
||||||
w: 200,
|
|
||||||
h: 200,
|
|
||||||
};
|
|
||||||
let result = fit_inside_bounds(rect, 1920, 1080);
|
|
||||||
assert_eq!(
|
|
||||||
result,
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
w: 150,
|
|
||||||
h: 170
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fit_inside_completely_out_of_bounds() {
|
|
||||||
let rect = Rect {
|
|
||||||
x: 2000,
|
|
||||||
y: 2000,
|
|
||||||
w: 100,
|
|
||||||
h: 100,
|
|
||||||
};
|
|
||||||
let result = fit_inside_bounds(rect, 1920, 1080);
|
|
||||||
assert_eq!(
|
|
||||||
result,
|
|
||||||
Rect {
|
|
||||||
x: 1920,
|
|
||||||
y: 1080,
|
|
||||||
w: 0,
|
|
||||||
h: 0
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fit_inside_zero_size_rect() {
|
|
||||||
let rect = Rect {
|
|
||||||
x: 100,
|
|
||||||
y: 100,
|
|
||||||
w: 0,
|
|
||||||
h: 0,
|
|
||||||
};
|
|
||||||
let result = fit_inside_bounds(rect, 1920, 1080);
|
|
||||||
assert_eq!(
|
|
||||||
result,
|
|
||||||
Rect {
|
|
||||||
x: 100,
|
|
||||||
y: 100,
|
|
||||||
w: 0,
|
|
||||||
h: 0
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fit_inside_zero_bounds() {
|
|
||||||
let rect = Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
w: 100,
|
|
||||||
h: 100,
|
|
||||||
};
|
|
||||||
let result = fit_inside_bounds(rect, 0, 0);
|
|
||||||
assert_eq!(
|
|
||||||
result,
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
w: 0,
|
|
||||||
h: 0
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user