feat: support authorized screencopy
This commit is contained in:
1 parent
05f801aee6
commit
20b468c6ac
6 files changed
+439
-2
No files matched your search
@@ -34,6 +34,18 @@ FERESE_ENABLE_INPUT_METHOD=1 cargo run -p ferese
|
||||
|
||||
`Ctrl+Alt+Escape` always releases an active client shortcut inhibitor.
|
||||
|
||||
Screen capture is hidden by default. A session that has already made an
|
||||
explicit capture-policy decision can expose screencopy-v1 to its clients with:
|
||||
|
||||
```bash
|
||||
FERESE_ENABLE_SCREENCOPY=1 cargo run -p ferese
|
||||
```
|
||||
|
||||
The current M5 implementation supports one-shot output and region capture into
|
||||
ARGB8888 shared-memory buffers. Captures currently include Ferese's software
|
||||
cursor even when a client does not request a cursor overlay; cursor-free
|
||||
capture remains part of the M5 interoperability acceptance work.
|
||||
|
||||
### Direct DRM session
|
||||
|
||||
Run the hardware backend from a spare TTY, outside another graphical session:
|
||||
|
||||
@@ -9,7 +9,10 @@ use smithay::{
|
||||
drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, GbmBufferedSurface},
|
||||
egl::{EGLContext, EGLDisplay},
|
||||
libinput::{LibinputInputBackend, LibinputSessionInterface},
|
||||
renderer::{Bind, ImportDma, ImportMemWl, damage::OutputDamageTracker, gles::GlesRenderer},
|
||||
renderer::{
|
||||
Bind, Frame, ImportDma, ImportMemWl, Renderer, damage::OutputDamageTracker,
|
||||
gles::GlesRenderer,
|
||||
},
|
||||
session::{Event as SessionEvent, Session, libseat::LibSeatSession},
|
||||
udev::{UdevBackend, UdevEvent, primary_gpu},
|
||||
},
|
||||
@@ -315,6 +318,20 @@ fn render_device(state: &mut Ferese, node: DrmNode) {
|
||||
&elements,
|
||||
[0.035, 0.04, 0.055, 1.0],
|
||||
)?;
|
||||
if state.process_screencopies(&mut device.renderer, &framebuffer, &device.output) {
|
||||
let _ = device
|
||||
.renderer
|
||||
.render(
|
||||
&mut framebuffer,
|
||||
device
|
||||
.output
|
||||
.current_mode()
|
||||
.expect("output has a mode")
|
||||
.size,
|
||||
device.output.current_transform(),
|
||||
)?
|
||||
.finish()?;
|
||||
}
|
||||
let Some(damage) = result.damage.cloned() else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ mod activation;
|
||||
mod compositor;
|
||||
mod input_method;
|
||||
mod layer_shell;
|
||||
pub(crate) mod screencopy;
|
||||
mod xdg_shell;
|
||||
|
||||
use smithay::{
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use smithay::{
|
||||
backend::{
|
||||
allocator::Fourcc,
|
||||
renderer::{ExportMem, TextureMapping},
|
||||
},
|
||||
output::Output,
|
||||
reexports::{
|
||||
wayland_protocols_wlr::screencopy::v1::server::{
|
||||
zwlr_screencopy_frame_v1::{self, ZwlrScreencopyFrameV1},
|
||||
zwlr_screencopy_manager_v1::{self, ZwlrScreencopyManagerV1},
|
||||
},
|
||||
wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
protocol::{wl_buffer::WlBuffer, wl_shm},
|
||||
},
|
||||
},
|
||||
utils::{Buffer, Logical, Rectangle, Size},
|
||||
wayland::shm::with_buffer_contents_mut,
|
||||
};
|
||||
|
||||
use crate::Ferese;
|
||||
|
||||
const BYTES_PER_PIXEL: usize = 4;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct FrameData {
|
||||
output: Option<Output>,
|
||||
region: Rectangle<i32, Buffer>,
|
||||
used: Mutex<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PendingScreencopy {
|
||||
frame: ZwlrScreencopyFrameV1,
|
||||
buffer: WlBuffer,
|
||||
output: Output,
|
||||
region: Rectangle<i32, Buffer>,
|
||||
with_damage: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn init_global(display: &DisplayHandle) {
|
||||
let enabled = std::env::var_os("FERESE_ENABLE_SCREENCOPY").is_some_and(|value| value == "1");
|
||||
|
||||
if enabled {
|
||||
display.create_global::<Ferese, ZwlrScreencopyManagerV1, ()>(3, ());
|
||||
tracing::info!("authorized screencopy is enabled for this session");
|
||||
}
|
||||
}
|
||||
|
||||
impl GlobalDispatch<ZwlrScreencopyManagerV1, ()> for Ferese {
|
||||
fn bind(
|
||||
_state: &mut Self,
|
||||
_display: &DisplayHandle,
|
||||
_client: &Client,
|
||||
resource: New<ZwlrScreencopyManagerV1>,
|
||||
_global_data: &(),
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
data_init.init(resource, ());
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ZwlrScreencopyManagerV1, ()> for Ferese {
|
||||
fn request(
|
||||
_state: &mut Self,
|
||||
_client: &Client,
|
||||
_manager: &ZwlrScreencopyManagerV1,
|
||||
request: zwlr_screencopy_manager_v1::Request,
|
||||
_data: &(),
|
||||
_display: &DisplayHandle,
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
match request {
|
||||
zwlr_screencopy_manager_v1::Request::CaptureOutput {
|
||||
frame,
|
||||
overlay_cursor: _,
|
||||
output,
|
||||
} => {
|
||||
create_frame(data_init, frame, output, None);
|
||||
}
|
||||
zwlr_screencopy_manager_v1::Request::CaptureOutputRegion {
|
||||
frame,
|
||||
overlay_cursor: _,
|
||||
output,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
} => {
|
||||
create_frame(
|
||||
data_init,
|
||||
frame,
|
||||
output,
|
||||
Some(Rectangle::new((x, y).into(), (width, height).into())),
|
||||
);
|
||||
}
|
||||
zwlr_screencopy_manager_v1::Request::Destroy => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ZwlrScreencopyFrameV1, Arc<FrameData>> for Ferese {
|
||||
fn request(
|
||||
state: &mut Self,
|
||||
_client: &Client,
|
||||
frame: &ZwlrScreencopyFrameV1,
|
||||
request: zwlr_screencopy_frame_v1::Request,
|
||||
data: &Arc<FrameData>,
|
||||
_display: &DisplayHandle,
|
||||
_data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
match request {
|
||||
zwlr_screencopy_frame_v1::Request::Copy { buffer } => {
|
||||
state.queue_screencopy(frame, data, buffer, false);
|
||||
}
|
||||
zwlr_screencopy_frame_v1::Request::CopyWithDamage { buffer } => {
|
||||
state.queue_screencopy(frame, data, buffer, true);
|
||||
}
|
||||
zwlr_screencopy_frame_v1::Request::Destroy => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ferese {
|
||||
fn queue_screencopy(
|
||||
&mut self,
|
||||
frame: &ZwlrScreencopyFrameV1,
|
||||
data: &Arc<FrameData>,
|
||||
buffer: WlBuffer,
|
||||
with_damage: bool,
|
||||
) {
|
||||
let mut used = data.used.lock().unwrap();
|
||||
if *used {
|
||||
frame.post_error(
|
||||
zwlr_screencopy_frame_v1::Error::AlreadyUsed,
|
||||
"screencopy frame has already been used",
|
||||
);
|
||||
return;
|
||||
}
|
||||
*used = true;
|
||||
|
||||
if !valid_shm_buffer(&buffer, data.region.size) {
|
||||
frame.post_error(
|
||||
zwlr_screencopy_frame_v1::Error::InvalidBuffer,
|
||||
"screencopy requires the advertised ARGB8888 SHM buffer",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(output) = data.output.clone() else {
|
||||
frame.failed();
|
||||
return;
|
||||
};
|
||||
|
||||
self.pending_screencopies.push(PendingScreencopy {
|
||||
frame: frame.clone(),
|
||||
buffer,
|
||||
output,
|
||||
region: data.region,
|
||||
with_damage,
|
||||
});
|
||||
crate::backends::direct::render_all(self);
|
||||
}
|
||||
|
||||
pub(crate) fn process_screencopies<R>(
|
||||
&mut self,
|
||||
renderer: &mut R,
|
||||
framebuffer: &R::Framebuffer<'_>,
|
||||
output: &Output,
|
||||
) -> bool
|
||||
where
|
||||
R: ExportMem,
|
||||
{
|
||||
let mut remaining = Vec::new();
|
||||
let mut framebuffer_binding_changed = false;
|
||||
let captures = std::mem::take(&mut self.pending_screencopies);
|
||||
|
||||
for capture in captures {
|
||||
if &capture.output != output {
|
||||
remaining.push(capture);
|
||||
continue;
|
||||
}
|
||||
if !capture.frame.is_alive() || !capture.buffer.is_alive() {
|
||||
continue;
|
||||
}
|
||||
|
||||
framebuffer_binding_changed = true;
|
||||
if complete_capture(renderer, framebuffer, &capture, self.start_time.elapsed()).is_err()
|
||||
{
|
||||
capture.frame.failed();
|
||||
}
|
||||
}
|
||||
|
||||
self.pending_screencopies = remaining;
|
||||
framebuffer_binding_changed
|
||||
}
|
||||
}
|
||||
|
||||
fn create_frame(
|
||||
data_init: &mut DataInit<'_, Ferese>,
|
||||
frame: New<ZwlrScreencopyFrameV1>,
|
||||
wl_output: smithay::reexports::wayland_server::protocol::wl_output::WlOutput,
|
||||
requested: Option<Rectangle<i32, Logical>>,
|
||||
) {
|
||||
let Some(output) = Output::from_resource(&wl_output) else {
|
||||
let frame = data_init.init(
|
||||
frame,
|
||||
Arc::new(FrameData {
|
||||
output: None,
|
||||
region: Rectangle::from_size((1, 1).into()),
|
||||
used: Mutex::new(true),
|
||||
}),
|
||||
);
|
||||
frame.failed();
|
||||
return;
|
||||
};
|
||||
let Some(region) = capture_region(&output, requested) else {
|
||||
let frame = data_init.init(
|
||||
frame,
|
||||
Arc::new(FrameData {
|
||||
output: Some(output),
|
||||
region: Rectangle::from_size((1, 1).into()),
|
||||
used: Mutex::new(true),
|
||||
}),
|
||||
);
|
||||
frame.failed();
|
||||
return;
|
||||
};
|
||||
|
||||
let size = region.size;
|
||||
let data = Arc::new(FrameData {
|
||||
output: Some(output),
|
||||
region,
|
||||
used: Mutex::new(false),
|
||||
});
|
||||
let frame = data_init.init(frame, data);
|
||||
|
||||
frame.buffer(
|
||||
wl_shm::Format::Argb8888,
|
||||
size.w as u32,
|
||||
size.h as u32,
|
||||
(size.w as usize * BYTES_PER_PIXEL) as u32,
|
||||
);
|
||||
if frame.version() >= 3 {
|
||||
frame.buffer_done();
|
||||
}
|
||||
}
|
||||
|
||||
fn capture_region(
|
||||
output: &Output,
|
||||
requested: Option<Rectangle<i32, Logical>>,
|
||||
) -> Option<Rectangle<i32, Buffer>> {
|
||||
let mode = output.current_mode()?;
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
let transform = output.current_transform();
|
||||
capture_region_for_geometry(mode.size, scale, transform, requested)
|
||||
}
|
||||
|
||||
fn capture_region_for_geometry(
|
||||
mode_size: Size<i32, smithay::utils::Physical>,
|
||||
scale: f64,
|
||||
transform: smithay::utils::Transform,
|
||||
requested: Option<Rectangle<i32, Logical>>,
|
||||
) -> Option<Rectangle<i32, Buffer>> {
|
||||
let logical_size = transform
|
||||
.transform_size(mode_size)
|
||||
.to_f64()
|
||||
.to_logical(scale)
|
||||
.to_i32_round();
|
||||
let output_region = Rectangle::from_size(logical_size);
|
||||
let logical_region = requested
|
||||
.unwrap_or(output_region)
|
||||
.intersection(output_region)?;
|
||||
|
||||
if logical_region.size.w <= 0 || logical_region.size.h <= 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
logical_region
|
||||
.to_f64()
|
||||
.to_buffer(scale, transform, &logical_size.to_f64())
|
||||
.to_i32_round(),
|
||||
)
|
||||
}
|
||||
|
||||
fn valid_shm_buffer(buffer: &WlBuffer, expected: Size<i32, Buffer>) -> bool {
|
||||
with_buffer_contents_mut(buffer, |_, length, data| {
|
||||
let stride = expected.w.checked_mul(BYTES_PER_PIXEL as i32);
|
||||
let required = data
|
||||
.stride
|
||||
.checked_mul(data.height)
|
||||
.and_then(|bytes| data.offset.checked_add(bytes));
|
||||
|
||||
data.format == wl_shm::Format::Argb8888
|
||||
&& data.width == expected.w
|
||||
&& data.height == expected.h
|
||||
&& Some(data.stride) == stride
|
||||
&& required.is_some_and(|required| required >= 0 && required as usize <= length)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn complete_capture<R>(
|
||||
renderer: &mut R,
|
||||
framebuffer: &R::Framebuffer<'_>,
|
||||
capture: &PendingScreencopy,
|
||||
timestamp: std::time::Duration,
|
||||
) -> Result<(), ()>
|
||||
where
|
||||
R: ExportMem,
|
||||
{
|
||||
let mapping = renderer
|
||||
.copy_framebuffer(framebuffer, capture.region, Fourcc::Argb8888)
|
||||
.map_err(|error| {
|
||||
tracing::warn!(?error, "failed to copy output framebuffer");
|
||||
})?;
|
||||
let source = renderer.map_texture(&mapping).map_err(|error| {
|
||||
tracing::warn!(?error, "failed to map output framebuffer copy");
|
||||
})?;
|
||||
let expected = capture.region.size;
|
||||
let copied = with_buffer_contents_mut(&capture.buffer, |destination, length, data| {
|
||||
let bytes = expected.w as usize * expected.h as usize * BYTES_PER_PIXEL;
|
||||
if source.len() < bytes || data.offset < 0 || data.offset as usize + bytes > length {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Preserve the renderer's row order and report it through the protocol
|
||||
// instead of making another full-frame copy on the compositor thread.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
source.as_ptr(),
|
||||
destination.add(data.offset as usize),
|
||||
bytes,
|
||||
);
|
||||
}
|
||||
true
|
||||
})
|
||||
.map_err(|_| ())?;
|
||||
|
||||
if !copied {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
if capture.with_damage {
|
||||
capture
|
||||
.frame
|
||||
.damage(0, 0, expected.w as u32, expected.h as u32);
|
||||
}
|
||||
let flags = if mapping.flipped() {
|
||||
zwlr_screencopy_frame_v1::Flags::YInvert
|
||||
} else {
|
||||
zwlr_screencopy_frame_v1::Flags::empty()
|
||||
};
|
||||
capture.frame.flags(flags);
|
||||
|
||||
let seconds = timestamp.as_secs();
|
||||
capture.frame.ready(
|
||||
(seconds >> 32) as u32,
|
||||
seconds as u32,
|
||||
timestamp.subsec_nanos(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use smithay::utils::{Physical, Transform};
|
||||
|
||||
#[test]
|
||||
fn capture_region_clips_then_scales_to_buffer_coordinates() {
|
||||
let mode = Size::<i32, Physical>::from((1_920, 1_080));
|
||||
let requested = Rectangle::new((-10, 20).into(), (110, 50).into());
|
||||
|
||||
let region =
|
||||
capture_region_for_geometry(mode, 2.0, Transform::Normal, Some(requested)).unwrap();
|
||||
|
||||
assert_eq!(region, Rectangle::new((0, 40).into(), (200, 100).into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capture_region_rejects_regions_outside_the_output() {
|
||||
let mode = Size::<i32, Physical>::from((1_920, 1_080));
|
||||
let requested = Rectangle::new((1_000, 600).into(), (100, 100).into());
|
||||
|
||||
assert!(
|
||||
capture_region_for_geometry(mode, 2.0, Transform::Normal, Some(requested),).is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,7 @@ pub struct Ferese {
|
||||
pub active_shortcuts_inhibitor: Option<KeyboardShortcutsInhibitor>,
|
||||
pub direct_backend: Option<crate::backends::direct::DirectBackendState>,
|
||||
pending_dmabuf_imports: Vec<(Dmabuf, ImportNotifier)>,
|
||||
pub(crate) pending_screencopies: Vec<crate::handlers::screencopy::PendingScreencopy>,
|
||||
next_window_id: u64,
|
||||
last_animation_tick: Instant,
|
||||
pub popups: PopupManager,
|
||||
@@ -135,6 +136,7 @@ impl Ferese {
|
||||
display: Display<Self>,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
let display_handle = display.handle();
|
||||
crate::handlers::screencopy::init_global(&display_handle);
|
||||
let compositor_state = CompositorState::new::<Self>(&display_handle);
|
||||
let cursor_shape_state = CursorShapeManagerState::new::<Self>(&display_handle);
|
||||
let xdg_shell_state = XdgShellState::new::<Self>(&display_handle);
|
||||
@@ -192,6 +194,7 @@ impl Ferese {
|
||||
active_shortcuts_inhibitor: None,
|
||||
direct_backend: None,
|
||||
pending_dmabuf_imports: Vec::new(),
|
||||
pending_screencopies: Vec::new(),
|
||||
next_window_id: 1,
|
||||
last_animation_tick: start_time,
|
||||
popups: PopupManager::default(),
|
||||
|
||||
+10
-1
@@ -6,7 +6,7 @@ use std::{
|
||||
use smithay::{
|
||||
backend::{
|
||||
renderer::{
|
||||
ImportDma,
|
||||
Frame, ImportDma, Renderer,
|
||||
damage::OutputDamageTracker,
|
||||
element::{
|
||||
AsRenderElements, Kind as RenderElementKind,
|
||||
@@ -142,6 +142,15 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
&elements,
|
||||
[0.035, 0.04, 0.055, 1.0],
|
||||
)?;
|
||||
if state.process_screencopies(renderer, &framebuffer, &output) {
|
||||
let _ = renderer
|
||||
.render(
|
||||
&mut framebuffer,
|
||||
output.current_mode().expect("output has a mode").size,
|
||||
output.current_transform(),
|
||||
)?
|
||||
.finish()?;
|
||||
}
|
||||
}
|
||||
backend.submit(Some(&[damage]))?;
|
||||
Ok(())
|
||||
|
||||
Reference in new issue
Block a user