Compare commits

..
24 changed files with 1167 additions and 1646 deletions

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
# Contributing
Contributions are warmly welcome. Before you start, please read the entire document carefully.
## AI Policy
Do not use LLMs to generate substantial pieces of code. You need to fully understand every line of code you change. No exceptions.
Commits, issues and pull requests must be made by humans. Co-authoring by AI is not allowed.
Looking to create an issue on the project, such as a feature request or bug report? Focus on writing what you know for sure in simple terms. We'll figure it out from there.
Usage of Gen-AI for visuals and audio is prohibited.
The only exception is LLM-translated strings generated by the scripts/translator tool.
## Basics
Before committing, make sure to format your Rust code by running:
```
cargo fmt
```
You should also check for any compiler warnings too:
```
cargo check
```
or even better:
```
cargo clippy
```
## Large contributions
If you want to submit a relatively big pull requests, it's highly advisable to create a separate issue request for it. That way we can discuss the approach and sort out the details first.
## Adding translations
Look at `scripts/translator/run.sh` for more information.
## Licensing
WayVR is developed and published under [GPL-3 License](https://www.gnu.org/licenses/gpl-3.0.en.html).
Generated
+586 -818
View File
File diff suppressed because it is too large. Load diff
+33 -4
View File
@@ -1,6 +1,32 @@
#!/bin/bash
# Do not run this script if you don't have at least 32 GiB of system memory.
# This is the translation tool intended for machine-translating strings.
#
# ###########################
# # Adding new translations #
# ###########################
#
# If you want to add a new language, follow these steps:
#
# 1. Include the language in `wlx-common/src/locale.rs`:
# - add your entry in `enum Language`
# - add your entry in Language::code() function
# - add your language code in Language::add_codes() function
#
# 2. Create a new template file
# - Add your language code in `scripts/translator/templates/<lang code>.json`
# (you can copy one of nearby files and adapt them accordingly)
#
# 3. Modify this very file
# - Go to the bottom and add your language code entry here too
#
# 4. Run the script (it will hint you which LANG_PATHs are supported)
#
# ############################################
# # Running machine translations (local LLM) #
# ############################################
#
# Do not run this script if you don't have at least ~16 GiB of free system memory.
# Submitting PRs that lack translated strings is okay if you don't meet the
# system requirements to run this script, or if you simply prefer not to; we are
# regularly updating the missing translation strings anyway.
@@ -8,15 +34,18 @@
# Base language: English (en.json)
#
# This tool expects llama.cpp server being present (llama-server) at port 8080.
#
# Currently we're using Gemma 4 for machine translations.
# Model URL:
# https://huggingface.co/unsloth/gemma-4-31B-it-GGUF
# Recommended quantization: Q4_K_M
#
set -e
cd "$(dirname "$0")"
bun install
# model URL:
# https://huggingface.co/unsloth/gemma-4-31B-it-GGUF
# you can specify your own if you want to.
if [ -z "${MODEL}" ]; then
export MODEL="gemma-4-31B-it-Q4_K_M"
fi
+15 -16
View File
@@ -21,7 +21,7 @@ use smithay::{
use wgui::log::LogErr;
use xkbcommon::xkb;
use crate::backend::wayvr::{ExternalProcessRequest, WayVRTask};
use crate::backend::wayvr::WayVRTask;
use super::{
ProcessWayVREnv,
@@ -32,6 +32,7 @@ use super::{
pub struct WayVRClient {
pub client: wayland_server::Client,
pub pid: u32,
pub process: process::ProcessHandle,
}
pub struct WayVRCompositor {
@@ -160,16 +161,17 @@ impl WayVRCompositor {
let process_env = get_wayvr_env_from_pid(credentials.pid)?;
// Find suitable auth key from the process list
for (_, process) in processes {
if let process::Process::Managed(process) = &process
for (handle, process) in processes.iter() {
if let process::Process::Managed(process) = process
&& let Some(auth_key) = &process_env.display_auth
{
// Find process with matching auth key
if process.auth_key.as_str() == auth_key {
// Add client
// Associate client now, instead of trying to match env later
self.add_client(WayVRClient {
client,
pid: credentials.pid as u32,
process: handle,
});
return Ok(());
}
@@ -178,18 +180,15 @@ impl WayVRCompositor {
// This is a new process which we didn't met before.
// Treat external processes exclusively (spawned by the user or external program)
log::warn!(
"External process ID {} connected to this Wayland server",
credentials.pid
);
self.state
.wayvr_tasks
.send(WayVRTask::NewExternalProcess(ExternalProcessRequest {
env: process_env,
client,
pid: credentials.pid as u32,
}));
let pid = credentials.pid as u32;
log::warn!("External process ID {pid} connected to this Wayland server");
let process =
processes.insert(process::Process::External(process::ExternalProcess { pid }));
self.add_client(WayVRClient {
client,
pid,
process,
});
Ok(())
}
+1 -3
View File
@@ -114,8 +114,6 @@ impl ImageImporter {
num_planes: dmabuf.num_planes(),
planes: Default::default(),
mouse: None,
acquire_sync: Default::default(),
lease: None,
};
for (i, handle) in dmabuf.handles().enumerate() {
@@ -131,7 +129,7 @@ impl ImageImporter {
frame.planes[i].stride = stride as _;
}
let image = self.gfx.dmabuf_texture(&frame)?;
let image = self.gfx.dmabuf_texture(frame)?;
log::info!("DMA-buf import: creating image view");
let image_view = self.gfx.create_image_view(image)?;
log::info!("DMA-buf import: image view created");
+16 -35
View File
@@ -118,14 +118,6 @@ pub struct ProcessWayVREnv {
pub display_name: Option<String>, // Externally spawned process by a user script
}
#[derive(Clone)]
pub struct ExternalProcessRequest {
#[allow(dead_code)]
pub env: ProcessWayVREnv,
pub client: wayland_server::Client,
pub pid: u32,
}
#[derive(Clone)]
pub enum WayVRTask {
NewToplevel(ClientId, ToplevelSurface),
@@ -133,7 +125,6 @@ pub enum WayVRTask {
MinimizeRequest(ClientId, ToplevelSurface),
TitleChange(ClientId, ToplevelSurface),
VisibilityChange(window::WindowHandle, bool),
NewExternalProcess(ExternalProcessRequest),
ProcessTerminationRequest(process::ProcessHandle, KillSignal),
CloseWindowRequest(window::WindowHandle),
DndStarted {
@@ -173,10 +164,6 @@ pub enum PointerFocusTarget {
None,
}
pub enum TickTask {
NewExternalProcess(ExternalProcessRequest), // Call WayVRCompositor::add_client after receiving this message
}
const KEY_REPEAT_DELAY: i32 = 500;
const KEY_REPEAT_RATE: i32 = 50;
const WAYVR_SCREEN_RES: [i32; 2] = [2560, 1440];
@@ -325,26 +312,29 @@ impl WvrServerState {
#[allow(clippy::too_many_lines)]
#[allow(clippy::significant_drop_tightening)]
pub fn tick_events(app: &mut AppState) -> anyhow::Result<Vec<TickTask>> {
let mut tasks: Vec<TickTask> = Vec::new();
pub fn tick_events(app: &mut AppState) -> anyhow::Result<()> {
let Some(wvr_server) = app.wvr_server.as_mut() else {
return Ok(tasks);
return Ok(());
};
app.ipc_server.tick(&mut ipc_server::TickParams {
wvr_server,
input_state: &app.input_state,
tasks: &mut tasks,
signals: &app.wayvr_signals,
hid_wrapper: &mut app.hid_provider,
});
// Tick all child processes
// Keep launch records while wl client is attached
let attached_processes: HashSet<_> = wvr_server
.manager
.clients
.iter()
.map(|client| client.process)
.collect();
let mut to_remove: SmallVec<[process::ProcessHandle; 2]> = SmallVec::new();
for (handle, process) in &mut wvr_server.processes {
if !process.is_running() {
if !process.is_running() && !attached_processes.contains(&handle) {
to_remove.push(handle);
}
}
@@ -356,9 +346,6 @@ impl WvrServerState {
while let Some(task) = wvr_server.tasks.read() {
match task {
WayVRTask::NewExternalProcess(req) => {
tasks.push(TickTask::NewExternalProcess(req));
}
WayVRTask::NewToplevel(client_id, toplevel) => {
let toplevel = Rc::new(toplevel);
@@ -368,15 +355,14 @@ impl WvrServerState {
continue;
}
let Some(process_handle) =
process::find_by_pid(&wvr_server.processes, client.pid)
else {
let process_handle = client.process;
if !wvr_server.processes.contains_key(process_handle) {
log::error!(
"WayVR window creation failed: Unexpected process ID {}. It wasn't registered before.",
"WayVR window creation failed: Process for PID {} was removed after the client connected.",
client.pid
);
continue;
};
}
let output_bounds = wvr_server.manager.state.output_logical_size();
@@ -601,7 +587,7 @@ impl WvrServerState {
}
let Some(window_handle) = wvr_server.wm.find_window_handle(&toplevel)
else {
log::warn!("MinimizeRequest: Couldn't find matching window handle");
log::warn!("TitleChange: Couldn't find matching window handle");
continue;
};
if let Some(oid) = wvr_server.window_to_overlay.get(&window_handle) {
@@ -703,7 +689,7 @@ impl WvrServerState {
wvr_server.ticks += 1;
Ok(tasks)
Ok(())
}
pub fn config_changed(&mut self, config: &GeneralConfig) {
@@ -1307,11 +1293,6 @@ impl WvrServerState {
self.manager.state.set_clipboard_text(content);
}
pub fn add_external_process(&mut self, pid: u32) -> process::ProcessHandle {
self.processes
.insert(process::Process::External(process::ExternalProcess { pid }))
}
#[allow(clippy::too_many_arguments)]
pub fn spawn_process(
&mut self,
+13 -9
View File
@@ -87,12 +87,14 @@ impl Process {
impl Drop for WayVRProcess {
fn drop(&mut self) {
log::info!(
"Sending SIGTERM (graceful exit) to process {} ({})",
self.child.id(),
self.exec_path.as_str()
);
self.kill(libc::SIGTERM);
if self.is_running() {
log::info!(
"Sending SIGTERM (graceful exit) to process {} ({})",
self.child.id(),
self.exec_path.as_str()
);
self.kill(libc::SIGTERM);
}
}
}
@@ -127,9 +129,11 @@ impl WayVRProcess {
}
fn kill(&mut self, signal: i32) {
unsafe {
// Gracefully stop process
libc::kill(self.child.id() as i32, signal);
if self.is_running() {
unsafe {
// Gracefully stop process
libc::kill(self.child.id() as i32, signal);
}
}
}
+2 -2
View File
@@ -4,11 +4,11 @@ use wgui::gfx::{DmabufImportInfo, DmabufPlaneLayout, Format, Image, ImageView, W
use wlx_capture::{DrmFormat, DrmFourcc, DrmModifier, frame::DmabufFrame};
pub trait WGfxDmabuf {
fn dmabuf_texture(&self, frame: &DmabufFrame) -> anyhow::Result<Arc<Image>>;
fn dmabuf_texture(&self, frame: DmabufFrame) -> anyhow::Result<Arc<Image>>;
}
impl WGfxDmabuf for WGfx {
fn dmabuf_texture(&self, frame: &DmabufFrame) -> anyhow::Result<Arc<Image>> {
fn dmabuf_texture(&self, frame: DmabufFrame) -> anyhow::Result<Arc<Image>> {
let format = fourcc_to_vk(frame.format.drm_format.code)?;
let modifier = (!matches!(frame.format.drm_format.modifier, DrmModifier::Invalid))
.then(|| u64::from(frame.format.drm_format.modifier));
-17
View File
@@ -8,8 +8,6 @@ use wgui::gfx::{
BufferUsage, BuiltCommandBuffer, Format, ShaderModule, Vert2Buf, Vert2Uv, WGfx, WGfxCreateInfo,
};
use wlx_capture::DrmFormat;
#[cfg(feature = "pipewire")]
use wlx_capture::syncobj::DrmRenderNode;
use crate::shaders::{
frag_color, frag_grid, frag_screen, frag_simple, frag_sky, frag_srgb, frag_wayland, vert_quad,
@@ -22,8 +20,6 @@ pub struct WGfxExtras {
pub drm_formats: Arc<[DrmFormat]>,
pub quad_verts: Vert2Buf,
pub drm_device: Option<(i64, i64)>,
#[cfg(feature = "pipewire")]
pub drm_render_node: Option<DrmRenderNode>,
}
impl WGfxExtras {
@@ -59,32 +55,19 @@ impl WGfxExtras {
];
let quad_verts = gfx.new_buffer(BufferUsage::VERTEX_BUFFER, &vertices)?;
#[cfg(feature = "pipewire")]
let drm_device = gfx.device_info().drm_render_node.map(|(major, minor)| {
log::info!("DRM render device: {major} {minor}");
(i64::from(major), i64::from(minor))
});
#[cfg(feature = "pipewire")]
if drm_device.is_none() {
log::warn!("No DRM device.");
}
#[cfg(feature = "pipewire")]
let drm_render_node = drm_device.and_then(|(maj, min)| {
use wgui::log::LogErr;
use wlx_capture::syncobj::open_render_node;
open_render_node(maj as u32, min as u32)
.log_err("Could not open DRM render node")
.ok()
});
Ok(Self {
shaders,
drm_formats,
quad_verts,
drm_device,
#[cfg(feature = "pipewire")]
drm_render_node,
})
}
}
+2 -23
View File
@@ -1,6 +1,6 @@
use std::sync::Arc;
use crate::backend::wayvr::{self, WayVRTask, WvrServerState};
use crate::backend::wayvr::{WayVRTask, WvrServerState};
use wayvr_ipc::packet_client::{HandsfreeMode, HandsfreeParams};
use wayvr_ipc::packet_server::PacketServer;
use wlx_common::config::HandsfreePointer;
@@ -15,24 +15,6 @@ use crate::{
windowing::{OverlaySelector, manager::OverlayWindowManager},
};
fn process_tick_tasks(
tick_tasks: Vec<backend::wayvr::TickTask>,
server_state: &mut WvrServerState,
) {
for tick_task in tick_tasks {
match tick_task {
backend::wayvr::TickTask::NewExternalProcess(request) => {
log::info!("Registering external process with PID {}", request.pid);
server_state.add_external_process(request.pid);
server_state.manager.add_client(wayvr::client::WayVRClient {
client: request.client,
pid: request.pid,
});
}
}
}
}
pub fn tick_events<O>(
app: &mut AppState,
overlays: &mut OverlayWindowManager<O>,
@@ -135,10 +117,7 @@ where
}
}
let tick_tasks = WvrServerState::tick_events(app)?;
if let Some(wayvr_server) = app.wvr_server.as_mut() {
process_tick_tasks(tick_tasks, wayvr_server);
}
WvrServerState::tick_events(app)?;
Ok(())
}
-2
View File
@@ -89,8 +89,6 @@ fn read_payload(conn: &mut local_socket::Stream, size: u32) -> anyhow::Result<Op
pub struct TickParams<'a> {
pub wvr_server: &'a mut WvrServerState,
#[allow(dead_code)]
pub tasks: &'a mut Vec<wayvr::TickTask>,
pub signals: &'a SyncEventQueue<WayVRSignal>,
pub input_state: &'a InputState,
pub hid_wrapper: &'a mut HidWrapper,
+2 -24
View File
@@ -48,12 +48,10 @@ pub(super) enum CaptureType {
pub struct ScreenBackend {
name: Arc<str>,
// frames mut come before `capture` to maintain drop order
cur_frame: Option<WlxCaptureOut>,
pending_frame: Option<WlxCaptureOut>,
capture_type: CaptureType,
capture: Box<dyn WlxCapture<WlxCaptureIn, WlxCaptureOut>>,
pipeline: Option<ScreenPipeline>,
cur_frame: Option<WlxCaptureOut>,
meta: Option<FrameMeta>,
mouse_transform: Affine2,
interaction_transform: Option<Affine2>,
@@ -79,7 +77,6 @@ impl ScreenBackend {
capture,
pipeline: None,
cur_frame: None,
pending_frame: None,
meta: None,
mouse_transform: Affine2::ZERO,
interaction_transform: None,
@@ -207,26 +204,7 @@ impl OverlayBackend for ScreenBackend {
return Ok(ShouldRender::Unable);
}
// don't submit the new image until the producer signaled
if let Some(mut frame) = self.capture.receive()
&& (self.pending_frame.is_none() || matches!(frame.is_ready(), Ok(true)))
{
self.pending_frame = Some(frame);
}
let pending_ready = match self.pending_frame.as_mut() {
Some(frame) => match frame.is_ready() {
Ok(ready) => ready,
Err(e) => {
log::warn!("{}: failed to check DMA-buf acquire fence: {e}", self.name);
self.pending_frame = None;
false
}
},
None => false,
};
if pending_ready {
let frame = self.pending_frame.take().unwrap();
if let Some(frame) = self.capture.receive() {
let stereo = self.stereo.unwrap_or(StereoMode::None);
let mut meta = frame.get_frame_meta(&app.session.config, stereo);
+2 -19
View File
@@ -558,15 +558,9 @@ pub(super) struct WlxCaptureOut {
pub(super) image: Arc<ImageView>,
pub(super) format: FrameFormat,
pub(super) mouse: Option<MouseMeta>,
pub(super) acquire_sync: wlx_frame::FrameAcquireSync,
pub(super) _lease: Option<wlx_frame::FrameLease>,
}
impl WlxCaptureOut {
pub(super) fn is_ready(&mut self) -> std::io::Result<bool> {
self.acquire_sync.check_ready()
}
pub(super) fn get_frame_meta(&self, config: &GeneralConfig, stereo: StereoMode) -> FrameMeta {
FrameMeta {
clear: WGfxClearMode::DontCare,
@@ -725,23 +719,18 @@ fn upload_image_with_row_length(
pub(super) fn receive_callback(me: &WlxCaptureIn, frame: WlxFrame) -> Option<WlxCaptureOut> {
match frame {
WlxFrame::Dmabuf(mut frame) => {
WlxFrame::Dmabuf(frame) => {
if !frame.is_valid() {
log::error!("{}: Invalid frame", me.name);
return None;
}
log::trace!("{}: New DMA-buf frame", me.name);
let format = frame.format;
let lease = frame.lease.clone();
let acquire_sync = std::mem::take(&mut frame.acquire_sync);
match me.gfx.dmabuf_texture(&frame) {
match me.gfx.dmabuf_texture(frame) {
Ok(image) => Some(WlxCaptureOut {
image: me.gfx.create_image_view(image).ok()?,
format,
mouse: None,
acquire_sync,
_lease: lease,
}),
Err(e) => {
log::error!("{}: Failed to create DMA-buf vkImage: {}", me.name, e);
@@ -777,8 +766,6 @@ pub(super) fn receive_callback(me: &WlxCaptureIn, frame: WlxFrame) -> Option<Wlx
image: me.gfx.create_image_view(image).ok()?,
format: frame.format,
mouse: None,
acquire_sync: wlx_frame::FrameAcquireSync::Ready,
_lease: None,
})
}
WlxFrame::MemPtr(frame) => {
@@ -813,8 +800,6 @@ pub(super) fn receive_callback(me: &WlxCaptureIn, frame: WlxFrame) -> Option<Wlx
image: me.gfx.create_image_view(image).ok()?,
format: frame.format,
mouse: frame.mouse,
acquire_sync: wlx_frame::FrameAcquireSync::Ready,
_lease: None,
})
}
WlxFrame::Implicit(transform) => {
@@ -838,8 +823,6 @@ pub(super) fn receive_callback(me: &WlxCaptureIn, frame: WlxFrame) -> Option<Wlx
image,
format,
mouse: None,
acquire_sync: wlx_frame::FrameAcquireSync::Ready,
_lease: None,
})
}
}
+2 -8
View File
@@ -344,14 +344,8 @@ fn check(mut par: CheckParams, finalize_fn: ScreenCastFinalizeFn) {
par.app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
OverlaySelector::Name(par.name.clone()),
Box::new(move |app, owc| {
let capture = new_wlx_capture!(
app,
PipewireCapture::new(
par.name.clone(),
node_id,
app.gfx_extras.drm_render_node.clone()
)
);
let capture =
new_wlx_capture!(app, PipewireCapture::new(par.name.clone(), node_id));
owc.backend =
finalize_fn(par.name, par.logical_pos, par.logical_size, capture, app);
+1 -2
View File
@@ -16,7 +16,6 @@ idmap = { workspace = true }
libc.workspace = true
log = { workspace = true }
pipewire = { workspace = true, optional = true }
timeline_syncobj = { git = "https://github.com/Schmarni-Dev/timeline_syncobj", rev = "7f67e49ebf466487cf35138fc108618092acb9e8", optional = true }
slotmap.workspace = true
wayland-client = { workspace = true, optional = true }
wayland-protocols = {
@@ -47,7 +46,7 @@ smithay-client-toolkit = { version = "0.20.0", optional = true }
[features]
default = ["pipewire", "wayland"]
egl = []
pipewire = ["dep:dbus", "dep:pipewire", "dep:timeline_syncobj", "wayland"]
pipewire = ["dep:dbus", "dep:pipewire", "wayland"]
wayland = [
"dep:smithay-client-toolkit",
"dep:wayland-client",
+11
View File
@@ -30,6 +30,17 @@ let capture = PipewireCapture::new(
);
```
### Wlr-Dmabuf Setup
```rust
let wl = WlxClient::new();
// select desired screen
let output_id = wl.outputs.keys().first().unwrap();
let mut capture = WlrDmabufCapture::new(wl, *output_id).unwrap();
```
### XSHM Setup
```rust
let monitors = XshmCapture::get_monitors();
+1 -110
View File
@@ -1,10 +1,4 @@
use std::{
os::fd::{AsRawFd, OwnedFd, RawFd},
sync::Arc,
};
#[cfg(feature = "pipewire")]
use timeline_syncobj::timeline_syncobj::TimelineSyncObj;
use std::os::fd::RawFd;
use drm_fourcc::{DrmFormat, DrmFourcc, DrmModifier};
@@ -75,8 +69,6 @@ pub struct DmabufFrame {
pub num_planes: usize,
pub planes: [FramePlane; 4],
pub mouse: Option<MouseMeta>,
pub acquire_sync: FrameAcquireSync,
pub lease: Option<FrameLease>,
}
impl DmabufFrame {
@@ -147,107 +139,6 @@ pub struct MouseMeta {
pub y: f32,
}
#[derive(Clone)]
pub struct FrameLease(Arc<FrameLeaseInner>);
struct FrameLeaseInner(Box<dyn Fn() + Send + Sync>);
impl FrameLease {
pub fn new(f: impl Fn() + Send + Sync + 'static) -> Self {
Self(Arc::new(FrameLeaseInner(Box::new(f))))
}
}
impl Drop for FrameLeaseInner {
fn drop(&mut self) {
(self.0)();
}
}
#[derive(Clone, Default)]
pub enum FrameAcquireSync {
#[default]
/// Frame is ready, no sync needed
Ready,
/// Implicit sync using polled FDs
Implicit { fds: Arc<[OwnedFd]> },
#[cfg(feature = "pipewire")]
/// Explicit sync using DRM SyncObj
Explicit {
timeline: Arc<TimelineSyncObj>,
point: u64,
},
}
impl FrameAcquireSync {
pub fn check_ready(&mut self) -> std::io::Result<bool> {
match self {
FrameAcquireSync::Ready => Ok(true),
FrameAcquireSync::Implicit { fds } => {
if fds.is_empty() {
return Err(std::io::Error::other("implicit DMA-buf without planes"));
}
for plane_fd in fds.iter() {
let fd = plane_fd.as_raw_fd();
let mut pfd = libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
};
let result = unsafe { libc::poll(&mut pfd, 1, 0) };
if result < 0 {
let error = std::io::Error::last_os_error();
if error.kind() == std::io::ErrorKind::Interrupted {
return Ok(false);
}
return Err(error);
}
if result == 0 {
return Ok(false);
}
if pfd.revents & (libc::POLLNVAL | libc::POLLHUP) != 0 {
return Err(std::io::Error::other(format!(
"DMA-buf plane fd {fd} poll returned with {:#x}",
pfd.revents
)));
}
if pfd.revents & libc::POLLERR != 0 {
static WARNED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
if !WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
log::error!(
"Could not poll implicit acquire fence. Falling back to unsynchronized behavior. Visual artifacts may happen."
);
}
continue;
}
if pfd.revents & libc::POLLIN == 0 {
return Ok(false);
}
}
*self = FrameAcquireSync::Ready;
Ok(true)
}
#[cfg(feature = "pipewire")]
FrameAcquireSync::Explicit { timeline, point } => {
if *point == 0 {
return Ok(true);
}
if timeline
.query_signaled_point()
.map(|signaled| signaled >= *point)
.map_err(|e| std::io::Error::from_raw_os_error(e.raw_os_error()))?
{
*self = FrameAcquireSync::Ready;
Ok(true)
} else {
Ok(false)
}
}
}
}
}
pub trait DmaExporter {
fn next_frame(
&mut self,
+3 -3
View File
@@ -6,12 +6,12 @@ use frame::WlxFrame;
pub mod frame;
#[cfg(feature = "pipewire")]
pub mod syncobj;
#[cfg(feature = "wayland")]
pub mod wayland;
#[cfg(feature = "wlr")]
pub mod wlr_dmabuf;
#[cfg(feature = "wlr")]
pub mod wlr_screencopy;
+155 -338
View File
@@ -1,9 +1,5 @@
use std::any::Any;
use std::cell::RefCell;
use std::collections::HashMap;
use std::os::fd::{FromRawFd, OwnedFd, RawFd};
use std::ptr::NonNull;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::mpsc;
use std::thread::JoinHandle;
@@ -14,9 +10,8 @@ use drm_fourcc::DrmModifier;
use pipewire as pw;
use pipewire::context::ContextRc;
use pipewire::main_loop::MainLoopRc;
use pipewire::spa::buffer::meta::{
MetaCursor, MetaHeader, MetaHeaderFlags, MetaVideoTransform, MetaVideoTransformValue,
};
use pipewire::spa::buffer::meta::MetaVideoTransformValue;
use pipewire::spa::buffer::meta::{MetaCursor, MetaHeader, MetaHeaderFlags, MetaVideoTransform};
use pipewire::stream::StreamRc;
use pw::spa;
@@ -26,7 +21,7 @@ use pw::stream::{Stream, StreamFlags};
use spa::buffer::DataType;
use spa::param::ParamType;
use spa::param::video::VideoFormat;
use spa::param::video::{VideoFlags, VideoInfoRaw};
use spa::param::video::VideoInfoRaw;
use spa::pod::ChoiceValue;
use spa::pod::Pod;
use spa::pod::serialize::GenError;
@@ -34,24 +29,13 @@ use spa::pod::{Object, Property, PropertyFlags, Value};
use spa::utils::Choice;
use spa::utils::ChoiceEnum;
use spa::utils::ChoiceFlags;
use timeline_syncobj::render_node::DrmRenderNode;
use wayland_client::backend::smallvec::SmallVec;
use super::raw_buffer::DequeuedBuffer;
use crate::WlxCapture;
use crate::frame::FrameFormat;
use crate::frame::MouseMeta;
use crate::frame::Transform;
use crate::frame::WlxFrame;
use crate::frame::{
DmabufFrame, FrameAcquireSync, FrameLease, FramePlane, MemFdFrame, MemPtrFrame,
};
use crate::pipewire::raw_buffer::{HeldBuffers, MAX_PW_BUFFERS};
use crate::syncobj::import_timeline;
const SPA_DATA_SYNCOBJ: u32 = 5;
const SPA_META_SYNC_TIMELINE: u32 = 9;
const SPA_PARAM_BUFFERS_META_TYPE: u32 = 7;
use crate::frame::{DmabufFrame, FramePlane, MemFdFrame, MemPtrFrame};
#[derive(Default)]
struct StreamData {
@@ -64,7 +48,6 @@ pub enum PwChangeRequest {
Pause,
Resume,
Stop,
ReleaseBuffer(usize),
}
struct CaptureData<R>
@@ -83,20 +66,18 @@ where
data: Option<CaptureData<R>>,
node_id: u32,
handle: Option<JoinHandle<Result<(), Error>>>,
drm_render_node: Option<DrmRenderNode>,
}
impl<R> PipewireCapture<R>
where
R: Any + Send,
{
pub fn new(name: Arc<str>, node_id: u32, drm_render_node: Option<DrmRenderNode>) -> Self {
pub fn new(name: Arc<str>, node_id: u32) -> Self {
PipewireCapture {
name,
data: None,
node_id,
handle: None,
drm_render_node,
}
}
}
@@ -106,8 +87,7 @@ where
R: Any + Send,
{
fn drop(&mut self) {
if let Some(data) = self.data.take() {
drop(data.rx_frame);
if let Some(data) = &self.data {
let _ = data.tx_ctrl.send(PwChangeRequest::Stop);
}
if let Some(handle) = self.handle.take() {
@@ -127,15 +107,10 @@ where
user_data: U,
receive_callback: fn(&U, WlxFrame) -> Option<R>,
) {
let (tx_frame, rx_frame) = mpsc::sync_channel(4);
let (tx_frame, rx_frame) = mpsc::sync_channel(2);
let (tx_ctrl, rx_ctrl) = pw::channel::channel();
self.data = Some(CaptureData {
tx_ctrl: tx_ctrl.clone(),
rx_frame,
});
let drm_render_node = self.drm_render_node.clone();
self.data = Some(CaptureData { tx_ctrl, rx_frame });
self.handle = Some(std::thread::spawn({
let name = self.name.clone();
@@ -148,11 +123,9 @@ where
node_id,
formats,
tx_frame,
tx_ctrl,
rx_ctrl,
user_data,
receive_callback,
drm_render_node,
)
}
}));
@@ -202,12 +175,10 @@ fn main_loop<U, R>(
name: Arc<str>,
node_id: u32,
dmabuf_formats: Vec<DrmFormat>,
tx_frame: mpsc::SyncSender<R>,
tx_ctrl: pw::channel::Sender<PwChangeRequest>,
rx_ctrl: pw::channel::Receiver<PwChangeRequest>,
sender: mpsc::SyncSender<R>,
receiver: pw::channel::Receiver<PwChangeRequest>,
user_data: U,
receive_callback: fn(&U, WlxFrame) -> Option<R>,
drm_render_node: Option<DrmRenderNode>,
) -> Result<(), Error>
where
U: Any,
@@ -218,8 +189,6 @@ where
let context = ContextRc::new(&main_loop, None)?;
let core = context.connect_rc(None)?;
let held_buffers = Rc::new(RefCell::new(HeldBuffers::default()));
let stream = StreamRc::new(
core,
&name,
@@ -248,7 +217,6 @@ where
})
.param_changed({
let name = name.clone();
let support_explicit_sync = drm_render_node.is_some();
move |stream, format, id, param| {
let Some(param) = param else {
return;
@@ -266,278 +234,186 @@ where
format.drm_format.code = spa_to_fourcc(info.format());
format.drm_format.modifier = DrmModifier::from(info.modifier());
let dmabuf = info.flags().contains(VideoFlags::MODIFIER);
let kind = if dmabuf { "DMA-buf" } else { "SHM" };
let kind = if info.modifier() != 0 {
"DMA-buf"
} else {
"SHM"
};
log::info!("{}: got {} video format:", name, kind);
log::info!(" format: {} ({:?})", info.format().as_raw(), info.format());
log::info!(" size: {}x{}", info.size().width, info.size().height);
log::info!(" modifier: {}", info.modifier());
let Ok(params_bytes) = obj_to_bytes(get_buffer_params()) else {
log::warn!("{}: failed to serialize buffer params", name);
return;
};
let Some(params_pod) = Pod::from_bytes(&params_bytes) else {
log::warn!("{}: failed to deserialize buffer params", name);
return;
};
let mut param_bytes = Vec::new();
if dmabuf && support_explicit_sync {
param_bytes.push(obj_to_bytes(get_sync_buffer_params()).unwrap());
// order defines preference, we prefer explicit sync
param_bytes.push(
obj_to_bytes(get_meta_object(
SPA_META_SYNC_TIMELINE,
std::mem::size_of::<MetaSyncTimeline>(),
))
.unwrap(),
);
}
param_bytes.push(obj_to_bytes(get_buffer_params()).unwrap());
param_bytes.push(
obj_to_bytes(get_meta_object(
spa::sys::SPA_META_Header,
std::mem::size_of::<spa::sys::spa_meta_header>(),
))
.unwrap(),
);
param_bytes.push(
obj_to_bytes(get_meta_object(
spa::sys::SPA_META_VideoTransform,
std::mem::size_of::<spa::sys::spa_meta_videotransform>(),
))
.unwrap(),
);
let header_bytes = obj_to_bytes(get_meta_object(
spa::sys::SPA_META_Header,
std::mem::size_of::<spa::sys::spa_meta_header>(),
))
.unwrap(); // want panic
let header_pod = Pod::from_bytes(&header_bytes).unwrap(); // want panic
let mut pods: Vec<&Pod> = param_bytes
.iter()
.map(|bytes| Pod::from_bytes(bytes).unwrap())
.collect();
let xform_bytes = obj_to_bytes(get_meta_object(
spa::sys::SPA_META_VideoTransform,
std::mem::size_of::<spa::sys::spa_meta_videotransform>(),
))
.unwrap(); // want panic
let xform_pod = Pod::from_bytes(&xform_bytes).unwrap(); // want panic
let mut pods = [params_pod, header_pod, xform_pod];
if let Err(e) = stream.update_params(&mut pods) {
log::error!("{}: failed to update params: {}", name, e);
}
}
})
.remove_buffer({
let held_buffers = held_buffers.clone();
let name = name.clone();
move |_, _, ptr| {
if let Some(buffer) = NonNull::new(ptr)
&& held_buffers.borrow_mut().on_removed(buffer)
{
log::warn!("{name}: PipeWire removed a still-leased buffer");
}
}
})
.process({
let name = name.clone();
let sync_device = drm_render_node.clone();
let held_buffers = held_buffers.clone();
let tx_ctrl = tx_ctrl.clone();
let u = user_data;
move |stream, format| {
let mut new_buffer = None;
while let Some(buffer) = DequeuedBuffer::dequeue(stream) {
new_buffer = Some(buffer);
}
let Some(buffer) = new_buffer else { return };
if let Some(header) = buffer.find_meta::<MetaHeader>()
&& header.flags().contains(MetaHeaderFlags::CORRUPTED)
{
log::warn!("{name}: PipeWire buffer is corrupt");
return;
let mut maybe_buffer = None;
// discard all but the newest frame
while let Some(buffer) = stream.dequeue_buffer() {
maybe_buffer = Some(buffer);
}
if let Some(transform) = buffer.find_meta::<MetaVideoTransform>() {
format.transform = match transform.transform() {
MetaVideoTransformValue::NONE => Transform::Normal,
MetaVideoTransformValue::ROTATED90 => Transform::Rotated90,
MetaVideoTransformValue::ROTATED180 => Transform::Rotated180,
MetaVideoTransformValue::ROTATED270 => Transform::Rotated270,
MetaVideoTransformValue::FLIPPED => Transform::Flipped,
MetaVideoTransformValue::FLIPPED90 => Transform::Flipped90,
MetaVideoTransformValue::FLIPPED180 => Transform::Flipped180,
MetaVideoTransformValue::FLIPPED270 => Transform::Flipped270,
_ => Transform::Undefined,
};
}
if let Some(mut buffer) = maybe_buffer {
if let Some(header) = buffer.find_meta::<MetaHeader>()
&& header.flags().contains(MetaHeaderFlags::CORRUPTED)
{
log::warn!("{}: PipeWire buffer is corrupt.", name);
return;
}
let mouse_meta = buffer
.find_meta::<MetaCursor>()
.filter(|c| c.is_valid())
.map(|c| MouseMeta {
x: c.position().x as f32 / format.width as f32,
y: c.position().y as f32 / format.height as f32,
});
let acquire_point = buffer
.find_meta::<MetaSyncTimeline>()
.map(|meta| meta.acquire_point);
let datas = buffer.datas_mut();
if datas.is_empty() {
log::debug!("{name}: empty PipeWire frame");
return;
}
let is_explicit = datas.len() >= 3
&& datas[datas.len() - 2].type_().as_raw() == SPA_DATA_SYNCOBJ
&& datas[datas.len() - 1].type_().as_raw() == SPA_DATA_SYNCOBJ;
if is_explicit != acquire_point.is_some() {
log::warn!("{name}: inconsistent SyncTimeline metadata / SyncObj FD layout");
return;
}
let acquire_fd = is_explicit.then(|| datas[datas.len() - 2].fd());
let datas = if is_explicit {
&datas[..datas.len() - 2]
} else {
datas
};
if datas.is_empty()
|| datas.len() > 4
|| datas
.iter()
.any(|d| d.type_().as_raw() == SPA_DATA_SYNCOBJ)
{
log::warn!("{name}: invalid PipeWire plane layout");
return;
}
let data_type = datas[0].type_();
if datas.iter().any(|d| d.type_() != data_type) {
log::warn!("{name}: mixed PipeWire data types");
return;
}
match datas[0].type_() {
DataType::DmaBuf => {
let mut planes: SmallVec<[FramePlane; 4]> = datas
.iter()
.map(|p| FramePlane {
fd: Some(p.as_raw().fd as _),
offset: p.chunk().offset(),
stride: p.chunk().stride(),
})
.collect();
let acquire_sync = if is_explicit {
let Some(device) = sync_device.as_ref() else {
log::error!("{name}: explicit sync without DRM timeline support");
return;
};
let acquire_point = acquire_point.unwrap();
let fd = acquire_fd.unwrap();
let acquire = match import_timeline(device, fd as RawFd) {
Ok(obj) => obj,
Err(e) => {
log::error!("{name}: acquire SyncObj import failed: {e}");
return;
}
};
FrameAcquireSync::Explicit {
timeline: acquire,
point: acquire_point,
}
} else {
let mut fds = Vec::with_capacity(planes.len());
for (index, plane) in planes.iter_mut().enumerate() {
let fd = plane.fd.unwrap(); // always Some
// duplicate the fd so that we can poll it for acquire sync
let duplicate = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 0) };
if duplicate < 0 {
log::error!(
"{name}: failed to duplicate DMA-buf plane {index} FD {fd}: {}",
std::io::Error::last_os_error()
);
return;
}
fds.push(unsafe { OwnedFd::from_raw_fd(duplicate) });
plane.fd = Some(duplicate);
}
FrameAcquireSync::Implicit { fds: fds.into() }
if let Some(transform) = buffer.find_meta::<MetaVideoTransform>() {
format.transform = match transform.transform() {
MetaVideoTransformValue::NONE => Transform::Normal,
MetaVideoTransformValue::ROTATED90 => Transform::Rotated90,
MetaVideoTransformValue::ROTATED180 => Transform::Rotated180,
MetaVideoTransformValue::ROTATED270 => Transform::Rotated270,
MetaVideoTransformValue::FLIPPED => Transform::Flipped,
MetaVideoTransformValue::FLIPPED90 => Transform::Flipped90,
MetaVideoTransformValue::FLIPPED180 => Transform::Flipped180,
MetaVideoTransformValue::FLIPPED270 => Transform::Flipped270,
_ => Transform::Undefined,
};
log::debug!("{}: Transform: {:?}", name, format.transform);
}
let lease_id = held_buffers.borrow_mut().insert(buffer);
let tx = tx_ctrl.clone();
let lease = FrameLease::new(move || {
let _ = tx.send(PwChangeRequest::ReleaseBuffer(lease_id));
let mouse_meta = buffer
.find_meta::<MetaCursor>()
.filter(|c| c.is_valid())
.map(|c| MouseMeta {
x: c.position().x as f32 / format.width as f32,
y: c.position().y as f32 / format.height as f32,
});
let mut dmabuf = DmabufFrame {
format: *format,
num_planes: planes.len(),
planes: Default::default(),
mouse: mouse_meta,
acquire_sync,
lease: Some(lease),
};
let datas = buffer.datas_mut();
if datas.is_empty() {
log::debug!("{}: no data", name);
return;
}
dmabuf.planes[..planes.len()].copy_from_slice(&planes[..planes.len()]);
let planes: Vec<FramePlane> = datas
.iter()
.map(|p| FramePlane {
fd: Some(p.as_raw().fd as _),
offset: p.chunk().offset(),
stride: p.chunk().stride(),
})
.collect();
if let Some(result) = receive_callback(&u, WlxFrame::Dmabuf(dmabuf)) {
match tx_frame.try_send(result) {
Ok(()) | Err(mpsc::TrySendError::Full(_)) => (),
Err(mpsc::TrySendError::Disconnected(_)) => {
log::warn!("{name}: frame receiver disconnected");
let _ = stream.disconnect();
match datas[0].type_() {
DataType::DmaBuf => {
let mut dmabuf = DmabufFrame {
format: *format,
num_planes: planes.len(),
mouse: mouse_meta,
planes: Default::default(),
};
dmabuf.planes[..planes.len()].copy_from_slice(&planes[..planes.len()]);
let frame = WlxFrame::Dmabuf(dmabuf);
if let Some(r) = receive_callback(&u, frame) {
match sender.try_send(r) {
Ok(_) => (),
Err(mpsc::TrySendError::Full(_)) => (),
Err(mpsc::TrySendError::Disconnected(_)) => {
log::warn!("{}: disconnected, stopping stream", name);
let _ = stream.disconnect();
}
}
}
}
}
DataType::MemFd => {
let memfd = MemFdFrame {
format: *format,
plane: FramePlane {
fd: Some(datas[0].as_raw().fd as _),
offset: datas[0].chunk().offset(),
stride: datas[0].chunk().stride(),
},
mouse: mouse_meta,
};
DataType::MemFd => {
let memfd = MemFdFrame {
format: *format,
plane: FramePlane {
fd: Some(datas[0].as_raw().fd as _),
offset: datas[0].chunk().offset(),
stride: datas[0].chunk().stride(),
},
mouse: mouse_meta,
};
let frame = WlxFrame::MemFd(memfd);
if let Some(r) = receive_callback(&u, frame) {
match tx_frame.try_send(r) {
Ok(_) => (),
Err(mpsc::TrySendError::Full(_)) => (),
Err(mpsc::TrySendError::Disconnected(_)) => {
log::warn!("{}: disconnected, stopping stream", name);
let _ = stream.disconnect();
let frame = WlxFrame::MemFd(memfd);
if let Some(r) = receive_callback(&u, frame) {
match sender.try_send(r) {
Ok(_) => (),
Err(mpsc::TrySendError::Full(_)) => (),
Err(mpsc::TrySendError::Disconnected(_)) => {
log::warn!("{}: disconnected, stopping stream", name);
let _ = stream.disconnect();
}
}
}
}
}
DataType::MemPtr => {
let raw = datas[0].as_raw();
let chunk = datas[0].chunk();
let offset = chunk.offset() as usize;
let chunk_size = chunk.size() as usize;
let Some(size) = offset.checked_add(chunk_size) else {
log::error!("{name}: PipeWire chunk range overflow");
return;
};
if size > raw.maxsize as usize {
log::error!("{name}: PipeWire chunk extends beyond allocation");
return;
}
let memptr = MemPtrFrame {
format: *format,
ptr: raw.data as _,
size,
offset,
stride: chunk.stride() as _,
mouse: mouse_meta,
};
DataType::MemPtr => {
let raw = datas[0].as_raw();
let chunk = datas[0].chunk();
let offset = chunk.offset() as usize;
let chunk_size = chunk.size() as usize;
let Some(size) = offset.checked_add(chunk_size) else {
log::error!("{name}: PipeWire chunk range overflow");
return;
};
if size > raw.maxsize as usize {
log::error!("{name}: PipeWire chunk extends beyond allocation");
return;
}
let memptr = MemPtrFrame {
format: *format,
ptr: raw.data as _,
size,
offset,
stride: chunk.stride() as _,
mouse: mouse_meta,
};
let frame = WlxFrame::MemPtr(memptr);
if let Some(r) = receive_callback(&u, frame) {
match tx_frame.try_send(r) {
Ok(_) => (),
Err(mpsc::TrySendError::Full(_)) => (),
Err(mpsc::TrySendError::Disconnected(_)) => {
log::warn!("{}: disconnected, stopping stream", name);
let _ = stream.disconnect();
let frame = WlxFrame::MemPtr(memptr);
if let Some(r) = receive_callback(&u, frame) {
match sender.try_send(r) {
Ok(_) => (),
Err(mpsc::TrySendError::Full(_)) => (),
Err(mpsc::TrySendError::Disconnected(_)) => {
log::warn!("{}: disconnected, stopping stream", name);
let _ = stream.disconnect();
}
}
}
}
_ => {
log::error!("Received invalid frame data type ({:?})", datas[0].type_())
}
}
_ => log::error!("Received invalid frame data type ({:?})", datas[0].type_()),
}
// DMA-buf frames transfer ownership to held_buffers,
// requeued when their lease expires
}
})
.register()?;
@@ -572,10 +448,9 @@ where
params.as_mut_slice(),
)?;
let _receiver = rx_ctrl.attach(main_loop.loop_(), {
let _receiver = receiver.attach(main_loop.loop_(), {
let name = name.clone();
let main_loop = main_loop.clone();
let held_buffers = held_buffers.clone();
move |req| {
log::debug!("{name}: request pipewire stream to {req:?}");
@@ -590,9 +465,6 @@ where
.set_active(true)
.inspect_err(|e| log::warn!("Could not {req:?} pipewire stream: {e:?}"));
}
PwChangeRequest::ReleaseBuffer(id) => {
held_buffers.borrow_mut().requeue(&stream, id);
}
PwChangeRequest::Stop => {
main_loop.quit();
}
@@ -619,25 +491,7 @@ fn get_buffer_params() -> Object {
| (1 << DataType::MemPtr.as_raw())
| (1 << DataType::DmaBuf.as_raw());
get_buffer_params_with_data_types(data_types)
}
fn get_buffer_params_with_data_types(data_types: i32) -> Object {
// since we hold onto buffers until rendering is done,
// fewer than 4 buffers may produce stalls
let buffers = Property {
key: spa::sys::SPA_PARAM_BUFFERS_buffers,
flags: PropertyFlags::empty(),
value: Value::Choice(ChoiceValue::Int(Choice(
ChoiceFlags::empty(),
ChoiceEnum::Range {
default: 6,
min: 4,
max: MAX_PW_BUFFERS,
},
))),
};
let data_types = Property {
let property = Property {
key: spa::sys::SPA_PARAM_BUFFERS_dataType,
flags: PropertyFlags::empty(),
value: Value::Int(data_types),
@@ -646,35 +500,10 @@ fn get_buffer_params_with_data_types(data_types: i32) -> Object {
spa::pod::object!(
spa::utils::SpaTypes::ObjectParamBuffers,
spa::param::ParamType::Buffers,
buffers,
data_types,
property,
)
}
fn get_sync_buffer_params() -> Object {
let mut obj = get_buffer_params_with_data_types(1 << DataType::DmaBuf.as_raw());
obj.properties.push(Property {
key: spa::sys::SPA_PARAM_BUFFERS_blocks,
flags: PropertyFlags::empty(),
value: Value::Choice(ChoiceValue::Int(Choice(
ChoiceFlags::empty(),
ChoiceEnum::Range {
default: 3,
// when using sync timeline, we want at least 1 image plane + 2 syncobj fds
min: 3,
// up to 4 image planes
max: 6,
},
))),
});
obj.properties.push(Property {
key: SPA_PARAM_BUFFERS_META_TYPE,
flags: PropertyFlags::MANDATORY,
value: Value::Int(1_i32 << SPA_META_SYNC_TIMELINE),
});
obj
}
fn get_meta_object(key: u32, size: usize) -> Object {
let meta_type_property = Property {
key: spa::sys::SPA_PARAM_META_type,
@@ -812,15 +641,3 @@ fn spa_to_fourcc(spa: VideoFormat) -> DrmFourcc {
_ => panic!("Unsupported format"),
}
}
#[repr(C)]
struct MetaSyncTimeline {
_flags: u32,
_padding: u32,
acquire_point: u64,
_release_point: u64,
}
impl spa::buffer::meta::Metadata for MetaSyncTimeline {
const META_TYPE: u32 = SPA_META_SYNC_TIMELINE;
}
-1
View File
@@ -26,7 +26,6 @@ slotmap::new_key_type! {
pub mod capture;
mod dbus_screencast;
mod raw_buffer;
#[derive(Debug, Clone)]
pub struct PipewireStream {
-123
View File
@@ -1,123 +0,0 @@
use std::{marker::PhantomData, mem, ptr::NonNull};
use pipewire::{
self as pw,
spa::{self, buffer::meta::Metadata},
stream::Stream,
};
pub(super) struct DequeuedBuffer<'a> {
stream: &'a Stream,
ptr: NonNull<pw::sys::pw_buffer>,
_not_send: PhantomData<std::rc::Rc<()>>,
}
impl<'a> DequeuedBuffer<'a> {
pub(super) fn dequeue(stream: &'a Stream) -> Option<Self> {
let ptr = NonNull::new(unsafe { stream.dequeue_raw_buffer() })?;
Some(Self {
stream,
ptr,
_not_send: PhantomData,
})
}
fn spa(&self) -> Option<NonNull<spa::sys::spa_buffer>> {
NonNull::new(unsafe { self.ptr.read().buffer })
}
pub(super) fn datas_mut(&self) -> &'a mut [pw::spa::buffer::Data] {
let Some(buffer) = self.spa() else {
return &mut [];
};
unsafe {
let n_datas = buffer.read().n_datas;
if n_datas > 0
&& let Some(datas) = NonNull::new(buffer.read().datas)
{
let datas = datas.as_ptr() as *mut pw::spa::buffer::Data;
std::slice::from_raw_parts_mut(datas, usize::try_from(n_datas).unwrap())
} else {
&mut []
}
}
}
pub(super) fn find_meta<T>(&self) -> Option<&'a T>
where
T: Metadata,
{
let buffer = self.spa()?;
unsafe {
if buffer.read().n_metas != 0 {
match T::META_TYPE {
pw::spa::sys::SPA_META_VideoDamage => {
let meta_data =
pw::spa::sys::spa_buffer_find_meta(buffer.as_ptr(), T::META_TYPE)
as *const T;
if !meta_data.is_null() {
return Some(&*meta_data);
}
}
_ => {
let meta_data = pw::spa::sys::spa_buffer_find_meta_data(
buffer.as_ptr(),
T::META_TYPE,
std::mem::size_of::<T>(),
) as *const T;
if !meta_data.is_null() {
return Some(&*meta_data);
}
}
}
}
}
None
}
}
pub(super) const MAX_PW_BUFFERS: i32 = 8;
impl Drop for DequeuedBuffer<'_> {
fn drop(&mut self) {
unsafe { self.stream.queue_raw_buffer(self.ptr.as_ptr()) };
}
}
#[derive(Default)]
pub(super) struct HeldBuffers {
buffers: Vec<NonNull<pw::sys::pw_buffer>>,
}
impl HeldBuffers {
pub(super) fn insert<'a>(&mut self, buffer: DequeuedBuffer<'a>) -> usize {
self.buffers.push(buffer.ptr);
let ret_val = buffer.ptr.as_ptr() as usize;
mem::forget(buffer);
ret_val
}
pub(super) fn on_removed(&mut self, buffer: NonNull<pw::sys::pw_buffer>) -> bool {
let before_count = self.buffers.len();
self.buffers.retain(|ptr| ptr.as_ptr() != buffer.as_ptr());
before_count != self.buffers.len()
}
pub(super) fn requeue(&mut self, stream: &pw::stream::Stream, id: usize) {
self.buffers.retain(|ptr| {
if ptr.as_ptr() as usize == id {
unsafe { stream.queue_raw_buffer(ptr.as_ptr()) };
false
} else {
true
}
});
}
}
impl Drop for HeldBuffers {
fn drop(&mut self) {
log::warn!("{} checked-out buffers at shutdown", self.buffers.len());
self.buffers.clear();
}
}
-88
View File
@@ -1,88 +0,0 @@
use std::{
fs, io,
os::{
fd::{AsFd, AsRawFd, BorrowedFd, RawFd},
unix::fs::{FileTypeExt, MetadataExt},
},
sync::Arc,
};
pub use timeline_syncobj::render_node::DrmRenderNode;
use timeline_syncobj::timeline_syncobj::TimelineSyncObj;
const DRM_CAP_SYNCOBJ: u64 = 0x13;
const DRM_CAP_SYNCOBJ_TIMELINE: u64 = 0x14;
#[repr(C)]
struct DrmGetCap {
capability: u64,
value: u64,
}
const DRM_IOCTL_GET_CAP: libc::c_ulong = ((3_u64 << 30)
| ((std::mem::size_of::<DrmGetCap>() as u64) << 16)
| ((b'd' as u64) << 8)
| 0x0c) as libc::c_ulong;
fn get_cap(node: &DrmRenderNode, capability: u64) -> io::Result<u64> {
let mut arg = DrmGetCap {
capability,
value: 0,
};
let ret = unsafe { libc::ioctl(node.as_fd().as_raw_fd(), DRM_IOCTL_GET_CAP, &mut arg) };
if ret < 0 {
Err(io::Error::last_os_error())
} else {
Ok(arg.value)
}
}
fn supports_timeline(node: &DrmRenderNode) -> bool {
[DRM_CAP_SYNCOBJ, DRM_CAP_SYNCOBJ_TIMELINE]
.into_iter()
.all(|cap| get_cap(node, cap).is_ok_and(|value| value != 0))
}
pub fn open_render_node(render_major: u32, render_minor: u32) -> io::Result<DrmRenderNode> {
if !(128..=255).contains(&render_minor) {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
format!("Unsupported DRM render minor: {render_minor}"),
));
}
let path = format!("/dev/dri/renderD{render_minor}");
let metadata = fs::metadata(&path)?;
// verify that device actually matches the provided minor/major
if !metadata.file_type().is_char_device()
|| metadata.rdev() != libc::makedev(render_major, render_minor)
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("{path} does not match DRM device {render_major}:{render_minor}"),
));
}
let node = DrmRenderNode::new(render_minor.into())
.map_err(|e| io::Error::from_raw_os_error(e.raw_os_error()))?;
if !supports_timeline(&node) {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
format!("{path} does not support DRM syncobj timelines"),
));
}
Ok(node)
}
pub(crate) fn import_timeline(node: &DrmRenderNode, fd: RawFd) -> io::Result<Arc<TimelineSyncObj>> {
if fd < 0 {
return Err(io::Error::from_raw_os_error(libc::EBADF));
}
let fd = unsafe { BorrowedFd::borrow_raw(fd) };
TimelineSyncObj::import(node, fd)
.map(Arc::new)
.map_err(|e| io::Error::from_raw_os_error(e.raw_os_error()))
}
+18 -1
View File
@@ -11,7 +11,10 @@ use smithay_client_toolkit::reexports::{
zxdg_output_manager_v1::ZxdgOutputManagerV1,
zxdg_output_v1::{self, ZxdgOutputV1},
},
protocols_wlr::screencopy::v1::client::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1,
protocols_wlr::{
export_dmabuf::v1::client::zwlr_export_dmabuf_manager_v1::ZwlrExportDmabufManagerV1,
screencopy::v1::client::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1,
},
};
use wayland_client::{
@@ -58,6 +61,7 @@ pub struct WlxOutput {
pub struct WlxClient {
pub connection: Arc<Connection>,
pub xdg_output_mgr: ZxdgOutputManagerV1,
pub maybe_wlr_dmabuf_mgr: Option<ZwlrExportDmabufManagerV1>,
pub maybe_wlr_screencopy_mgr: Option<ZwlrScreencopyManagerV1>,
pub maybe_zwp_linux_dmabuf: Option<ZwpLinuxDmabufV1>,
pub wl_seat: WlSeat,
@@ -89,6 +93,7 @@ impl WlxClient {
.bind(&qh, 4..=9, ())
.expect(WlSeat::interface().name),
wl_shm: globals.bind(&qh, 1..=1, ()).expect(WlShm::interface().name),
maybe_wlr_dmabuf_mgr: globals.bind(&qh, 1..=1, ()).ok(),
maybe_wlr_screencopy_mgr: globals.bind(&qh, 3..=3, ()).ok(),
maybe_zwp_linux_dmabuf: globals.bind(&qh, 4..=4, ()).ok(),
outputs: IdMap::new(),
@@ -393,6 +398,18 @@ impl Dispatch<ZxdgOutputManagerV1, ()> for WlxClient {
}
}
impl Dispatch<ZwlrExportDmabufManagerV1, ()> for WlxClient {
fn event(
_state: &mut Self,
_proxy: &ZwlrExportDmabufManagerV1,
_event: <ZwlrExportDmabufManagerV1 as Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
}
}
impl Dispatch<ZwlrScreencopyManagerV1, ()> for WlxClient {
fn event(
_state: &mut Self,
+258
View File
@@ -0,0 +1,258 @@
use std::{
any::Any,
collections::VecDeque,
os::fd::{FromRawFd, IntoRawFd, OwnedFd, RawFd},
sync::mpsc,
thread::JoinHandle,
};
use drm_fourcc::{DrmFormat, DrmFourcc, DrmModifier};
use smithay_client_toolkit::reexports::protocols_wlr::export_dmabuf::v1::client::zwlr_export_dmabuf_frame_v1::{self, ZwlrExportDmabufFrameV1};
use wayland_client::{Connection, QueueHandle, Dispatch, Proxy};
use crate::{
WlxCapture,
frame::{DmabufFrame, FrameFormat, FramePlane, WlxFrame},
wayland::WlxClient,
};
use log::{debug, warn};
struct CaptureData<U, R>
where
U: Any,
R: Any,
{
sender: mpsc::SyncSender<WlxFrame>,
receiver: mpsc::Receiver<WlxFrame>,
user_data: U,
receive_callback: fn(&U, WlxFrame) -> Option<R>,
}
pub struct WlrDmabufCapture<U, R>
where
U: Any + Send,
R: Any + Send,
{
output_id: u32,
wl: Option<Box<WlxClient>>,
handle: Option<JoinHandle<Box<WlxClient>>>,
data: Option<CaptureData<U, R>>,
fds: VecDeque<RawFd>,
}
impl<U, R> WlrDmabufCapture<U, R>
where
U: Any + Send,
R: Any + Send,
{
pub fn new(wl: WlxClient, output_id: u32) -> Self {
Self {
output_id,
wl: Some(Box::new(wl)),
handle: None,
data: None,
fds: VecDeque::new(),
}
}
}
impl<U, R> WlxCapture<U, R> for WlrDmabufCapture<U, R>
where
U: Any + Send,
R: Any + Send,
{
fn init(
&mut self,
_: &[DrmFormat],
user_data: U,
receive_callback: fn(&U, WlxFrame) -> Option<R>,
) {
debug_assert!(self.wl.is_some());
let (sender, receiver) = std::sync::mpsc::sync_channel::<WlxFrame>(2);
self.data = Some(CaptureData {
sender,
receiver,
user_data,
receive_callback,
});
}
fn is_ready(&self) -> bool {
self.data.is_some()
}
fn supports_dmbuf(&self) -> bool {
true
}
fn receive(&mut self) -> Option<R> {
if let Some(data) = self.data.as_ref()
&& let Some(WlxFrame::Dmabuf(last)) = data.receiver.try_iter().last()
{
// this is the only protocol that requires us to manually close the FD
while self.fds.len() > 6 * last.num_planes {
// safe unwrap
let _ = unsafe { OwnedFd::from_raw_fd(self.fds.pop_back().unwrap()) };
}
for p in 0..last.num_planes {
if let Some(fd) = last.planes[p].fd {
self.fds.push_front(fd);
}
}
return (data.receive_callback)(&data.user_data, WlxFrame::Dmabuf(last));
}
None
}
fn pause(&mut self) {}
fn resume(&mut self) {
self.receive(); // clear old frames
}
fn request_new_frame(&mut self) {
if let Some(handle) = self.handle.take() {
if handle.is_finished() {
self.wl = Some(handle.join().unwrap()); // safe to unwrap because we checked is_finished
} else {
self.handle = Some(handle);
return;
}
}
let Some(wl) = self.wl.take() else {
return;
};
self.handle = Some(std::thread::spawn({
let sender = self
.data
.as_ref()
.expect("must call init once before request_new_frame")
.sender
.clone();
let output_id = self.output_id;
move || request_dmabuf_frame(wl, output_id, sender)
}));
}
}
/// Request a new DMA-Buf frame using the wlr-export-dmabuf protocol.
fn request_dmabuf_frame(
client: Box<WlxClient>,
output_id: u32,
sender: mpsc::SyncSender<WlxFrame>,
) -> Box<WlxClient> {
let Some(dmabuf_manager) = client.maybe_wlr_dmabuf_mgr.as_ref() else {
return client;
};
let Some(output) = client.outputs.get(output_id) else {
return client;
};
let transform = output.transform;
let (tx, rx) = mpsc::sync_channel::<zwlr_export_dmabuf_frame_v1::Event>(16);
let name = output.name.clone();
let _ = dmabuf_manager.capture_output(1, &output.wl_output, &client.queue_handle, tx.clone());
let mut client = client;
client.dispatch();
let mut frame = None;
rx.try_iter().for_each(|event| match event {
zwlr_export_dmabuf_frame_v1::Event::Frame {
width,
height,
format,
mod_high,
mod_low,
num_objects,
..
} => {
let mut new_frame = DmabufFrame {
format: FrameFormat {
width,
height,
drm_format: DrmFormat {
code: match DrmFourcc::try_from(format) {
Ok(code) => code,
Err(e) => {
log::error!("Unrecognized fourcc: {e:?}");
return;
}
},
modifier: DrmModifier::Invalid,
},
transform,
},
mouse: None,
num_planes: num_objects as _,
planes: Default::default(),
};
new_frame.format.set_mod(mod_high, mod_low);
frame = Some(new_frame);
}
zwlr_export_dmabuf_frame_v1::Event::Object {
index,
fd,
offset,
stride,
..
} => {
let Some(ref mut frame) = frame else {
return;
};
frame.planes[index as usize] = FramePlane {
fd: Some(fd.into_raw_fd()),
offset,
stride: stride as _,
};
}
zwlr_export_dmabuf_frame_v1::Event::Ready { .. } => {
let Some(frame) = frame.take() else {
return;
};
debug!("DMA-Buf frame captured");
let frame = WlxFrame::Dmabuf(frame);
match sender.try_send(frame) {
Ok(_) => (),
Err(mpsc::TrySendError::Full(_)) => (),
Err(mpsc::TrySendError::Disconnected(_)) => {
log::warn!("{}: disconnected", name);
}
}
}
zwlr_export_dmabuf_frame_v1::Event::Cancel { .. } => {
warn!("DMA-Buf frame capture cancelled");
}
_ => {}
});
client
}
impl Dispatch<ZwlrExportDmabufFrameV1, mpsc::SyncSender<zwlr_export_dmabuf_frame_v1::Event>>
for WlxClient
{
fn event(
_state: &mut Self,
proxy: &ZwlrExportDmabufFrameV1,
event: <ZwlrExportDmabufFrameV1 as Proxy>::Event,
data: &mpsc::SyncSender<zwlr_export_dmabuf_frame_v1::Event>,
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
match event {
zwlr_export_dmabuf_frame_v1::Event::Ready { .. }
| zwlr_export_dmabuf_frame_v1::Event::Cancel { .. } => {
proxy.destroy();
}
_ => {}
}
let _ = data.send(event).or_else(|err| {
warn!("Failed to send DMA-Buf frame event: {}", err);
Ok::<(), mpsc::SendError<zwlr_export_dmabuf_frame_v1::Event>>(())
});
}
}