Support isolated live window ScreenCast streams
Render selected toplevels through a private portal-only capture protocol, preserve logical snapshot geometry, and renegotiate PipeWire video on window resizing. Bound outstanding snapshots and retry temporary contention; stop on source closure and session locking. Validation: portal unit tests (31), capture budget unit, private-bus portal contracts, and real nested/PipeWire isolation, workspace, resize, close and paused-lock tests pass. Independent review cleared. Monitor restore integration timed out on both this build and the unchanged previous compositor under the current nested host.
This commit is contained in:
1 parent
56a888adb3
commit
baca852ee9
26 files changed
+1307
-139
No files matched your search
@@ -109,6 +109,31 @@ impl DirectBackendState {
|
||||
output.current_scale().fractional_scale(),
|
||||
)
|
||||
}
|
||||
pub(crate) fn capture_window_frame(
|
||||
&mut self,
|
||||
state: &Ferese,
|
||||
window: &smithay::desktop::Window,
|
||||
output: &Output,
|
||||
cursor: Option<smithay::utils::Rectangle<i32, smithay::utils::Logical>>,
|
||||
) -> Result<crate::handlers::screenshot::CaptureBuffer, String> {
|
||||
let device = self
|
||||
.devices
|
||||
.values_mut()
|
||||
.find(|device| {
|
||||
device
|
||||
.outputs
|
||||
.values()
|
||||
.any(|candidate| &candidate.output == output)
|
||||
})
|
||||
.ok_or("Window output is unavailable")?;
|
||||
crate::winit::capture_window_frame(
|
||||
&mut device.renderer,
|
||||
window,
|
||||
window.geometry(),
|
||||
output.current_scale().fractional_scale(),
|
||||
cursor.map(|rect| (state, rect)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -5,6 +5,7 @@ mod layer_shell;
|
||||
pub(crate) mod screencopy;
|
||||
pub(crate) mod screenshot;
|
||||
pub(crate) mod screenshot_worker;
|
||||
pub(crate) mod window_capture;
|
||||
mod xdg_shell;
|
||||
|
||||
use smithay::{
|
||||
|
||||
@@ -28,10 +28,11 @@ const BYTES_PER_PIXEL: usize = 4;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct FrameData {
|
||||
output: Option<Output>,
|
||||
region: Rectangle<i32, Buffer>,
|
||||
overlay_cursor: bool,
|
||||
used: Mutex<bool>,
|
||||
pub(super) output: Option<Output>,
|
||||
pub(super) region: Rectangle<i32, Buffer>,
|
||||
pub(super) overlay_cursor: bool,
|
||||
pub(super) used: Mutex<bool>,
|
||||
pub(super) snapshot: Mutex<Option<super::window_capture::Snapshot>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -160,6 +161,7 @@ fn capture_opted_in(value: Option<&std::ffi::OsStr>) -> bool {
|
||||
pub(crate) fn init_global(display: &DisplayHandle) {
|
||||
if capture_allowed() {
|
||||
display.create_global::<Ferese, ZwlrScreencopyManagerV1, ()>(3, ());
|
||||
super::window_capture::init_global(display);
|
||||
tracing::info!("authorized screencopy is enabled for this session");
|
||||
}
|
||||
}
|
||||
@@ -235,7 +237,9 @@ impl Dispatch<ZwlrScreencopyFrameV1, Arc<FrameData>> for Ferese {
|
||||
zwlr_screencopy_frame_v1::Request::CopyWithDamage { buffer } => {
|
||||
state.queue_screencopy(frame, data, buffer, true);
|
||||
}
|
||||
zwlr_screencopy_frame_v1::Request::Destroy => {}
|
||||
zwlr_screencopy_frame_v1::Request::Destroy => {
|
||||
data.snapshot.lock().unwrap().take();
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -249,6 +253,10 @@ impl Ferese {
|
||||
buffer: WlBuffer,
|
||||
with_damage: bool,
|
||||
) {
|
||||
if data.snapshot.lock().unwrap().is_some() {
|
||||
super::window_capture::copy_snapshot(self, frame, data, &buffer);
|
||||
return;
|
||||
}
|
||||
let mut used = data.used.lock().unwrap();
|
||||
if self.session_lock.active {
|
||||
frame.failed();
|
||||
@@ -354,6 +362,7 @@ fn create_frame(
|
||||
region: Rectangle::from_size((1, 1).into()),
|
||||
overlay_cursor,
|
||||
used: Mutex::new(true),
|
||||
snapshot: Mutex::new(None),
|
||||
}),
|
||||
);
|
||||
frame.failed();
|
||||
@@ -367,6 +376,7 @@ fn create_frame(
|
||||
region: Rectangle::from_size((1, 1).into()),
|
||||
overlay_cursor,
|
||||
used: Mutex::new(true),
|
||||
snapshot: Mutex::new(None),
|
||||
}),
|
||||
);
|
||||
frame.failed();
|
||||
@@ -379,6 +389,7 @@ fn create_frame(
|
||||
region,
|
||||
overlay_cursor,
|
||||
used: Mutex::new(false),
|
||||
snapshot: Mutex::new(None),
|
||||
});
|
||||
let frame = data_init.init(frame, data);
|
||||
|
||||
@@ -439,7 +450,7 @@ pub(crate) fn capture_region_for_geometry(
|
||||
)
|
||||
}
|
||||
|
||||
fn valid_shm_buffer(buffer: &WlBuffer, expected: Size<i32, Buffer>) -> bool {
|
||||
pub(super) 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
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
use std::{
|
||||
os::unix::fs::MetadataExt,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use ferese_layout::WindowId;
|
||||
use ferese_protocols::window_capture::v1::server::ferese_window_capture_manager_v1::{
|
||||
self as manager, FereseWindowCaptureManagerV1,
|
||||
};
|
||||
use smithay::{
|
||||
reexports::{
|
||||
wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::{
|
||||
self as frame, ZwlrScreencopyFrameV1,
|
||||
},
|
||||
wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
protocol::{wl_buffer::WlBuffer, wl_shm},
|
||||
},
|
||||
},
|
||||
utils::Rectangle,
|
||||
wayland::shm::with_buffer_contents_mut,
|
||||
};
|
||||
|
||||
use super::{screencopy::FrameData, screenshot::CaptureBuffer};
|
||||
use crate::Ferese;
|
||||
|
||||
const MAX_BYTES: usize = 128 * 1024 * 1024;
|
||||
const MAX_FRAMES: usize = 8;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Budget {
|
||||
bytes: usize,
|
||||
frames: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Permit {
|
||||
budget: Arc<Mutex<Budget>>,
|
||||
bytes: usize,
|
||||
}
|
||||
|
||||
impl Permit {
|
||||
fn reserve(budget: &Arc<Mutex<Budget>>, bytes: usize) -> Option<Self> {
|
||||
let mut available = budget.lock().unwrap();
|
||||
if bytes == 0
|
||||
|| bytes > MAX_BYTES.saturating_sub(available.bytes)
|
||||
|| available.frames >= MAX_FRAMES
|
||||
{
|
||||
return None;
|
||||
}
|
||||
available.bytes += bytes;
|
||||
available.frames += 1;
|
||||
Some(Self {
|
||||
budget: budget.clone(),
|
||||
bytes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Permit {
|
||||
fn drop(&mut self) {
|
||||
let mut budget = self.budget.lock().unwrap();
|
||||
budget.bytes -= self.bytes;
|
||||
budget.frames -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct Snapshot {
|
||||
id: WindowId,
|
||||
logical_size: (u32, u32),
|
||||
pixels: CaptureBuffer,
|
||||
_permit: Permit,
|
||||
}
|
||||
|
||||
pub(crate) struct Global {
|
||||
budget: Arc<Mutex<Budget>>,
|
||||
}
|
||||
|
||||
pub(crate) fn is_portal(stream: &std::os::unix::net::UnixStream) -> bool {
|
||||
use std::os::fd::AsRawFd;
|
||||
let mut credentials = libc::ucred {
|
||||
pid: 0,
|
||||
uid: 0,
|
||||
gid: 0,
|
||||
};
|
||||
let mut length = std::mem::size_of_val(&credentials) as libc::socklen_t;
|
||||
// SAFETY: the connected socket is live and both out pointers cover their
|
||||
// initialized stack allocations. SO_PEERCRED identifies the connecting process.
|
||||
let valid = unsafe {
|
||||
libc::getsockopt(
|
||||
stream.as_raw_fd(),
|
||||
libc::SOL_SOCKET,
|
||||
libc::SO_PEERCRED,
|
||||
(&raw mut credentials).cast(),
|
||||
&raw mut length,
|
||||
) == 0
|
||||
};
|
||||
if !valid || length as usize != std::mem::size_of_val(&credentials) || credentials.pid <= 0 {
|
||||
return false;
|
||||
}
|
||||
let expected = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|exe| {
|
||||
exe.parent()
|
||||
.map(|dir| dir.join("xdg-desktop-portal-ferese"))
|
||||
})
|
||||
.and_then(|exe| std::fs::metadata(exe).ok());
|
||||
let actual = std::fs::metadata(format!("/proc/{}/exe", credentials.pid)).ok();
|
||||
matches!((expected, actual), (Some(expected), Some(actual)) if expected.dev() == actual.dev() && expected.ino() == actual.ino())
|
||||
}
|
||||
|
||||
pub(super) fn init_global(display: &DisplayHandle) {
|
||||
display.create_global::<Ferese, FereseWindowCaptureManagerV1, Global>(
|
||||
1,
|
||||
Global {
|
||||
budget: Arc::default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
impl GlobalDispatch<FereseWindowCaptureManagerV1, Global> for Ferese {
|
||||
fn can_view(client: Client, _: &Global) -> bool {
|
||||
client
|
||||
.get_data::<crate::state::ClientState>()
|
||||
.is_some_and(|state| {
|
||||
state
|
||||
.capabilities
|
||||
.contains(crate::private_client::ClientCapabilities::WINDOW_CAPTURE)
|
||||
})
|
||||
}
|
||||
|
||||
fn bind(
|
||||
_: &mut Self,
|
||||
_: &DisplayHandle,
|
||||
_: &Client,
|
||||
resource: New<FereseWindowCaptureManagerV1>,
|
||||
global: &Global,
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
data_init.init(resource, global.budget.clone());
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<FereseWindowCaptureManagerV1, Arc<Mutex<Budget>>> for Ferese {
|
||||
fn request(
|
||||
state: &mut Self,
|
||||
_: &Client,
|
||||
manager: &FereseWindowCaptureManagerV1,
|
||||
request: manager::Request,
|
||||
budget: &Arc<Mutex<Budget>>,
|
||||
_: &DisplayHandle,
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
if let manager::Request::CaptureWindow {
|
||||
window_hi,
|
||||
window_lo,
|
||||
overlay_cursor,
|
||||
frame,
|
||||
} = request
|
||||
{
|
||||
let id = WindowId((u64::from(window_hi) << 32) | u64::from(window_lo));
|
||||
let mut busy = false;
|
||||
let snapshot = (overlay_cursor <= 1)
|
||||
.then(|| state.window_snapshot(id, overlay_cursor == 1, budget, &mut busy))
|
||||
.flatten();
|
||||
let size = snapshot
|
||||
.as_ref()
|
||||
.map(|snapshot| (snapshot.pixels.width, snapshot.pixels.height))
|
||||
.unwrap_or((1, 1));
|
||||
let logical_size = snapshot.as_ref().map(|snapshot| snapshot.logical_size);
|
||||
let failed = snapshot.is_none();
|
||||
let resource = data_init.init(
|
||||
frame,
|
||||
Arc::new(FrameData {
|
||||
output: None,
|
||||
region: Rectangle::from_size(size.into()),
|
||||
overlay_cursor: false,
|
||||
used: Mutex::new(failed),
|
||||
snapshot: Mutex::new(snapshot),
|
||||
}),
|
||||
);
|
||||
if failed {
|
||||
if busy {
|
||||
manager.busy(&resource);
|
||||
}
|
||||
resource.failed();
|
||||
} else {
|
||||
let logical_size = logical_size.unwrap();
|
||||
manager.geometry(&resource, logical_size.0, logical_size.1);
|
||||
resource.buffer(
|
||||
wl_shm::Format::Argb8888,
|
||||
size.0 as u32,
|
||||
size.1 as u32,
|
||||
size.0 as u32 * 4,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ferese {
|
||||
fn window_snapshot(
|
||||
&mut self,
|
||||
id: WindowId,
|
||||
overlay_cursor: bool,
|
||||
budget: &Arc<Mutex<Budget>>,
|
||||
busy: &mut bool,
|
||||
) -> Option<Snapshot> {
|
||||
if self.session_lock.active || !super::screencopy::capture_allowed() {
|
||||
return None;
|
||||
}
|
||||
let window = self
|
||||
.window_ids
|
||||
.iter()
|
||||
.find(|(_, candidate)| **candidate == id)?
|
||||
.0
|
||||
.clone();
|
||||
let workspace = self.workspaces.workspace_for_window(id)?;
|
||||
let owner = self.output_workspaces.output_for_workspace(workspace)?;
|
||||
let output = self
|
||||
.output_ids
|
||||
.iter()
|
||||
.find(|(_, candidate)| **candidate == owner)?
|
||||
.0
|
||||
.clone();
|
||||
let geometry = window.geometry();
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
let size: smithay::utils::Size<i32, smithay::utils::Physical> =
|
||||
geometry.size.to_physical_precise_round(scale);
|
||||
let bytes = usize::try_from(size.w)
|
||||
.ok()?
|
||||
.checked_mul(usize::try_from(size.h).ok()?)?
|
||||
.checked_mul(4)?;
|
||||
if bytes == 0 || bytes > MAX_BYTES {
|
||||
return None;
|
||||
}
|
||||
let Some(permit) = Permit::reserve(budget, bytes) else {
|
||||
*busy = true;
|
||||
return None;
|
||||
};
|
||||
let cursor = if overlay_cursor && !self.overview.is_presenting() {
|
||||
self.seat.get_pointer().and_then(|pointer| {
|
||||
let location = pointer.current_location();
|
||||
if self.window_under_visual(location).as_ref() != Some(&window) {
|
||||
return None;
|
||||
}
|
||||
let (x, y) = self.inverse_presented_window_point(id, location.x, location.y)?;
|
||||
let surface_point = smithay::utils::Point::from((x, y)) + geometry.loc.to_f64();
|
||||
let (surface, _) = window
|
||||
.surface_under(surface_point, smithay::desktop::WindowSurfaceType::ALL)?;
|
||||
if pointer.current_focus().as_ref() != Some(&surface) {
|
||||
return None;
|
||||
}
|
||||
Some(Rectangle::new(
|
||||
(location - smithay::utils::Point::from((x, y))).to_i32_round(),
|
||||
geometry.size,
|
||||
))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let result = if let Some(backend) = self.nested_backend.clone() {
|
||||
let mut backend = backend.try_borrow_mut().ok()?;
|
||||
crate::winit::capture_window_frame(
|
||||
backend.renderer(),
|
||||
&window,
|
||||
geometry,
|
||||
scale,
|
||||
cursor.map(|rect| (&*self, rect)),
|
||||
)
|
||||
} else if let Some(mut backend) = self.direct_backend.take() {
|
||||
let result = backend.capture_window_frame(self, &window, &output, cursor);
|
||||
self.direct_backend = Some(backend);
|
||||
result
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
let pixels = result.ok()?;
|
||||
if let Some(toplevel) = window.toplevel() {
|
||||
smithay::desktop::utils::send_frames_surface_tree(
|
||||
toplevel.wl_surface(),
|
||||
&output,
|
||||
self.start_time.elapsed(),
|
||||
Some(std::time::Duration::ZERO),
|
||||
|_, _| Some(output.clone()),
|
||||
);
|
||||
}
|
||||
if pixels.pixels.len() != bytes {
|
||||
return None;
|
||||
}
|
||||
Some(Snapshot {
|
||||
id,
|
||||
logical_size: (geometry.size.w as u32, geometry.size.h as u32),
|
||||
pixels,
|
||||
_permit: permit,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn copy_snapshot(
|
||||
state: &Ferese,
|
||||
resource: &ZwlrScreencopyFrameV1,
|
||||
data: &FrameData,
|
||||
buffer: &WlBuffer,
|
||||
) {
|
||||
let mut used = data.used.lock().unwrap();
|
||||
if *used {
|
||||
resource.post_error(
|
||||
frame::Error::AlreadyUsed,
|
||||
"window capture frame has already been used",
|
||||
);
|
||||
return;
|
||||
}
|
||||
*used = true;
|
||||
let Some(snapshot) = data.snapshot.lock().unwrap().take() else {
|
||||
resource.failed();
|
||||
return;
|
||||
};
|
||||
if state.session_lock.active
|
||||
|| !super::screencopy::capture_allowed()
|
||||
|| !state.window_ids.values().any(|id| *id == snapshot.id)
|
||||
{
|
||||
resource.failed();
|
||||
return;
|
||||
}
|
||||
if !super::screencopy::valid_shm_buffer(buffer, data.region.size) {
|
||||
resource.post_error(
|
||||
frame::Error::InvalidBuffer,
|
||||
"window capture requires the advertised ARGB8888 SHM buffer",
|
||||
);
|
||||
return;
|
||||
}
|
||||
let copied = with_buffer_contents_mut(buffer, |destination, length, layout| {
|
||||
let bytes = snapshot.pixels.pixels.len();
|
||||
let Ok(offset) = usize::try_from(layout.offset) else {
|
||||
return false;
|
||||
};
|
||||
if offset.checked_add(bytes).is_none_or(|end| end > length) {
|
||||
return false;
|
||||
}
|
||||
// SAFETY: Smithay owns the writable mapping, validated format/stride and
|
||||
// checked offset cover the complete snapshot; the source is separately owned.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
snapshot.pixels.pixels.as_ptr(),
|
||||
destination.add(offset),
|
||||
bytes,
|
||||
);
|
||||
}
|
||||
true
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if !copied {
|
||||
resource.failed();
|
||||
return;
|
||||
}
|
||||
let timestamp = state.start_time.elapsed();
|
||||
resource.flags(frame::Flags::empty());
|
||||
resource.ready(
|
||||
(timestamp.as_secs() >> 32) as u32,
|
||||
timestamp.as_secs() as u32,
|
||||
timestamp.subsec_nanos(),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn outstanding_snapshots_release_both_limits_when_dropped() {
|
||||
let budget = Arc::default();
|
||||
let permits = (0..MAX_FRAMES)
|
||||
.map(|_| Permit::reserve(&budget, 4).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(Permit::reserve(&budget, 4).is_none());
|
||||
drop(permits);
|
||||
let full = Permit::reserve(&budget, MAX_BYTES).unwrap();
|
||||
assert!(Permit::reserve(&budget, 4).is_none());
|
||||
drop(full);
|
||||
assert!(Permit::reserve(&budget, MAX_BYTES).is_some());
|
||||
assert!(Permit::reserve(&budget, MAX_BYTES + 1).is_none());
|
||||
assert!(Permit::reserve(&budget, 0).is_none());
|
||||
assert_eq!(budget.lock().unwrap().bytes, 0);
|
||||
assert_eq!(budget.lock().unwrap().frames, 0);
|
||||
}
|
||||
}
|
||||
@@ -868,6 +868,8 @@ impl Ferese {
|
||||
"workspace": self.workspaces.workspace_for_window(*id).map(|workspace| workspace.0),
|
||||
"x": rect.as_ref().map(|rect| rect.loc.x),
|
||||
"y": rect.as_ref().map(|rect| rect.loc.y),
|
||||
"capture_width": window.geometry().size.w,
|
||||
"capture_height": window.geometry().size.h,
|
||||
"width": rect.as_ref().map(|rect| rect.size.w),
|
||||
"height": rect.as_ref().map(|rect| rect.size.h),
|
||||
}))
|
||||
|
||||
@@ -16,6 +16,7 @@ pub(crate) struct ClientCapabilities(u8);
|
||||
impl ClientCapabilities {
|
||||
pub(crate) const EFFECTS: Self = Self(1 << 0);
|
||||
pub(crate) const SHELL_CONTROL: Self = Self(1 << 1);
|
||||
pub(crate) const WINDOW_CAPTURE: Self = Self(1 << 2);
|
||||
|
||||
pub(crate) fn contains(self, capability: Self) -> bool {
|
||||
self.0 & capability.0 == capability.0
|
||||
|
||||
@@ -865,9 +865,15 @@ impl Ferese {
|
||||
let loop_handle = event_loop.handle();
|
||||
|
||||
loop_handle.insert_source(listening_socket, |client_stream, _, state| {
|
||||
let mut client_state = ClientState::default();
|
||||
if crate::handlers::window_capture::is_portal(&client_stream) {
|
||||
client_state
|
||||
.capabilities
|
||||
.insert(crate::private_client::ClientCapabilities::WINDOW_CAPTURE);
|
||||
}
|
||||
if let Err(error) = state
|
||||
.display_handle
|
||||
.insert_client(client_stream, Arc::new(ClientState::default()))
|
||||
.insert_client(client_stream, Arc::new(client_state))
|
||||
{
|
||||
tracing::warn!(%error, "failed to register Wayland client");
|
||||
}
|
||||
|
||||
+32
-4
@@ -176,10 +176,38 @@ pub(crate) fn capture_window_buffer(
|
||||
geometry: Rectangle<i32, Logical>,
|
||||
scale: f64,
|
||||
) -> Result<crate::handlers::screenshot::CaptureBuffer, String> {
|
||||
let snapshot = capture_resize_snapshot(renderer, window, geometry, scale, 128 * 1024 * 1024)
|
||||
.map_err(|error| error.to_string())?
|
||||
.ok_or("Window has no capturable content")?;
|
||||
capture_window_frame(renderer, window, geometry, scale, None)
|
||||
}
|
||||
|
||||
pub(crate) fn capture_window_frame(
|
||||
renderer: &mut GlesRenderer,
|
||||
window: &smithay::desktop::Window,
|
||||
geometry: Rectangle<i32, Logical>,
|
||||
scale: f64,
|
||||
cursor: Option<(&Ferese, Rectangle<i32, Logical>)>,
|
||||
) -> Result<crate::handlers::screenshot::CaptureBuffer, String> {
|
||||
let mut snapshot =
|
||||
capture_resize_snapshot(renderer, window, geometry, scale, 128 * 1024 * 1024)
|
||||
.map_err(|error| error.to_string())?
|
||||
.ok_or("Window has no capturable content")?;
|
||||
let size = snapshot.texture.size();
|
||||
if let Some((state, cursor_geometry)) = cursor {
|
||||
let elements = cursor_elements(state, renderer, cursor_geometry, scale);
|
||||
let mut target = renderer
|
||||
.bind(&mut snapshot.texture)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut frame = renderer
|
||||
.render(&mut target, (size.w, size.h).into(), Transform::Normal)
|
||||
.map_err(|e| e.to_string())?;
|
||||
draw_render_elements(
|
||||
&mut frame,
|
||||
scale,
|
||||
&elements,
|
||||
&[Rectangle::from_size((size.w, size.h).into())],
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let _ = frame.finish().map_err(|e| e.to_string())?;
|
||||
}
|
||||
let mapping = renderer
|
||||
.copy_texture(
|
||||
&snapshot.texture,
|
||||
@@ -551,7 +579,7 @@ fn blur_damage(
|
||||
DamageSet::default()
|
||||
} else {
|
||||
// A new scene needs its entire sampling halo repainted before capture.
|
||||
DamageSet::from_slice(&[Rectangle::from_size(size)])
|
||||
DamageSet::from_slice(&[Rectangle::from_size((size.w, size.h).into())])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user