use std::error::Error;
use std::os::fd::AsFd;
use std::time::{Duration, Instant};
use wayland_client::protocol::{
wl_buffer::{self, WlBuffer},
wl_callback, wl_output, wl_shm,
wl_subsurface::WlSubsurface,
wl_surface::WlSurface,
};
use wayland_client::{Connection, Dispatch, QueueHandle, WEnum};
use wayland_protocols::ext::image_capture_source::v1::client::ext_image_capture_source_v1::ExtImageCaptureSourceV1;
use wayland_protocols::ext::image_copy_capture::v1::client::{
ext_image_copy_capture_frame_v1::{self, ExtImageCopyCaptureFrameV1},
ext_image_copy_capture_manager_v1,
ext_image_copy_capture_session_v1::{self, ExtImageCopyCaptureSessionV1},
};
use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport;
use crate::app::App;
use crate::shm;
use crate::target::{Kind, Target};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Live {
All,
Current,
None,
}
impl Live {
pub fn parse(s: &str) -> Result<Self, String> {
match s.trim() {
"all" => Ok(Live::All),
"current" => Ok(Live::Current),
"none" => Ok(Live::None),
other => Err(format!("{other:?} is not all, current or none")),
}
}
}
pub struct Slot {
pub(crate) buffer: WlBuffer,
pub(crate) busy: bool,
}
pub struct Tile {
pub(crate) target: Target,
pub(crate) session: Option<ExtImageCopyCaptureSessionV1>,
pub(crate) frame: Option<ExtImageCopyCaptureFrameV1>,
pub(crate) filling: Option<usize>,
pub(crate) slots: Vec<Slot>,
pub(crate) showing: Option<usize>,
pub(crate) formats: Vec<wl_shm::Format>,
pub(crate) format: Option<wl_shm::Format>,
pub(crate) size: (u32, u32),
pub(crate) transform: wl_output::Transform,
pub(crate) session_done: bool,
pub(crate) ready: bool,
pub(crate) settled: bool,
pub(crate) asked: Option<Instant>,
pub(crate) frames: u32,
pub(crate) surface: Option<WlSurface>,
pub(crate) subsurface: Option<WlSubsurface>,
pub(crate) viewport: Option<WpViewport>,
}
impl Tile {
pub fn new(target: Target) -> Self {
Self {
target,
session: None,
frame: None,
filling: None,
slots: Vec::new(),
showing: None,
formats: Vec::new(),
format: None,
size: (0, 0),
transform: wl_output::Transform::Normal,
session_done: false,
ready: false,
settled: false,
asked: None,
frames: 0,
surface: None,
subsurface: None,
viewport: None,
}
}
pub fn bytes(&self) -> usize {
self.size.0 as usize * 4 * self.size.1 as usize
}
pub fn rotated(&self) -> bool {
use wl_output::Transform;
matches!(
self.transform,
Transform::_90 | Transform::_270 | Transform::Flipped90 | Transform::Flipped270
)
}
}
impl App {
pub fn open_sessions(&mut self, qh: &QueueHandle<Self>) {
for (i, tile) in self.tiles.iter_mut().enumerate() {
let source: Option<ExtImageCaptureSourceV1> = match tile.target.kind {
Kind::Window => self
.toplevels
.iter()
.find(|(_, id)| !id.is_empty() && *id == tile.target.ft_id)
.map(|(handle, _)| self.src_mgr.create_source(handle, qh, ())),
Kind::Output => self
.outputs
.iter()
.find(|(_, n)| *n == tile.target.id)
.and_then(|(output, _)| {
self.output_src_mgr
.as_ref()
.map(|mgr| mgr.create_source(output, qh, ()))
}),
};
let Some(source) = source else {
tile.settled = true;
continue;
};
tile.session = Some(self.copy_mgr.create_session(
&source,
ext_image_copy_capture_manager_v1::Options::empty(),
qh,
i,
));
source.destroy();
}
}
pub fn start_captures(&mut self, qh: &QueueHandle<Self>) -> Result<(), Box<dyn Error>> {
const PAGE: usize = 4096;
let mut total = 0usize;
let mut offsets: Vec<Vec<usize>> = Vec::with_capacity(self.tiles.len());
for tile in &mut self.tiles {
offsets.push(Vec::new());
if tile.session.is_none() {
continue;
}
if !tile.session_done || tile.size.0 == 0 || tile.size.1 == 0 {
tile.settled = true;
continue;
}
tile.format = tile
.formats
.iter()
.copied()
.find(|f| matches!(f, wl_shm::Format::Xrgb8888 | wl_shm::Format::Argb8888))
.or_else(|| tile.formats.first().copied());
if tile.format.is_none() {
tile.settled = true;
continue;
}
let slots = if self.live == Live::None || tile.target.kind == Kind::Output {
1
} else {
2
};
let last = offsets.last_mut().expect("just pushed");
for _ in 0..slots {
last.push(total);
total += tile.bytes().div_ceil(PAGE) * PAGE;
}
}
if total == 0 {
return Ok(());
}
self.stats.pool_bytes = total;
let file = shm::memfd("wl-pick-capture", total)?;
let pool = self.shm.create_pool(file.as_fd(), total as i32, qh, ());
for (i, slot_offsets) in offsets.iter().enumerate() {
let (w, h, format) = {
let t = &self.tiles[i];
if t.settled || t.session.is_none() || t.format.is_none() {
continue;
}
(t.size.0 as i32, t.size.1 as i32, t.format.unwrap())
};
for &offset in slot_offsets {
let slot = self.tiles[i].slots.len();
let buffer = pool.create_buffer(offset as i32, w, h, w * 4, format, qh, (i, slot));
self.tiles[i].slots.push(Slot {
buffer,
busy: false,
});
}
self.request_capture(i, qh);
}
pool.destroy(); Ok(())
}
fn request_capture(&mut self, i: usize, qh: &QueueHandle<Self>) {
let t = &mut self.tiles[i];
if t.frame.is_some() || t.session.is_none() {
return; }
let Some(slot) = t.slots.iter().position(|s| !s.busy) else {
self.stats.starved += 1;
return; };
let (w, h) = (t.size.0 as i32, t.size.1 as i32);
let frame = t
.session
.as_ref()
.expect("checked above")
.create_frame(qh, i);
frame.attach_buffer(&t.slots[slot].buffer);
frame.damage_buffer(0, 0, w, h);
frame.capture();
t.frame = Some(frame);
t.filling = Some(slot);
t.asked = Some(Instant::now());
}
fn frame_ready(&mut self, i: usize) -> bool {
let visible = self.layout.tile(i as i32, self.scroll).is_some();
let t = &mut self.tiles[i];
let Some(slot) = t.filling.take() else {
return false;
};
t.frames += 1;
t.ready = true;
t.settled = true;
t.slots[slot].busy = true; let previous = t.showing.replace(slot);
match t.surface.clone() {
Some(surface) if visible => {
let (w, h) = (t.size.0 as i32, t.size.1 as i32);
surface.attach(Some(&t.slots[slot].buffer), 0, 0);
surface.damage_buffer(0, 0, w, h);
surface.commit();
}
_ => {
if let Some(prev) = previous {
t.slots[prev].busy = false;
}
}
}
t.surface.is_none()
}
pub fn arm_frame_callback(&mut self, qh: &QueueHandle<Self>) {
if self.live == Live::None {
return;
}
if let Some(surface) = self.surface.clone() {
surface.frame(qh, ());
surface.commit();
}
}
pub fn tick(&mut self, qh: &QueueHandle<Self>) {
self.stats.ticks += 1;
if self.live == Live::None {
return;
}
let interval = Duration::from_secs_f64(1.0 / self.fps.max(1) as f64);
let now = Instant::now();
for i in 0..self.tiles.len() {
if self.live == Live::Current && i != self.sel {
continue;
}
if self.tiles[i].target.kind == Kind::Output {
continue;
}
if self.layout.tile(i as i32, self.scroll).is_none() {
continue;
}
let t = &self.tiles[i];
if t.slots.is_empty() || t.frame.is_some() {
continue;
}
if t.asked.is_some_and(|a| now.duration_since(a) < interval) {
continue;
}
self.request_capture(i, qh);
}
}
}
impl Dispatch<ExtImageCopyCaptureSessionV1, usize> for App {
fn event(
app: &mut Self,
_: &ExtImageCopyCaptureSessionV1,
event: ext_image_copy_capture_session_v1::Event,
&i: &usize,
_: &Connection,
_: &QueueHandle<Self>,
) {
let Some(tile) = app.tiles.get_mut(i) else {
return;
};
match event {
ext_image_copy_capture_session_v1::Event::BufferSize { width, height } => {
tile.size = (width, height)
}
ext_image_copy_capture_session_v1::Event::ShmFormat {
format: WEnum::Value(f),
} => tile.formats.push(f),
ext_image_copy_capture_session_v1::Event::Done => tile.session_done = true,
ext_image_copy_capture_session_v1::Event::Stopped => tile.settled = true,
_ => {}
}
}
}
impl Dispatch<ExtImageCopyCaptureFrameV1, usize> for App {
fn event(
app: &mut Self,
_: &ExtImageCopyCaptureFrameV1,
event: ext_image_copy_capture_frame_v1::Event,
&i: &usize,
_: &Connection,
qh: &QueueHandle<Self>,
) {
let Some(tile) = app.tiles.get_mut(i) else {
return;
};
match event {
ext_image_copy_capture_frame_v1::Event::Transform {
transform: WEnum::Value(t),
} => tile.transform = t,
ext_image_copy_capture_frame_v1::Event::Ready => {
if let Some(frame) = tile.frame.take() {
frame.destroy();
}
if app.frame_ready(i) {
app.sync_tiles(qh);
}
}
ext_image_copy_capture_frame_v1::Event::Failed { reason } => {
if tile.frames == 0 {
eprintln!(
"wl-pick: capture failed for {:?} ({reason:?})",
tile.target.title
);
}
tile.settled = true;
if let Some(slot) = tile.filling.take() {
tile.slots[slot].busy = false;
}
if let Some(frame) = tile.frame.take() {
frame.destroy();
}
}
_ => {}
}
}
}
impl Dispatch<WlBuffer, (usize, usize)> for App {
fn event(
app: &mut Self,
_: &WlBuffer,
event: wl_buffer::Event,
&(tile, slot): &(usize, usize),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let wl_buffer::Event::Release = event
&& let Some(t) = app.tiles.get_mut(tile)
{
t.slots[slot].busy = false;
}
}
}
impl Dispatch<wl_callback::WlCallback, ()> for App {
fn event(
app: &mut Self,
_: &wl_callback::WlCallback,
event: wl_callback::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_callback::Event::Done { .. } = event {
app.tick(qh);
app.arm_frame_callback(qh);
}
}
}