Honor output orientation in recording and screen sharing

This commit is contained in:
Samuel Onoja committed 2026-09-28 10:19:19 +01:00
1 parent 3ad0108d80
commit e1e25e46e6
1 file changed
+132 -16
+132 -16
View File
@@ -33,6 +33,7 @@ struct Output {
global: u32, global: u32,
proxy: wl_output::WlOutput, proxy: wl_output::WlOutput,
source: Source, source: Source,
transform: wl_output::Transform,
} }
pub struct Frame { pub struct Frame {
@@ -110,7 +111,13 @@ impl Capture {
self.state self.state
.outputs .outputs
.iter() .iter()
.map(|o| o.source.clone()) .map(|o| {
let mut source = o.source.clone();
if swaps_axes(o.transform) {
std::mem::swap(&mut source.width, &mut source.height);
}
source
})
.collect() .collect()
} }
@@ -128,6 +135,7 @@ impl Capture {
.find(|o| o.source.name == name) .find(|o| o.source.name == name)
.ok_or("Shared monitor disconnected")?; .ok_or("Shared monitor disconnected")?;
let transform = output.transform;
self.state.result = None; self.state.result = None;
self.state.flipped = false; self.state.flipped = false;
@@ -154,22 +162,20 @@ impl Capture {
.buffer .buffer
.as_ref() .as_ref()
.ok_or("Capture supplied no buffer")?; .ok_or("Capture supplied no buffer")?;
pixels.resize(buffer.map.len(), 0); let (width, height) = copy_oriented_pixels(
pixels.copy_from_slice(&buffer.map); &buffer.map,
buffer.width,
if self.state.flipped { buffer.height,
let stride = buffer.stride as usize; buffer.stride,
for y in 0..buffer.height as usize / 2 { transform,
let bottom = (buffer.height as usize - y - 1) * stride; self.state.flipped,
let (top, tail) = pixels.split_at_mut(bottom); &mut pixels,
top[y * stride..(y + 1) * stride].swap_with_slice(&mut tail[..stride]); );
}
}
Ok(Frame { Ok(Frame {
width: buffer.width, width,
height: buffer.height, height,
stride: buffer.stride, stride: width * 4,
pixels, pixels,
}) })
} }
@@ -231,6 +237,7 @@ impl Dispatch<wl_registry::WlRegistry, ()> for State {
} => match interface.as_str() { } => match interface.as_str() {
"wl_output" => state.outputs.push(Output { "wl_output" => state.outputs.push(Output {
global: name, global: name,
transform: wl_output::Transform::Normal,
proxy: registry.bind(name, version.min(4), qh, name), proxy: registry.bind(name, version.min(4), qh, name),
source: Source { source: Source {
name: format!("output-{name}"), name: format!("output-{name}"),
@@ -270,7 +277,12 @@ impl Dispatch<wl_output::WlOutput, u32> for State {
match event { match event {
wl_output::Event::Name { name } => output.source.name = name, wl_output::Event::Name { name } => output.source.name = name,
wl_output::Event::Description { description } => output.source.label = description, wl_output::Event::Description { description } => output.source.label = description,
wl_output::Event::Geometry { x, y, .. } => { wl_output::Event::Geometry {
x, y, transform, ..
} => {
if let WEnum::Value(transform) = transform {
output.transform = transform;
}
output.source.x = x; output.source.x = x;
output.source.y = y; output.source.y = y;
} }
@@ -380,6 +392,70 @@ impl Dispatch<frame::ZwlrScreencopyFrameV1, ()> for State {
} }
} }
fn swaps_axes(transform: wl_output::Transform) -> bool {
matches!(
transform,
wl_output::Transform::_90
| wl_output::Transform::_270
| wl_output::Transform::Flipped90
| wl_output::Transform::Flipped270
)
}
// Screencopy supplies output-buffer coordinates. Normalize both the output's
// presentation transform and the independent YInvert storage flag before
// publishing upright, tightly packed BGRx frames to PipeWire.
fn copy_oriented_pixels(
source: &[u8],
width: u32,
height: u32,
stride: u32,
transform: wl_output::Transform,
y_invert: bool,
pixels: &mut Vec<u8>,
) -> (u32, u32) {
use wl_output::Transform;
let (out_width, out_height) = if swaps_axes(transform) {
(height, width)
} else {
(width, height)
};
let row = out_width as usize * 4;
pixels.resize(row * out_height as usize, 0);
// The usual DRM and nested outputs need only one contiguous copy per row.
if matches!(transform, Transform::Normal | Transform::Flipped180) {
let flip = y_invert ^ (transform == Transform::Flipped180);
for (y, dst) in pixels.chunks_exact_mut(row).enumerate() {
let sy = if flip { height as usize - 1 - y } else { y };
let start = sy * stride as usize;
dst.copy_from_slice(&source[start..start + row]);
}
return (out_width, out_height);
}
for y in 0..out_height {
for x in 0..out_width {
let (sx, mut sy) = match transform {
Transform::_90 => (y, height - 1 - x),
Transform::_180 => (width - 1 - x, height - 1 - y),
Transform::_270 => (width - 1 - y, x),
Transform::Flipped => (width - 1 - x, y),
Transform::Flipped90 => (y, x),
Transform::Flipped270 => (width - 1 - y, height - 1 - x),
_ => (x, y),
};
if y_invert {
sy = height - 1 - sy;
}
let src = sy as usize * stride as usize + sx as usize * 4;
let dst = y as usize * row + x as usize * 4;
pixels[dst..dst + 4].copy_from_slice(&source[src..src + 4]);
}
}
(out_width, out_height)
}
fn buffer_size(width: u32, height: u32, stride: u32) -> Result<usize, String> { fn buffer_size(width: u32, height: u32, stride: u32) -> Result<usize, String> {
let row = width.checked_mul(4).ok_or("Capture dimensions overflow")?; let row = width.checked_mul(4).ok_or("Capture dimensions overflow")?;
let bytes = stride let bytes = stride
@@ -414,6 +490,46 @@ wayland_client::delegate_noop!(State: ignore manager::ZwlrScreencopyManagerV1);
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn capture_orientation_matches_presentation_for_all_transforms() {
use wl_output::Transform::*;
// Asymmetric 3x2 image with row padding and distinct four-byte pixels.
let cases = [
(Normal, (3, 2), "abcdef"),
(_90, (2, 3), "daebfc"),
(_180, (3, 2), "fedcba"),
(_270, (2, 3), "cfbead"),
(Flipped, (3, 2), "cbafed"),
(Flipped90, (2, 3), "adbecf"),
(Flipped180, (3, 2), "defabc"),
(Flipped270, (2, 3), "fcebda"),
];
let pixel = |value: u8| [value, value + 1, value + 2, 255];
let mut pixels = vec![0; 100];
for (transform, size, expected) in cases {
for y_invert in [false, true] {
let mut source = Vec::new();
let rows = if y_invert {
[b"def", b"abc"]
} else {
[b"abc", b"def"]
};
for row in rows {
for &value in row {
source.extend_from_slice(&pixel(value));
}
source.extend_from_slice(&[0; 4]);
}
assert_eq!(
copy_oriented_pixels(&source, 3, 2, 16, transform, y_invert, &mut pixels),
size,
);
let expected: Vec<_> = expected.bytes().flat_map(pixel).collect();
assert_eq!(pixels, expected, "{transform:?}, YInvert={y_invert}");
}
}
}
#[test] #[test]
fn rejects_unbounded_and_invalid_buffers() { fn rejects_unbounded_and_invalid_buffers() {
assert_eq!(buffer_size(640, 480, 2560).unwrap(), 1228800); assert_eq!(buffer_size(640, 480, 2560).unwrap(), 1228800);