perf(capture): add SceneLens GPU streams and retained scene reuse
Use persistent privacy-filtered capture targets and client-owned GPU buffers with fence completion, backpressure, negotiated frame rates and shared-memory fallback. Invalidate output views on scene changes while preserving inactive-window callbacks and capture revocation. Include OBS memory-mask negotiation and native pixel, transport and recording regressions.
This commit is contained in:
1 parent
f49f62583d
commit
533cd74c94
40 files changed
+4704
-704
No files matched your search
+8
-258
@@ -1,19 +1,13 @@
|
||||
use std::fs::File;
|
||||
use std::os::fd::{AsFd, AsRawFd, FromRawFd};
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use ferese_protocols::window_capture::v1::client::ferese_window_capture_manager_v1 as window_manager;
|
||||
use memmap2::MmapMut;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wayland_client::protocol::{wl_buffer, wl_output, wl_registry, wl_shm, wl_shm_pool};
|
||||
use wayland_client::protocol::{wl_output, wl_registry, wl_shm};
|
||||
use wayland_client::{Connection, Dispatch, EventQueue, QueueHandle, WEnum};
|
||||
use wayland_protocols_wlr::screencopy::v1::client::{
|
||||
zwlr_screencopy_frame_v1 as frame, zwlr_screencopy_manager_v1 as manager,
|
||||
};
|
||||
|
||||
pub const BUSY: &str = "Window capture snapshot pool is busy";
|
||||
use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_manager_v1 as manager;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Source {
|
||||
@@ -34,53 +28,23 @@ impl Source {
|
||||
|
||||
struct Output {
|
||||
global: u32,
|
||||
proxy: wl_output::WlOutput,
|
||||
_proxy: wl_output::WlOutput,
|
||||
source: Source,
|
||||
transform: wl_output::Transform,
|
||||
}
|
||||
|
||||
pub struct Frame {
|
||||
pub logical_size: Option<(u32, u32)>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub stride: u32,
|
||||
pub pixels: Vec<u8>,
|
||||
}
|
||||
|
||||
struct Buffer {
|
||||
_file: File,
|
||||
map: MmapMut,
|
||||
proxy: wl_buffer::WlBuffer,
|
||||
width: u32,
|
||||
height: u32,
|
||||
stride: u32,
|
||||
format: wl_shm::Format,
|
||||
}
|
||||
|
||||
impl Drop for Buffer {
|
||||
fn drop(&mut self) {
|
||||
self.proxy.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
outputs: Vec<Output>,
|
||||
manager: Option<manager::ZwlrScreencopyManagerV1>,
|
||||
window_manager: Option<window_manager::FereseWindowCaptureManagerV1>,
|
||||
shm: Option<wl_shm::WlShm>,
|
||||
buffer: Option<Buffer>,
|
||||
result: Option<Result<(), String>>,
|
||||
flipped: bool,
|
||||
logical_size: Option<(u32, u32)>,
|
||||
busy: bool,
|
||||
}
|
||||
|
||||
pub struct Capture {
|
||||
connection: Connection,
|
||||
queue: EventQueue<State>,
|
||||
state: State,
|
||||
selected_output: Option<u32>,
|
||||
}
|
||||
|
||||
impl Capture {
|
||||
@@ -96,7 +60,6 @@ impl Capture {
|
||||
connection,
|
||||
queue,
|
||||
state: State::default(),
|
||||
selected_output: None,
|
||||
};
|
||||
// Two syncs deliver registry bindings and output metadata without an
|
||||
// uninterruptible roundtrip on a stalled compositor.
|
||||
@@ -134,14 +97,6 @@ impl Capture {
|
||||
.map(|output| output.global)
|
||||
}
|
||||
|
||||
pub fn pin_output(&mut self, name: &str, generation: u32) -> Result<(), String> {
|
||||
if self.generation(name) != Some(generation) {
|
||||
return Err("Shared monitor was replaced".into());
|
||||
}
|
||||
self.selected_output = Some(generation);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_sources(
|
||||
&mut self,
|
||||
selected: &[(String, u32)],
|
||||
@@ -170,99 +125,6 @@ impl Capture {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn frame(
|
||||
&mut self,
|
||||
name: &str,
|
||||
cursor: bool,
|
||||
stop: &AtomicBool,
|
||||
mut pixels: Vec<u8>,
|
||||
) -> Result<Frame, String> {
|
||||
self.state.result = None;
|
||||
self.state.flipped = false;
|
||||
self.state.logical_size = None;
|
||||
self.state.busy = false;
|
||||
let (frame, transform) = if let Some(id) = name
|
||||
.strip_prefix("window:")
|
||||
.and_then(|id| id.parse::<u64>().ok())
|
||||
{
|
||||
let manager = self
|
||||
.state
|
||||
.window_manager
|
||||
.as_ref()
|
||||
.ok_or("Window capture is unavailable")?;
|
||||
(
|
||||
manager.capture_window(
|
||||
(id >> 32) as u32,
|
||||
id as u32,
|
||||
cursor as u32,
|
||||
&self.queue.handle(),
|
||||
(),
|
||||
),
|
||||
wl_output::Transform::Normal,
|
||||
)
|
||||
} else {
|
||||
let output = self
|
||||
.state
|
||||
.outputs
|
||||
.iter()
|
||||
.find(|o| o.source.name == name)
|
||||
.ok_or("Shared monitor disconnected")?;
|
||||
if self
|
||||
.selected_output
|
||||
.is_some_and(|selected| selected != output.global)
|
||||
{
|
||||
return Err("Shared monitor was replaced".into());
|
||||
}
|
||||
self.selected_output = Some(output.global);
|
||||
let manager = self
|
||||
.state
|
||||
.manager
|
||||
.as_ref()
|
||||
.ok_or("Monitor capture is unavailable")?;
|
||||
(
|
||||
manager.capture_output(cursor as i32, &output.proxy, &self.queue.handle(), ()),
|
||||
output.transform,
|
||||
)
|
||||
};
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
|
||||
while self.state.result.is_none() {
|
||||
if let Err(error) = self.dispatch(stop, deadline) {
|
||||
frame.destroy();
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
frame.destroy();
|
||||
self.state
|
||||
.result
|
||||
.take()
|
||||
.expect("the capture dispatch loop exits after storing its result")?;
|
||||
|
||||
let buffer = self
|
||||
.state
|
||||
.buffer
|
||||
.as_ref()
|
||||
.ok_or("Capture supplied no buffer")?;
|
||||
let (width, height) = copy_oriented_pixels(
|
||||
&buffer.map,
|
||||
buffer.width,
|
||||
buffer.height,
|
||||
buffer.stride,
|
||||
transform,
|
||||
self.state.flipped,
|
||||
&mut pixels,
|
||||
);
|
||||
|
||||
Ok(Frame {
|
||||
logical_size: self.state.logical_size,
|
||||
width,
|
||||
height,
|
||||
stride: width * 4,
|
||||
pixels,
|
||||
})
|
||||
}
|
||||
|
||||
fn dispatch(&mut self, stop: &AtomicBool, deadline: Instant) -> Result<(), String> {
|
||||
if stop.load(Ordering::Relaxed) {
|
||||
return Err("Capture cancelled".into());
|
||||
@@ -321,7 +183,7 @@ impl Dispatch<wl_registry::WlRegistry, ()> for State {
|
||||
"wl_output" => state.outputs.push(Output {
|
||||
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 {
|
||||
name: format!("output-{name}"),
|
||||
label: "Display".into(),
|
||||
@@ -387,100 +249,6 @@ impl Dispatch<wl_output::WlOutput, u32> for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<frame::ZwlrScreencopyFrameV1, ()> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
frame: &frame::ZwlrScreencopyFrameV1,
|
||||
event: frame::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
frame::Event::Buffer {
|
||||
format: WEnum::Value(format),
|
||||
width,
|
||||
height,
|
||||
stride,
|
||||
} => {
|
||||
let result = (|| {
|
||||
if format != wl_shm::Format::Argb8888 && format != wl_shm::Format::Xrgb8888 {
|
||||
return Err("Unsupported capture pixel format".into());
|
||||
}
|
||||
|
||||
let bytes = buffer_size(width, height, stride)?;
|
||||
if let Some(buffer) = &state.buffer
|
||||
&& (buffer.width, buffer.height, buffer.stride, buffer.format)
|
||||
== (width, height, stride, format)
|
||||
{
|
||||
frame.copy(&buffer.proxy);
|
||||
return Ok(());
|
||||
}
|
||||
// SAFETY: fixed NUL-terminated name and a newly owned memfd.
|
||||
let fd = unsafe {
|
||||
libc::memfd_create(c"ferese-screencast".as_ptr(), libc::MFD_CLOEXEC)
|
||||
};
|
||||
|
||||
if fd < 0 {
|
||||
return Err(std::io::Error::last_os_error().to_string());
|
||||
}
|
||||
|
||||
// SAFETY: memfd_create succeeded above and returned a fresh descriptor whose
|
||||
// ownership transfers to File.
|
||||
let file = unsafe { File::from_raw_fd(fd) };
|
||||
file.set_len(bytes as u64).map_err(|e| e.to_string())?;
|
||||
// SAFETY: the file has the checked mapping length and lives with the mapping.
|
||||
let map = unsafe { MmapMut::map_mut(&file) }.map_err(|e| e.to_string())?;
|
||||
let pool = state
|
||||
.shm
|
||||
.as_ref()
|
||||
.ok_or("Missing shared memory global")?
|
||||
.create_pool(file.as_fd(), bytes as i32, qh, ());
|
||||
let proxy = pool.create_buffer(
|
||||
0,
|
||||
width as i32,
|
||||
height as i32,
|
||||
stride as i32,
|
||||
format,
|
||||
qh,
|
||||
(),
|
||||
);
|
||||
|
||||
pool.destroy();
|
||||
frame.copy(&proxy);
|
||||
state.buffer = Some(Buffer {
|
||||
_file: file,
|
||||
map,
|
||||
proxy,
|
||||
width,
|
||||
height,
|
||||
stride,
|
||||
format,
|
||||
});
|
||||
|
||||
Ok::<(), String>(())
|
||||
})();
|
||||
|
||||
if let Err(e) = result {
|
||||
state.result = Some(Err(e));
|
||||
}
|
||||
}
|
||||
frame::Event::Ready { .. } => state.result = Some(Ok(())),
|
||||
frame::Event::Failed => {
|
||||
state.result = Some(Err(if state.busy {
|
||||
BUSY.into()
|
||||
} else {
|
||||
"Capture ended: session locked, source closed, or compositor unavailable".into()
|
||||
}))
|
||||
}
|
||||
frame::Event::Flags {
|
||||
flags: WEnum::Value(flags),
|
||||
} => state.flipped = flags.contains(frame::Flags::YInvert),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn swaps_axes(transform: wl_output::Transform) -> bool {
|
||||
matches!(
|
||||
transform,
|
||||
@@ -494,6 +262,7 @@ fn swaps_axes(transform: wl_output::Transform) -> bool {
|
||||
// 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.
|
||||
#[cfg(test)]
|
||||
fn copy_oriented_pixels(
|
||||
source: &[u8],
|
||||
width: u32,
|
||||
@@ -545,6 +314,7 @@ fn copy_oriented_pixels(
|
||||
(out_width, out_height)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn buffer_size(width: u32, height: u32, stride: u32) -> Result<usize, String> {
|
||||
let row = width.checked_mul(4).ok_or("Capture dimensions overflow")?;
|
||||
let bytes = stride
|
||||
@@ -572,27 +342,8 @@ impl Dispatch<wayland_client::protocol::wl_callback::WlCallback, Arc<AtomicBool>
|
||||
}
|
||||
|
||||
wayland_client::delegate_noop!(State: ignore wl_shm::WlShm);
|
||||
wayland_client::delegate_noop!(State: ignore wl_shm_pool::WlShmPool);
|
||||
wayland_client::delegate_noop!(State: ignore wl_buffer::WlBuffer);
|
||||
wayland_client::delegate_noop!(State: ignore manager::ZwlrScreencopyManagerV1);
|
||||
impl Dispatch<window_manager::FereseWindowCaptureManagerV1, ()> for State {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_: &window_manager::FereseWindowCaptureManagerV1,
|
||||
event: window_manager::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
window_manager::Event::Geometry { width, height, .. } => {
|
||||
state.logical_size = Some((width, height))
|
||||
}
|
||||
window_manager::Event::Busy { .. } => state.busy = true,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
wayland_client::delegate_noop!(State: ignore window_manager::FereseWindowCaptureManagerV1);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -622,7 +373,6 @@ mod tests {
|
||||
connection,
|
||||
queue,
|
||||
state: State::default(),
|
||||
selected_output: None,
|
||||
};
|
||||
capture.validate_sources(&[], &flag).unwrap_err()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user