feat: wayland
This commit is contained in:
@@ -1,209 +1,306 @@
|
||||
mod compositor;
|
||||
mod data_device;
|
||||
mod decoration;
|
||||
mod seat;
|
||||
mod state;
|
||||
mod surface;
|
||||
// mod xdg_activation;
|
||||
mod drm;
|
||||
mod utils;
|
||||
mod xdg_shell;
|
||||
pub mod core;
|
||||
#[cfg(feature = "dmabuf")]
|
||||
pub mod dmabuf;
|
||||
pub mod util;
|
||||
pub mod xdg;
|
||||
|
||||
use self::{state::WaylandState, surface::CORE_SURFACES};
|
||||
use crate::{core::task, wayland::state::ClientState};
|
||||
use color_eyre::eyre::{Result, ensure};
|
||||
use parking_lot::Mutex;
|
||||
use smithay::{
|
||||
backend::{
|
||||
allocator::dmabuf::Dmabuf,
|
||||
egl::EGLContext,
|
||||
renderer::{ImportDma, Renderer, gles::GlesRenderer},
|
||||
use crate::wayland::core::seat::SeatMessage;
|
||||
use crate::{
|
||||
BevyMaterial,
|
||||
core::{
|
||||
error::{Result, ServerError},
|
||||
graphics_info::GraphicsInfo,
|
||||
task,
|
||||
},
|
||||
output::Output,
|
||||
reexports::wayland_server::{Display, DisplayHandle, ListeningSocket},
|
||||
wayland::dmabuf,
|
||||
};
|
||||
use bevy::{asset::Assets, ecs::resource::Resource, image::Image};
|
||||
use cluFlock::ToFlock;
|
||||
use core::{
|
||||
buffer::{BUFFER_REGISTRY, Buffer},
|
||||
callback::Callback,
|
||||
display::Display,
|
||||
surface::WL_SURFACE_REGISTRY,
|
||||
};
|
||||
#[cfg(feature = "dmabuf")]
|
||||
use dmabuf::buffer_params::BufferParams;
|
||||
use mint::Vector2;
|
||||
use std::{
|
||||
ffi::{OsStr, c_void},
|
||||
os::fd::AsFd,
|
||||
fs::{self, OpenOptions},
|
||||
io::{self, ErrorKind},
|
||||
os::unix::fs::OpenOptionsExt,
|
||||
path::PathBuf,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
use stereokit_rust::system::{Backend, BackendGraphics};
|
||||
use tokio::{
|
||||
io::unix::AsyncFd,
|
||||
sync::{
|
||||
Notify,
|
||||
mpsc::{self, UnboundedReceiver},
|
||||
use tokio::{net::UnixStream, sync::mpsc, task::AbortHandle};
|
||||
use tokio_stream::StreamExt;
|
||||
use tracing::{debug_span, instrument};
|
||||
#[cfg(feature = "dmabuf")]
|
||||
use waynest::server::protocol::stable::linux_dmabuf_v1::zwp_linux_buffer_params_v1::ZwpLinuxBufferParamsV1;
|
||||
use waynest::{
|
||||
server::{
|
||||
self,
|
||||
protocol::{
|
||||
core::wayland::{wl_buffer::WlBuffer, wl_callback::WlCallback, wl_display::WlDisplay},
|
||||
stable::xdg_shell::xdg_toplevel::XdgToplevel,
|
||||
},
|
||||
},
|
||||
task::AbortHandle,
|
||||
wire::{DecodeError, ObjectId},
|
||||
};
|
||||
use tracing::{debug_span, info, instrument};
|
||||
use xdg::toplevel::Toplevel;
|
||||
|
||||
pub static WAYLAND_DISPLAY: OnceLock<String> = OnceLock::new();
|
||||
pub static WAYLAND_DISPLAY: OnceLock<PathBuf> = OnceLock::new();
|
||||
|
||||
struct EGLRawHandles {
|
||||
display: *const c_void,
|
||||
config: *const c_void,
|
||||
context: *const c_void,
|
||||
impl From<waynest::server::Error> for ServerError {
|
||||
fn from(err: waynest::server::Error) -> Self {
|
||||
ServerError::WaylandError(err)
|
||||
}
|
||||
}
|
||||
fn get_sk_egl() -> Result<EGLRawHandles> {
|
||||
ensure!(
|
||||
Backend::graphics() == BackendGraphics::OpenGLESEGL,
|
||||
"StereoKit is not running using EGL!"
|
||||
);
|
||||
|
||||
Ok(unsafe {
|
||||
EGLRawHandles {
|
||||
display: stereokit_rust::system::backend_opengl_egl_get_display() as *const c_void,
|
||||
config: stereokit_rust::system::backend_opengl_egl_get_config() as *const c_void,
|
||||
context: stereokit_rust::system::backend_opengl_egl_get_context() as *const c_void,
|
||||
pub fn get_free_wayland_socket_path() -> Option<PathBuf> {
|
||||
// Use XDG runtime directory for secure, user-specific sockets
|
||||
let base_dirs = directories::BaseDirs::new()?;
|
||||
let runtime_dir = base_dirs.runtime_dir()?;
|
||||
|
||||
// Iterate through conventional display numbers (matches X11 behavior)
|
||||
for display in 0..=32 {
|
||||
let socket_path = runtime_dir.join(format!("wayland-{display}"));
|
||||
let socket_lock_path = runtime_dir.join(format!("wayland-{display}.lock"));
|
||||
|
||||
// Open lock file without truncation to preserve existing locks
|
||||
let mut _lock = OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(false) // Prevent destroying other processes' locks
|
||||
.read(true)
|
||||
.write(true)
|
||||
.mode(0o660) // Match Wayland-compositor permissions
|
||||
.open(&socket_lock_path)
|
||||
.ok()?;
|
||||
|
||||
// Atomic mutual exclusion: fail if another process holds the lock
|
||||
if _lock.try_exclusive_lock().is_err() {
|
||||
continue; // Lock held by active compositor
|
||||
}
|
||||
})
|
||||
|
||||
// Check for zombie sockets (file exists but nothing listening)
|
||||
if socket_path.exists() {
|
||||
match std::os::unix::net::UnixStream::connect(&socket_path) {
|
||||
Ok(_) => continue, // Active compositor found - skip
|
||||
Err(e) if e.kind() == ErrorKind::ConnectionRefused => {
|
||||
// Stale socket - safe to remove since we hold the lock
|
||||
let _ = fs::remove_file(&socket_path);
|
||||
}
|
||||
Err(_) => continue, // Transient error - conservative skip
|
||||
}
|
||||
}
|
||||
|
||||
// Found viable candidate: lock held, socket cleared/available
|
||||
return Some(socket_path);
|
||||
}
|
||||
|
||||
None // Exhausted all conventional display numbers
|
||||
}
|
||||
|
||||
pub enum Message {
|
||||
Frame(Arc<Callback>),
|
||||
ReleaseBuffer(Arc<Buffer>),
|
||||
#[cfg(feature = "dmabuf")]
|
||||
DmabufImportSuccess(Arc<BufferParams>, Arc<Buffer>),
|
||||
#[cfg(feature = "dmabuf")]
|
||||
DmabufImportFailure(Arc<BufferParams>),
|
||||
CloseToplevel(Arc<Toplevel>),
|
||||
ResizeToplevel {
|
||||
toplevel: Arc<Toplevel>,
|
||||
size: Option<Vector2<u32>>,
|
||||
},
|
||||
SetToplevelVisualActive {
|
||||
toplevel: Arc<Toplevel>,
|
||||
active: bool,
|
||||
},
|
||||
Seat(SeatMessage),
|
||||
}
|
||||
|
||||
pub type MessageSink = mpsc::UnboundedSender<Message>;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct WaylandClient {
|
||||
abort_handle: AbortHandle,
|
||||
}
|
||||
impl WaylandClient {
|
||||
pub fn from_stream(socket: UnixStream) -> Result<Self> {
|
||||
let pid = socket.peer_cred().ok().and_then(|c| c.pid());
|
||||
let mut client = server::Client::new(socket)?;
|
||||
let (message_sink, message_source) = mpsc::unbounded_channel();
|
||||
|
||||
client.insert(ObjectId::DISPLAY, Display::new(message_sink, pid));
|
||||
let abort_handle = task::new(
|
||||
|| "wayland client",
|
||||
Self::handle_client_messages(client, message_source),
|
||||
)?
|
||||
.abort_handle();
|
||||
|
||||
Ok(WaylandClient { abort_handle })
|
||||
}
|
||||
async fn handle_client_messages(
|
||||
mut client: server::Client,
|
||||
mut render_message_rx: mpsc::UnboundedReceiver<Message>,
|
||||
) -> Result<()> {
|
||||
loop {
|
||||
tokio::select! {
|
||||
// send all queued up messages
|
||||
msg = render_message_rx.recv() => {
|
||||
if let Some(msg) = msg {
|
||||
Self::handle_render_message(&mut client, msg).await?;
|
||||
}
|
||||
}
|
||||
// handle the next message
|
||||
msg = client.next_message() => {
|
||||
match msg {
|
||||
Ok(Some(mut msg)) => {
|
||||
if let Err(e) = client.handle_message(&mut msg).await {
|
||||
tracing::error!("Wayland: Error handling message: {:?}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// wayland clients really aren't nice when disconnecting properly, are they? :p
|
||||
if let server::Error::Decode(DecodeError::IoError(e)) = &e {
|
||||
if e.kind() == io::ErrorKind::ConnectionReset {
|
||||
if let Some(pid) = client.get::<Display>(ObjectId::DISPLAY).and_then(|d| d.pid) {
|
||||
tracing::info!("Wayland: Client with pid: {pid} disconnected from server");
|
||||
} else {
|
||||
tracing::info!("Wayland: Unknown client disconnected from server");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
tracing::error!("Wayland: Error reading message: {:?}", e);
|
||||
break;
|
||||
}
|
||||
Ok(None) => {
|
||||
if let Some(pid) = client.get::<Display>(ObjectId::DISPLAY).and_then(|d| d.pid) {
|
||||
tracing::info!("Wayland: Client with pid: {pid} disconnected from server");
|
||||
} else {
|
||||
tracing::info!("Wayland: Unknown client disconnected from server");
|
||||
}
|
||||
// Message stream ended
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_render_message(
|
||||
client: &mut server::Client,
|
||||
message: Message,
|
||||
) -> Result<(), waynest::server::Error> {
|
||||
match message {
|
||||
Message::Frame(callback) => {
|
||||
let serial = client.next_event_serial();
|
||||
callback.done(client, callback.0, serial).await?;
|
||||
client
|
||||
.get::<Display>(ObjectId::DISPLAY)
|
||||
.unwrap()
|
||||
.delete_id(client, ObjectId::DISPLAY, callback.0.as_raw())
|
||||
.await?;
|
||||
client.remove(callback.0);
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(feature = "dmabuf")]
|
||||
Message::DmabufImportSuccess(params, buffer) => {
|
||||
params.created(client, params.id, buffer.id).await
|
||||
}
|
||||
#[cfg(feature = "dmabuf")]
|
||||
Message::DmabufImportFailure(params) => {
|
||||
client.remove(params.id);
|
||||
params.failed(client, params.id).await
|
||||
}
|
||||
Message::ReleaseBuffer(buffer) => buffer.release(client, buffer.id).await,
|
||||
Message::CloseToplevel(toplevel) => toplevel.close(client, toplevel.id).await,
|
||||
Message::ResizeToplevel { toplevel, size } => {
|
||||
toplevel.set_size(size);
|
||||
toplevel.reconfigure(client).await
|
||||
}
|
||||
Message::SetToplevelVisualActive { toplevel, active } => {
|
||||
toplevel.set_activated(active);
|
||||
toplevel.reconfigure(client).await
|
||||
}
|
||||
Message::Seat(seat_message) => {
|
||||
if let Some(seat) = client.get::<Display>(ObjectId::DISPLAY).unwrap().seat.get() {
|
||||
seat.handle_message(client, seat_message).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Drop for WaylandClient {
|
||||
fn drop(&mut self) {
|
||||
self.abort_handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Resource)]
|
||||
pub struct Wayland {
|
||||
flush_notify: Arc<Notify>,
|
||||
client_listener: AbortHandle,
|
||||
client_dispatcher: AbortHandle,
|
||||
renderer: GlesRenderer,
|
||||
output: Output,
|
||||
dmabuf_rx: UnboundedReceiver<(Dmabuf, Option<dmabuf::ImportNotifier>)>,
|
||||
abort_handle: AbortHandle,
|
||||
}
|
||||
impl Wayland {
|
||||
pub fn new() -> Result<Self> {
|
||||
let egl_raw_handles = get_sk_egl()?;
|
||||
let renderer = unsafe {
|
||||
GlesRenderer::new(EGLContext::from_raw(
|
||||
egl_raw_handles.display,
|
||||
egl_raw_handles.config,
|
||||
egl_raw_handles.context,
|
||||
)?)?
|
||||
pub fn new(socket_path: Option<PathBuf>) -> Result<Self> {
|
||||
let socket_path = if let Some(path) = socket_path {
|
||||
path
|
||||
} else {
|
||||
get_free_wayland_socket_path().ok_or(ServerError::WaylandError(
|
||||
waynest::server::Error::IoError(std::io::ErrorKind::AddrNotAvailable.into()),
|
||||
))?
|
||||
};
|
||||
|
||||
let display: Display<WaylandState> = Display::new()?;
|
||||
let display_handle = display.handle();
|
||||
let _ = WAYLAND_DISPLAY.set(socket_path.clone());
|
||||
|
||||
let (dmabuf_tx, dmabuf_rx) = mpsc::unbounded_channel();
|
||||
let listener =
|
||||
server::Listener::new_with_path(&socket_path).map_err(ServerError::WaylandError)?;
|
||||
|
||||
let wayland_state = WaylandState::new(display_handle.clone(), &renderer, dmabuf_tx);
|
||||
let output = wayland_state.lock().output.clone();
|
||||
let abort_handle =
|
||||
task::new(|| "wayland loop", Self::handle_wayland_loop(listener))?.abort_handle();
|
||||
|
||||
let socket = ListeningSocket::bind_auto("wayland", 0..33)?;
|
||||
let socket_name = socket
|
||||
.socket_name()
|
||||
.and_then(OsStr::to_str)
|
||||
.map(ToString::to_string);
|
||||
if let Some(socket_name) = &socket_name {
|
||||
let _ = WAYLAND_DISPLAY.set(socket_name.clone());
|
||||
}
|
||||
info!(socket_name, "Wayland active");
|
||||
|
||||
let flush_notify = Arc::new(Notify::new());
|
||||
let client_listener = task::new(
|
||||
|| "Wayland client listener loop",
|
||||
Wayland::client_listener_loop(display_handle, socket, wayland_state.clone()),
|
||||
)?
|
||||
.abort_handle();
|
||||
let client_dispatcher = task::new(
|
||||
|| "Wayland dispatch client loop",
|
||||
Wayland::dispatch_client_loop(display, flush_notify.clone(), wayland_state),
|
||||
)?
|
||||
.abort_handle();
|
||||
|
||||
Ok(Wayland {
|
||||
flush_notify,
|
||||
client_listener,
|
||||
client_dispatcher,
|
||||
renderer,
|
||||
output,
|
||||
dmabuf_rx,
|
||||
})
|
||||
Ok(Self { abort_handle })
|
||||
}
|
||||
|
||||
async fn client_listener_loop(
|
||||
mut display_handle: DisplayHandle,
|
||||
socket: ListeningSocket,
|
||||
state: Arc<Mutex<WaylandState>>,
|
||||
) -> Result<()> {
|
||||
let async_fd = AsyncFd::new(socket.as_fd())?;
|
||||
async fn handle_wayland_loop(mut listener: server::Listener) -> Result<()> {
|
||||
let mut clients = Vec::new();
|
||||
loop {
|
||||
let mut guard = async_fd.readable().await?;
|
||||
let Ok(Some(stream)) = socket.accept() else {
|
||||
guard.clear_ready();
|
||||
continue;
|
||||
};
|
||||
|
||||
let stream = tokio::net::UnixStream::from_std(stream)?;
|
||||
let pid = stream.peer_cred().ok().and_then(|c| c.pid());
|
||||
|
||||
// New client connected
|
||||
let client_state = Arc::new(ClientState {
|
||||
pid,
|
||||
id: OnceLock::new(),
|
||||
compositor_state: Default::default(),
|
||||
seat: state.lock().seat.clone(),
|
||||
});
|
||||
let _client = display_handle.insert_client(stream.into_std()?, client_state.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
async fn dispatch_client_loop(
|
||||
mut display: Display<WaylandState>,
|
||||
flush_notify: Arc<Notify>,
|
||||
state: Arc<Mutex<WaylandState>>,
|
||||
) -> std::io::Result<()> {
|
||||
loop {
|
||||
let poll_fd = display.backend().poll_fd();
|
||||
let async_fd = AsyncFd::new(poll_fd)?;
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = async_fd.readable() => {
|
||||
drop(async_fd);
|
||||
let _span = debug_span!("Dispatch wayland event");
|
||||
let _span = _span.enter();
|
||||
let _ = display.dispatch_clients(&mut *state.lock());
|
||||
let _ = display.flush_clients();
|
||||
}
|
||||
_ = flush_notify.notified() => {
|
||||
drop(async_fd);
|
||||
let _ = display.flush_clients();
|
||||
},
|
||||
if let Ok(Some(stream)) = listener.try_next().await {
|
||||
debug_span!("Accept wayland client").in_scope(|| {
|
||||
if let Ok(client) = WaylandClient::from_stream(stream) {
|
||||
clients.push(client);
|
||||
}
|
||||
});
|
||||
}
|
||||
clients.retain(|client| !client.abort_handle.is_finished());
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn early_frame(graphics_info: &mut GraphicsInfo) {
|
||||
for buffer in BUFFER_REGISTRY.get_valid_contents() {
|
||||
buffer.init_tex(graphics_info);
|
||||
}
|
||||
for surface in WL_SURFACE_REGISTRY.get_valid_contents() {
|
||||
surface.frame_event();
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", name = "Wayland frame", skip(self))]
|
||||
pub fn update(&mut self) {
|
||||
while let Ok((dmabuf, notifier)) = self.dmabuf_rx.try_recv() {
|
||||
if self.renderer.import_dmabuf(&dmabuf, None).is_err() {
|
||||
if let Some(notifier) = notifier {
|
||||
notifier.failed();
|
||||
}
|
||||
}
|
||||
}
|
||||
for core_surface in CORE_SURFACES.get_valid_contents() {
|
||||
core_surface.process(&mut self.renderer);
|
||||
}
|
||||
let _ = self.renderer.cleanup_texture_cache();
|
||||
|
||||
self.flush_notify.notify_waiters();
|
||||
}
|
||||
|
||||
pub fn frame_event(&self) {
|
||||
for core_surface in CORE_SURFACES.get_valid_contents() {
|
||||
core_surface.frame(self.output.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_context_current(&self) {
|
||||
unsafe {
|
||||
let _ = self.renderer.egl_context().make_current();
|
||||
#[instrument(level = "debug", name = "Wayland frame", skip_all)]
|
||||
pub fn update_graphics(materials: &mut Assets<BevyMaterial>, images: &mut Assets<Image>) {
|
||||
for surface in WL_SURFACE_REGISTRY.get_valid_contents() {
|
||||
surface.update_graphics(materials, images);
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Drop for Wayland {
|
||||
fn drop(&mut self) {
|
||||
self.client_listener.abort();
|
||||
self.client_dispatcher.abort();
|
||||
self.abort_handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user