From 145b5d3e7e9916df9941fe911f8fbed773514d81 Mon Sep 17 00:00:00 2001 From: dailz Date: Sun, 28 Jun 2026 13:58:52 +0800 Subject: [PATCH] chore: design cleanup, dead-code purge, README/doc refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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>> 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. --- .gitignore | 3 + README.md | 31 +++- src/avhw.rs | 56 +++--- src/bin/vaapi_import_bench.rs | 30 +--- src/cap_portal.rs | 33 +++- src/state.rs | 14 +- src/state_portal.rs | 30 +--- src/transform.rs | 318 +--------------------------------- 8 files changed, 118 insertions(+), 397 deletions(-) diff --git a/.gitignore b/.gitignore index e95279e..275e611 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ Thumbs.db .playwright-mcp/ wl-webrtc.log webrtc-p0-success.png + +# Stray review-tool output (regenerated per review run) +review.json diff --git a/README.md b/README.md index 030f3b1..ad7ff81 100644 --- a/README.md +++ b/README.md @@ -38,19 +38,46 @@ wl-webrtc --output output.mp4 --drm-device /dev/dri/renderD128 # Verbose mode 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 +> `src/args.rs` is the authoritative source. Run `wl-webrtc --help` for the live list. + | 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 | | `--fps` | 30 | Target frames per second | | `--codec` | h264 | Video codec (h264 only for MVP) | | `--hw-accel` | vaapi | Hardware acceleration method | | `--drm-device` | auto | DRM render device path | | `--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 | | `-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). diff --git a/src/avhw.rs b/src/avhw.rs index c6fdd1c..abe118a 100644 --- a/src/avhw.rs +++ b/src/avhw.rs @@ -1,6 +1,9 @@ use std::ffi::CString; 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::path::Path; 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. unsafe { - import_dma_buf_to_vaapi( - frames.as_ptr(), - frame.fd.as_raw_fd(), - frame.width, - frame.height, - frame.format, - frame.modifier, - frame.stride, - frame.offset, - ) + import_dma_buf_to_vaapi(frames.as_ptr(), frame) }?; 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`. /// /// # 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 -/// - `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( frames_ctx: *mut ffi::AVBufferRef, - raw_fd: RawFd, - width: u32, - height: u32, - drm_format: u32, - modifier: u64, - stride: u32, - offset: u64, + frame: &PwDmaBufFrame, ) -> Result { - let duped_fd = libc::dup(raw_fd); + let duped_fd = libc::dup(frame.fd.as_raw_fd()); if duped_fd < 0 { 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(); desc.nb_objects = 1; desc.objects[0].fd = duped_fd; - desc.objects[0].size = (height as usize) * (stride as usize); - desc.objects[0].format_modifier = modifier; + desc.objects[0].size = (frame.height as usize) * (frame.stride as usize); + desc.objects[0].format_modifier = frame.modifier; 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].planes[0].object_index = 0; - desc.layers[0].planes[0].offset = offset as isize; - desc.layers[0].planes[0].pitch = stride as isize; + desc.layers[0].planes[0].offset = frame.offset as isize; + desc.layers[0].planes[0].pitch = frame.stride as isize; let desc_box = Box::new(desc); 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(); (*sp).format = ffi::AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32; - (*sp).width = width as i32; - (*sp).height = height as i32; + (*sp).width = frame.width as i32; + (*sp).height = frame.height as i32; (*sp).data[0] = (*buf_ref).data; (*sp).buf[0] = buf_ref; } @@ -509,11 +506,10 @@ impl EncState { // avformat_query_codec checks codec+format compatibility; both pointers are // valid and FF_COMPLIANCE_NORMAL is a constant. All three reads happen in one // 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 oformat = (*fmt_ctx_ptr).oformat; - let compat = ffi::avformat_query_codec(oformat, codec_id, ffi::FF_COMPLIANCE_NORMAL); - (codec_id, oformat, compat) + ffi::avformat_query_codec(oformat, codec_id, ffi::FF_COMPLIANCE_NORMAL) }; if compat < 0 { bail!("H.264 codec not supported by output container format"); diff --git a/src/bin/vaapi_import_bench.rs b/src/bin/vaapi_import_bench.rs index 52069d5..09371bd 100644 --- a/src/bin/vaapi_import_bench.rs +++ b/src/bin/vaapi_import_bench.rs @@ -417,17 +417,10 @@ fn import_frame( ) -> Result { // 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. + // SAFETY: frames_ctx is a valid VAAPI frames context; `frame` carries the + // DMA-BUF metadata read by the function. unsafe { - import_dma_buf_to_vaapi( - frames_ctx.as_ptr(), - frame.fd.as_raw_fd(), - frame.width, - frame.height, - frame.format, - frame.modifier, - frame.stride, - frame.offset, - ) + import_dma_buf_to_vaapi(frames_ctx.as_ptr(), frame) } } @@ -940,20 +933,11 @@ fn main() -> Result<()> { // SAFETY: delegates to avhw::import_dma_buf_to_vaapi (itself an unsafe fn). // frames_ctx is a valid AVBufferRef from AvHwFrameCtx::for_capture above; - // first_frame's fd/width/height/format/modifier/stride/offset are all sourced - // from the PipeWire-formatted PwDmaBufFrame. See that function's own SAFETY - // contract for the full rationale. + // `first_frame` is the PipeWire-formatted PwDmaBufFrame whose metadata the + // function reads directly. See that function's own SAFETY contract for the + // full rationale. let vaapi_frame = unsafe { - import_dma_buf_to_vaapi( - 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, - ) + import_dma_buf_to_vaapi(frames_ctx.as_ptr(), &first_frame) }; match &vaapi_frame { diff --git a/src/cap_portal.rs b/src/cap_portal.rs index a6221be..03a22b1 100644 --- a/src/cap_portal.rs +++ b/src/cap_portal.rs @@ -95,6 +95,23 @@ pub struct PwDmaBufFrame { 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>`; +/// `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 捕获线程发送给消费者的控制事件。 @@ -744,7 +761,7 @@ fn pipewire_thread(ctx: PwThreadCtx) { } }; - let format_info: Rc>> = Rc::new(Cell::new(None)); + let format_info: Rc>> = Rc::new(Cell::new(None)); let event_tx_state = event_tx.clone(); let _listener = stream @@ -796,9 +813,14 @@ fn pipewire_thread(ctx: PwThreadCtx) { let max_framerate = info.max_framerate(); // 保存协商后的格式信息,供 process 回调读取 let previous_format = format_info.get(); - format_info.set(Some((width, height, drm_format, modifier))); - if let Some((previous_width, previous_height, _, _)) = previous_format { - if width != previous_width || height != previous_height { + format_info.set(Some(PortalFormatInfo { + width, + height, + drm_format, + modifier, + })); + if let Some(prev) = previous_format { + if width != prev.width || height != prev.height { tracing::warn!( "PipeWire dimensions changed: {}x{} (format renegotiation)", 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. unsafe { stream.queue_raw_buffer(raw_buf) }; return; }; + let PortalFormatInfo { width, height, drm_format: format, modifier } = fmt; if width == 0 || height == 0 || format == 0 { tracing::trace!("process: invalid dimensions {width}x{height} format={format}"); // SAFETY: raw_buf still owned, returning it. diff --git a/src/state.rs b/src/state.rs index 64c448a..3b5ea16 100644 --- a/src/state.rs +++ b/src/state.rs @@ -98,7 +98,9 @@ pub struct PartialOutputInfo { /// 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)>, } @@ -144,8 +146,14 @@ impl StreamingEncoder { // --------------------------------------------------------------------------- // 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 { +pub(crate) enum EncConstructionStage { ProbingOutputs { outputs: Vec, bound_outputs: Vec, @@ -199,7 +207,7 @@ pub enum InFlightSurface { // --------------------------------------------------------------------------- pub struct State { - pub stage: EncConstructionStage, + pub(crate) stage: EncConstructionStage, pub in_flight_surface: InFlightSurface, pub starting_timestamp: Option, pub stats_start_time: Option, diff --git a/src/state_portal.rs b/src/state_portal.rs index 3330816..48ec3f6 100644 --- a/src/state_portal.rs +++ b/src/state_portal.rs @@ -479,20 +479,11 @@ impl StatePortal { if let Some(enc) = self.enc.as_mut() { // 将 DMA-BUF 帧零拷贝导入 VAAPI 硬件帧池 // 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 - // fd/width/height/format/modifier/stride/offset come straight from the - // PipeWire-formatted PwDmaBufFrame. See that function's own SAFETY contract. + // frames_rgb pointer is a valid AVBufferRef owned by enc, and `frame` is the + // PipeWire-formatted PwDmaBufFrame whose metadata the function reads directly. + // See that function's own SAFETY contract. let mut vaapi_frame = unsafe { - avhw::import_dma_buf_to_vaapi( - enc.frames_rgb().as_ptr(), - frame.fd.as_raw_fd(), - frame.width, - frame.height, - frame.format, - frame.modifier, - frame.stride, - frame.offset, - ) + avhw::import_dma_buf_to_vaapi(enc.frames_rgb().as_ptr(), &frame) }?; let import_us = t_import_start.elapsed().as_micros() as u64; @@ -522,18 +513,9 @@ impl StatePortal { self.stats.record_encode(&timings); } else if let Some(import) = self.enc_import.as_mut() { // 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 { - avhw::import_dma_buf_to_vaapi( - import.frames_rgb().as_ptr(), - frame.fd.as_raw_fd(), - frame.width, - frame.height, - frame.format, - frame.modifier, - frame.stride, - frame.offset, - ) + avhw::import_dma_buf_to_vaapi(import.frames_rgb().as_ptr(), &frame) }?; // SAFETY: vaapi_frame is the valid AVFrame returned above; pts is plain i64. unsafe { diff --git a/src/transform.rs b/src/transform.rs index b6bac3b..c800715 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -1,8 +1,12 @@ /// Coordinate transformation module for Wayland output transforms. -/// -/// Handles the 8 `wl_output` transform variants (rotation + reflection) -/// and ROI clipping for screen capture. -/// +// +// Historically exposed a family of `Rect`/`screen_to_frame`/`fit_inside_bounds` +// 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`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Transform { @@ -16,68 +20,6 @@ pub enum Transform { 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. /// /// 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)] mod tests { 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 ───────────────────────── #[test] @@ -292,118 +104,4 @@ mod tests { (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 - } - ); - } }