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
This commit is contained in:
291
src/transform.rs
Normal file
291
src/transform.rs
Normal file
@@ -0,0 +1,291 @@
|
||||
/// Coordinate transformation module for Wayland output transforms.
|
||||
///
|
||||
/// Handles the 8 `wl_output` transform variants (rotation + reflection)
|
||||
/// and ROI clipping for screen capture.
|
||||
///
|
||||
/// Wayland output transform enum, matching `wl_output::Transform`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Transform {
|
||||
Normal,
|
||||
Normal90,
|
||||
Normal180,
|
||||
Normal270,
|
||||
Flipped,
|
||||
Flipped90,
|
||||
Flipped180,
|
||||
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
|
||||
/// relative to the input. This helper returns `(h, w)` for those cases
|
||||
/// and `(w, h)` unchanged otherwise.
|
||||
pub fn transpose_if_transform_transposed(transform: Transform, w: i32, h: i32) -> (i32, i32) {
|
||||
match transform {
|
||||
Transform::Normal90 | Transform::Normal270 | Transform::Flipped90 | Transform::Flipped270 => {
|
||||
(h, w)
|
||||
}
|
||||
_ => (w, h),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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]
|
||||
fn transpose_normal_no_swap() {
|
||||
assert_eq!(transpose_if_transform_transposed(Transform::Normal, 1920, 1080), (1920, 1080));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transpose_90_swaps() {
|
||||
assert_eq!(transpose_if_transform_transposed(Transform::Normal90, 1920, 1080), (1080, 1920));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transpose_180_no_swap() {
|
||||
assert_eq!(transpose_if_transform_transposed(Transform::Normal180, 1920, 1080), (1920, 1080));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transpose_270_swaps() {
|
||||
assert_eq!(transpose_if_transform_transposed(Transform::Normal270, 1920, 1080), (1080, 1920));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transpose_flipped_no_swap() {
|
||||
assert_eq!(transpose_if_transform_transposed(Transform::Flipped, 1920, 1080), (1920, 1080));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transpose_flipped90_swaps() {
|
||||
assert_eq!(transpose_if_transform_transposed(Transform::Flipped90, 1920, 1080), (1080, 1920));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transpose_flipped180_no_swap() {
|
||||
assert_eq!(transpose_if_transform_transposed(Transform::Flipped180, 1920, 1080), (1920, 1080));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transpose_flipped270_swaps() {
|
||||
assert_eq!(transpose_if_transform_transposed(Transform::Flipped270, 1920, 1080), (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