2639 lines
122 KiB
Rust
2639 lines
122 KiB
Rust
//! Wo Wayland compositor
|
||
|
||
mod backend;
|
||
mod config;
|
||
mod cursor;
|
||
mod dmabuf;
|
||
mod electron;
|
||
mod handlers;
|
||
mod nested;
|
||
|
||
use ::input::Libinput;
|
||
use anyhow::Context;
|
||
use calloop::EventLoop;
|
||
use gbm::Modifier;
|
||
use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
|
||
use smithay::backend::allocator::{Fourcc, Slot};
|
||
|
||
use smithay::backend::renderer::element::{Element, RenderElement};
|
||
use smithay::wayland::xdg_activation::{XdgActivationToken, XdgActivationTokenData};
|
||
use smithay::{
|
||
backend::{
|
||
allocator::{
|
||
dmabuf::AsDmabuf,
|
||
gbm::{GbmAllocator, GbmBufferFlags, GbmDevice},
|
||
Swapchain,
|
||
},
|
||
drm::{
|
||
gbm::{framebuffer_from_bo, GbmFramebuffer}, DrmDevice, DrmDeviceFd, DrmNode, PlaneConfig,
|
||
PlaneState,
|
||
},
|
||
egl::{EGLContext, EGLDisplay},
|
||
libinput::{LibinputInputBackend, LibinputSessionInterface},
|
||
renderer::{gles::GlesRenderer, Bind, Color32F, Frame, ImportDma, Renderer},
|
||
session::{Session, libseat::LibSeatSession},
|
||
udev::primary_gpu,
|
||
},
|
||
output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel},
|
||
reexports::{
|
||
drm::control::{connector, Device as _, ModeTypeFlags},
|
||
rustix::fs::OFlags,
|
||
wayland_server::{Display, ListeningSocket},
|
||
},
|
||
utils::{Buffer, DeviceFd, Physical, Rectangle, Transform},
|
||
wayland::{
|
||
xdg_activation::XdgActivationHandler,
|
||
xwayland_shell::XWaylandShellState,
|
||
},
|
||
xwayland::{xwm::X11Wm, XWayland, XWaylandEvent},
|
||
};
|
||
use std::path::Path;
|
||
use std::{
|
||
io::Write,
|
||
time::{Duration, Instant},
|
||
};
|
||
use tracing::{error, info, trace, warn};
|
||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||
|
||
pub mod state;
|
||
pub mod syscall;
|
||
|
||
use crate::{
|
||
config::Config,
|
||
dmabuf::import_electron_frame,
|
||
electron::{ElectronIpc, ElectronProcess},
|
||
state::{BackendData, WoState},
|
||
};
|
||
|
||
struct EventLoopData {
|
||
display: Display<WoState>,
|
||
state: WoState,
|
||
msg_rx: tokio::sync::mpsc::Receiver<crate::electron::ElectronMessage>,
|
||
swapchain: Swapchain<GbmAllocator<DrmDeviceFd>>,
|
||
drm_device_fd: DrmDeviceFd,
|
||
drm: DrmDevice,
|
||
drm_surface: smithay::backend::drm::DrmSurface,
|
||
output_rect: Rectangle<i32, Physical>,
|
||
src_rect: Rectangle<f64, Buffer>,
|
||
bg: Color32F,
|
||
width: u32,
|
||
height: u32,
|
||
frame_time: Duration,
|
||
last_render: std::time::Instant,
|
||
autostart_tasks: Vec<(crate::config::AutostartConfig, std::time::Instant)>,
|
||
first_render: bool,
|
||
wayland_socket: ListeningSocket,
|
||
/// Unused after frame throttle rework — kept as placeholder for future
|
||
/// per-window frame stats if needed.
|
||
#[allow(dead_code)]
|
||
frame_timestamps: std::collections::HashMap<String, Instant>,
|
||
gles_tex_cache: std::collections::HashMap<String, smithay::backend::renderer::gles::GlesTexture>,
|
||
input_events_pending: bool,
|
||
session: smithay::backend::session::libseat::LibSeatSession,
|
||
active: bool,
|
||
vblank_pending: bool,
|
||
/// When vblank_pending was first set; used to detect stuck vblank state.
|
||
vblank_since: Option<Instant>,
|
||
consecutive_flip_failures: u32,
|
||
/// DRI render node path for spawning Electron processes (e.g. `/dev/dri/renderD128`).
|
||
electron_render_node: String,
|
||
/// Total successful page flips (for startup diagnostics).
|
||
total_flips: u64,
|
||
/// Hold the buffer the CRTC is currently scanning out so its GBM BO is
|
||
/// not recycled. Promoted from `pending_slot` on VBlank.
|
||
current_slot: Option<Slot<smithay::backend::allocator::gbm::GbmBuffer>>,
|
||
/// Hold the buffer submitted for the next page flip, waiting for VBlank
|
||
/// confirmation. Without this, the CRTC's active scanout buffer could be
|
||
/// freed prematurely, causing every-other-frame flicker.
|
||
pending_slot: Option<Slot<smithay::backend::allocator::gbm::GbmBuffer>>,
|
||
/// Clone of the libinput context for suspend/resume on VT switch.
|
||
/// The original is consumed by `LibinputInputBackend`; this clone lets us
|
||
/// call `suspend()` / `resume()` from the session notifier callback.
|
||
libinput_context: Libinput,
|
||
}
|
||
|
||
/// Newtype wrapper around `Option<EventLoopData>` for the DRM calloop event
|
||
/// loop. The `Option` is needed because the event loop is created before the
|
||
/// DRM resources are fully initialized (`None` during pre-init, `Some` after).
|
||
/// The newtype allows implementing XWayland traits (orphan rules forbid impls
|
||
/// on `Option<T>` directly).
|
||
struct DrmLoopData(Option<EventLoopData>);
|
||
|
||
impl std::ops::Deref for DrmLoopData {
|
||
type Target = Option<EventLoopData>;
|
||
fn deref(&self) -> &Self::Target { &self.0 }
|
||
}
|
||
impl std::ops::DerefMut for DrmLoopData {
|
||
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
|
||
}
|
||
|
||
// XwmHandler delegation so X11Wm calloop sources work on the DRM event loop.
|
||
impl smithay::xwayland::xwm::XwmHandler for DrmLoopData {
|
||
fn xwm_state(&mut self, xwm: smithay::xwayland::xwm::XwmId) -> &mut X11Wm {
|
||
self.0.as_mut().expect("EventLoopData initialized").state.xwm_state(xwm)
|
||
}
|
||
fn new_window(&mut self, xwm: smithay::xwayland::xwm::XwmId, window: smithay::xwayland::xwm::X11Surface) {
|
||
self.0.as_mut().expect("EventLoopData initialized").state.new_window(xwm, window)
|
||
}
|
||
fn new_override_redirect_window(&mut self, xwm: smithay::xwayland::xwm::XwmId, window: smithay::xwayland::xwm::X11Surface) {
|
||
self.0.as_mut().expect("EventLoopData initialized").state.new_override_redirect_window(xwm, window)
|
||
}
|
||
fn map_window_request(&mut self, xwm: smithay::xwayland::xwm::XwmId, window: smithay::xwayland::xwm::X11Surface) {
|
||
self.0.as_mut().expect("EventLoopData initialized").state.map_window_request(xwm, window)
|
||
}
|
||
fn mapped_override_redirect_window(&mut self, xwm: smithay::xwayland::xwm::XwmId, window: smithay::xwayland::xwm::X11Surface) {
|
||
self.0.as_mut().expect("EventLoopData initialized").state.mapped_override_redirect_window(xwm, window)
|
||
}
|
||
fn unmapped_window(&mut self, xwm: smithay::xwayland::xwm::XwmId, window: smithay::xwayland::xwm::X11Surface) {
|
||
self.0.as_mut().expect("EventLoopData initialized").state.unmapped_window(xwm, window)
|
||
}
|
||
fn destroyed_window(&mut self, xwm: smithay::xwayland::xwm::XwmId, window: smithay::xwayland::xwm::X11Surface) {
|
||
self.0.as_mut().expect("EventLoopData initialized").state.destroyed_window(xwm, window)
|
||
}
|
||
fn configure_request(
|
||
&mut self,
|
||
xwm: smithay::xwayland::xwm::XwmId,
|
||
window: smithay::xwayland::xwm::X11Surface,
|
||
x: Option<i32>,
|
||
y: Option<i32>,
|
||
w: Option<u32>,
|
||
h: Option<u32>,
|
||
reorder: Option<smithay::xwayland::xwm::Reorder>,
|
||
) {
|
||
self.0.as_mut().expect("EventLoopData initialized").state.configure_request(xwm, window, x, y, w, h, reorder)
|
||
}
|
||
fn configure_notify(
|
||
&mut self,
|
||
xwm: smithay::xwayland::xwm::XwmId,
|
||
window: smithay::xwayland::xwm::X11Surface,
|
||
geometry: smithay::utils::Rectangle<i32, smithay::utils::Logical>,
|
||
above: Option<smithay::xwayland::xwm::X11Window>,
|
||
) {
|
||
self.0.as_mut().expect("EventLoopData initialized").state.configure_notify(xwm, window, geometry, above)
|
||
}
|
||
fn resize_request(
|
||
&mut self,
|
||
xwm: smithay::xwayland::xwm::XwmId,
|
||
window: smithay::xwayland::xwm::X11Surface,
|
||
button: u32,
|
||
resize_edge: smithay::xwayland::xwm::ResizeEdge,
|
||
) {
|
||
self.0.as_mut().expect("EventLoopData initialized").state.resize_request(xwm, window, button, resize_edge)
|
||
}
|
||
fn move_request(&mut self, xwm: smithay::xwayland::xwm::XwmId, window: smithay::xwayland::xwm::X11Surface, button: u32) {
|
||
self.0.as_mut().expect("EventLoopData initialized").state.move_request(xwm, window, button)
|
||
}
|
||
fn fullscreen_request(&mut self, xwm: smithay::xwayland::xwm::XwmId, window: smithay::xwayland::xwm::X11Surface) {
|
||
self.0.as_mut().expect("EventLoopData initialized").state.fullscreen_request(xwm, window)
|
||
}
|
||
fn unfullscreen_request(&mut self, xwm: smithay::xwayland::xwm::XwmId, window: smithay::xwayland::xwm::X11Surface) {
|
||
self.0.as_mut().expect("EventLoopData initialized").state.unfullscreen_request(xwm, window)
|
||
}
|
||
}
|
||
|
||
impl smithay::wayland::xwayland_shell::XWaylandShellHandler for DrmLoopData {
|
||
fn xwayland_shell_state(&mut self) -> &mut smithay::wayland::xwayland_shell::XWaylandShellState {
|
||
self.0.as_mut().expect("EventLoopData initialized").state.xwayland_shell_state()
|
||
}
|
||
fn surface_associated(
|
||
&mut self,
|
||
xwm: smithay::xwayland::xwm::XwmId,
|
||
wl_surface: smithay::reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||
window: smithay::xwayland::xwm::X11Surface,
|
||
) {
|
||
self.0.as_mut().expect("EventLoopData initialized").state.surface_associated(xwm, wl_surface, window)
|
||
}
|
||
}
|
||
|
||
const MAX_CONSECUTIVE_FLIP_FAILURES: u32 = 5;
|
||
|
||
fn write_flip_failure_report(report: &str) {
|
||
if let Ok(mut file) = std::fs::OpenOptions::new()
|
||
.create(true)
|
||
.append(true)
|
||
.open("/tmp/wo-compositor-fatal.log")
|
||
{
|
||
let ts = std::time::SystemTime::now()
|
||
.duration_since(std::time::UNIX_EPOCH)
|
||
.map(|d| d.as_secs())
|
||
.unwrap_or(0);
|
||
let _ = writeln!(file, "[{ts}] {report}");
|
||
}
|
||
}
|
||
|
||
fn is_permission_denied_message(msg: &str) -> bool {
|
||
let lowered = msg.to_ascii_lowercase();
|
||
lowered.contains("permission denied") || lowered.contains("eacces") || lowered.contains("eperm")
|
||
}
|
||
|
||
fn is_eagain_message(msg: &str) -> bool {
|
||
let lowered = msg.to_ascii_lowercase();
|
||
lowered.contains("resource temporarily unavailable")
|
||
|| lowered.contains("eagain")
|
||
|| lowered.contains("wouldblock")
|
||
}
|
||
|
||
/// Transient errors from `session.open()` that should be retried with event
|
||
/// loop pumping:
|
||
///
|
||
/// • **EAGAIN** – libseat's seat manager hasn't finished processing a prior
|
||
/// request. Well-documented; all compositors retry.
|
||
///
|
||
/// • **EACCES / EPERM** – On logind-managed sessions (common with Ly, greetd,
|
||
/// and similar display managers) the session activation inside logind may
|
||
/// lag behind libseat's own `is_active()` flag. logind's `TakeDevice`
|
||
/// D-Bus method returns EACCES until it has fully activated the session on
|
||
/// the foreground VT. Pumping the event loop lets the remaining activation
|
||
/// messages propagate, after which the retry succeeds.
|
||
fn is_transient_open_error(msg: &str) -> bool {
|
||
is_eagain_message(msg) || is_permission_denied_message(msg)
|
||
}
|
||
|
||
fn enumerate_drm_card_paths() -> Vec<std::path::PathBuf> {
|
||
let mut paths: Vec<std::path::PathBuf> = std::fs::read_dir("/dev/dri")
|
||
.ok()
|
||
.into_iter()
|
||
.flat_map(|entries| entries.filter_map(Result::ok))
|
||
.map(|entry| entry.path())
|
||
.filter(|path| {
|
||
path.file_name()
|
||
.and_then(|name| name.to_str())
|
||
.map(|name| name.starts_with("card"))
|
||
.unwrap_or(false)
|
||
})
|
||
.collect();
|
||
paths.sort();
|
||
paths
|
||
}
|
||
|
||
/// Resolve the DRI render node (e.g. `/dev/dri/renderD128`) that belongs to
|
||
/// the same physical GPU as `card_path` (e.g. `/dev/dri/card1`).
|
||
///
|
||
/// The lookup uses sysfs: `/sys/class/drm/card<N>/device/drm/renderD*`.
|
||
/// Falls back to `/dev/dri/renderD128` if the mapping cannot be determined.
|
||
fn render_node_for_card(card_path: &std::path::Path) -> String {
|
||
let card_name = card_path
|
||
.file_name()
|
||
.and_then(|n| n.to_str())
|
||
.unwrap_or("card0");
|
||
let sys_dir = format!("/sys/class/drm/{card_name}/device/drm");
|
||
if let Ok(entries) = std::fs::read_dir(&sys_dir) {
|
||
for entry in entries.filter_map(Result::ok) {
|
||
let name = entry.file_name();
|
||
if let Some(s) = name.to_str() {
|
||
if s.starts_with("renderD") {
|
||
let path = format!("/dev/dri/{s}");
|
||
info!(card = %card_path.display(), render_node = %path, "resolved render node");
|
||
return path;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
let fallback = "/dev/dri/renderD128".to_string();
|
||
warn!(
|
||
card = %card_path.display(),
|
||
"could not resolve render node via sysfs, falling back to {fallback}"
|
||
);
|
||
fallback
|
||
}
|
||
|
||
fn is_egl_context_or_surface_lost(err: &str) -> bool {
|
||
let lowered = err.to_ascii_lowercase();
|
||
lowered.contains("context has been lost")
|
||
|| lowered.contains("egl_bad_surface")
|
||
|| lowered.contains("bad_surface")
|
||
|| lowered.contains("not current draw surface")
|
||
|| lowered.contains("egl_bad_alloc")
|
||
|| lowered.contains("failed to allocate resources")
|
||
}
|
||
|
||
fn reinitialize_renderer(data: &mut EventLoopData) -> bool {
|
||
let gbm_device = match GbmDevice::new(data.drm_device_fd.clone()) {
|
||
Ok(device) => device,
|
||
Err(e) => {
|
||
error!("Failed to recreate GBM device after context loss: {e}");
|
||
return false;
|
||
}
|
||
};
|
||
|
||
let egl_display = match unsafe { EGLDisplay::new(gbm_device) } {
|
||
Ok(display) => display,
|
||
Err(e) => {
|
||
error!("Failed to recreate EGL display after context loss: {e}");
|
||
return false;
|
||
}
|
||
};
|
||
|
||
let egl_context = match EGLContext::new(&egl_display) {
|
||
Ok(context) => context,
|
||
Err(e) => {
|
||
error!("Failed to recreate EGL context after context loss: {e}");
|
||
return false;
|
||
}
|
||
};
|
||
|
||
let new_renderer = match unsafe { GlesRenderer::new(egl_context) } {
|
||
Ok(renderer) => renderer,
|
||
Err(e) => {
|
||
error!("Failed to recreate GLES renderer after context loss: {e}");
|
||
return false;
|
||
}
|
||
};
|
||
|
||
let renderer_arc = match data.state.backend.renderer() {
|
||
Some(arc) => arc,
|
||
None => {
|
||
error!("Renderer backend is missing during context-loss recovery");
|
||
return false;
|
||
}
|
||
};
|
||
|
||
match renderer_arc.lock() {
|
||
Ok(mut renderer) => {
|
||
*renderer = new_renderer;
|
||
}
|
||
Err(e) => {
|
||
error!("Failed to lock renderer during context-loss recovery: {e}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
data.gles_tex_cache.clear();
|
||
data.state.texture_cache = crate::dmabuf::TextureCache::default();
|
||
data.state.damaged_windows.extend(
|
||
data.state
|
||
.config
|
||
.windows
|
||
.iter()
|
||
.map(|window| window.name.clone()),
|
||
);
|
||
data.first_render = true;
|
||
data.vblank_pending = false;
|
||
info!("Successfully reinitialized renderer after EGL context loss");
|
||
true
|
||
}
|
||
|
||
fn main() -> Result<(), anyhow::Error> {
|
||
let log_file = std::fs::OpenOptions::new()
|
||
.create(true)
|
||
.append(true)
|
||
.open("/tmp/wo-compositor-debug.log")
|
||
.ok();
|
||
|
||
let registry = tracing_subscriber::registry()
|
||
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| "wo=info,smithay=warn".into()))
|
||
.with(tracing_subscriber::fmt::layer());
|
||
|
||
if let Some(file) = log_file {
|
||
registry
|
||
.with(
|
||
tracing_subscriber::fmt::layer()
|
||
.with_ansi(false)
|
||
.with_writer(std::sync::Mutex::new(file)),
|
||
)
|
||
.init();
|
||
} else {
|
||
registry.init();
|
||
}
|
||
|
||
info!("wo compositor starting");
|
||
|
||
let config = Config::load().context("loading config")?;
|
||
|
||
// Determine backend priority: nested (gles) is prioritized with DRM fallback
|
||
let force_drm = matches!(config.compositor.nested, Some(false));
|
||
|
||
if !force_drm {
|
||
// Try nested mode first (prioritizes gles renderer)
|
||
info!("Attempting nested mode (gles renderer)");
|
||
match nested::run_nested(config.clone()) {
|
||
Ok(_) => return Ok(()),
|
||
Err(e) => {
|
||
info!("Nested mode failed ({}), falling back to DRM/KMS", e);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Fall back to DRM/KMS mode
|
||
info!("Running in DRM/KMS mode");
|
||
run_drm(config)
|
||
}
|
||
|
||
/// Render a single frame - called by VBlank events
|
||
fn render_frame(data: &mut EventLoopData) {
|
||
// Bail if session is paused (VT switched away)
|
||
if !data.active {
|
||
trace!("Skipping render: session inactive");
|
||
return;
|
||
}
|
||
|
||
// Vblank-pending gate: skip if a page flip is outstanding.
|
||
// Safety net: if vblank_pending has been stuck for >500ms, the VBlank event
|
||
// was likely lost (e.g. during DRM master transitions). Force-clear so
|
||
// rendering can resume.
|
||
if data.vblank_pending {
|
||
if let Some(since) = data.vblank_since {
|
||
if since.elapsed() > Duration::from_millis(500) {
|
||
warn!(
|
||
"vblank_pending stuck for {:?} (total_flips={}), force-clearing",
|
||
since.elapsed(),
|
||
data.total_flips,
|
||
);
|
||
data.vblank_pending = false;
|
||
data.vblank_since = None;
|
||
data.current_slot = data.pending_slot.take();
|
||
} else {
|
||
return;
|
||
}
|
||
} else {
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Frame pacing: respect adaptive timing for input responsiveness
|
||
let adaptive_frame_time = if data.input_events_pending {
|
||
Duration::from_millis(8)
|
||
} else {
|
||
data.frame_time
|
||
};
|
||
|
||
if !data.first_render && data.last_render.elapsed() < adaptive_frame_time {
|
||
return;
|
||
}
|
||
data.last_render = std::time::Instant::now();
|
||
data.input_events_pending = false;
|
||
|
||
// First render: mark all windows damaged and log diagnostic state
|
||
if data.first_render {
|
||
info!(
|
||
"First render: output={}x{}, windows={}, bg={:?}",
|
||
data.width, data.height,
|
||
data.state.config.windows.len(),
|
||
data.bg,
|
||
);
|
||
for win_cfg in &data.state.config.windows {
|
||
data.state.damaged_windows.insert(win_cfg.name.clone());
|
||
}
|
||
data.first_render = false;
|
||
}
|
||
|
||
// Acquire swapchain slot
|
||
let slot = match data.swapchain.acquire() {
|
||
Ok(Some(slot)) => slot,
|
||
Ok(None) => {
|
||
if data.total_flips == 0 {
|
||
warn!("Swapchain: no free slots on first frame");
|
||
}
|
||
return;
|
||
}
|
||
Err(e) => {
|
||
warn!("Error acquiring swapchain slot: {e}");
|
||
return;
|
||
}
|
||
};
|
||
|
||
let mut target_dmabuf = match slot.export() {
|
||
Ok(dmabuf) => dmabuf,
|
||
Err(e) => {
|
||
warn!("Error exporting swapchain dmabuf: {e}");
|
||
data.swapchain.submitted(&slot);
|
||
return;
|
||
}
|
||
};
|
||
|
||
// Perform actual rendering
|
||
let mut context_lost = false;
|
||
let render_ok = (|| {
|
||
let renderer_arc = match data.state.backend.renderer() {
|
||
Some(arc) => arc,
|
||
None => {
|
||
warn!("No renderer available");
|
||
return false;
|
||
}
|
||
};
|
||
let mut renderer_guard = match renderer_arc.lock() {
|
||
Ok(guard) => guard,
|
||
Err(e) => {
|
||
warn!("Failed to lock renderer: {e}");
|
||
return false;
|
||
}
|
||
};
|
||
|
||
let renderer: &mut GlesRenderer = &mut renderer_guard;
|
||
|
||
// Prepare Wayland surface elements before starting the frame.
|
||
// When Electron IPC is active, surfaces are exported via SHM/DMABUF
|
||
// to the client — skip the expensive GL import here.
|
||
use smithay::backend::renderer::element::{surface::render_elements_from_surface_tree, surface::WaylandSurfaceRenderElement, Kind, RenderElement};
|
||
use smithay::utils::Scale as ScaleF;
|
||
|
||
let scale = ScaleF::from(1.0);
|
||
let has_electron = data.state.electron_ipc.is_some();
|
||
let mut wayland_elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> = Vec::new();
|
||
|
||
if !has_electron {
|
||
for window in data.state.space.elements() {
|
||
if let Some(location) = data.state.space.element_location(window) {
|
||
let surface = window.toplevel()
|
||
.map(|t| t.wl_surface().clone())
|
||
.or_else(|| window.x11_surface().and_then(|x| x.wl_surface().clone()));
|
||
|
||
if let Some(surface) = surface {
|
||
let elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> = render_elements_from_surface_tree(
|
||
renderer,
|
||
&surface,
|
||
(location.x, location.y),
|
||
scale,
|
||
1.0,
|
||
Kind::Unspecified,
|
||
);
|
||
|
||
wayland_elements.extend(elements);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Prepare cursor elements from the cursor surface (set by Wayland clients)
|
||
let cursor_elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> =
|
||
if let smithay::input::pointer::CursorImageStatus::Surface(ref cursor_surface) = data.state.cursor_status {
|
||
render_elements_from_surface_tree(
|
||
renderer,
|
||
cursor_surface,
|
||
(data.state.pointer_location.x as i32, data.state.pointer_location.y as i32),
|
||
scale,
|
||
1.0,
|
||
Kind::Cursor,
|
||
)
|
||
} else {
|
||
Vec::new()
|
||
};
|
||
|
||
// Load themed cursor texture for Named cursor status
|
||
let themed_cursor = if let smithay::input::pointer::CursorImageStatus::Named(ref icon) = data.state.cursor_status {
|
||
data.state.cursor_theme_manager.get_cursor(icon.name(), renderer).cloned()
|
||
} else if cursor_elements.is_empty() {
|
||
// No client surface and not Hidden — use default theme cursor as fallback
|
||
if !matches!(data.state.cursor_status, smithay::input::pointer::CursorImageStatus::Hidden) {
|
||
data.state.cursor_theme_manager.get_cursor("default", renderer).cloned()
|
||
} else {
|
||
None
|
||
}
|
||
} else {
|
||
None
|
||
};
|
||
|
||
let mut target = match renderer.bind(&mut target_dmabuf) {
|
||
Ok(t) => t,
|
||
Err(e) => {
|
||
warn!("Failed to bind renderer to dmabuf: {e}");
|
||
return false;
|
||
}
|
||
};
|
||
|
||
let mut frame = match renderer.render(
|
||
&mut target,
|
||
data.output_rect.size,
|
||
Transform::Normal,
|
||
) {
|
||
Ok(f) => f,
|
||
Err(e) => {
|
||
warn!("Failed to start frame: {e}");
|
||
return false;
|
||
}
|
||
};
|
||
|
||
if let Err(e) = frame.clear(data.bg, &[data.output_rect]) {
|
||
warn!("Clear failed: {e}");
|
||
return false;
|
||
}
|
||
|
||
// Render Electron window textures sorted by z_order (lowest first)
|
||
let mut sorted_windows: Vec<_> = data.state.config.windows.iter().collect();
|
||
sorted_windows.sort_by_key(|w| w.z_order);
|
||
|
||
let mut rendered_tex_count = 0u32;
|
||
for win_cfg in sorted_windows {
|
||
if let Some(texture) = data.gles_tex_cache.get(&win_cfg.name) {
|
||
let (x, y, _w, _h) = data.state
|
||
.window_positions
|
||
.get(&win_cfg.name)
|
||
.copied()
|
||
.unwrap_or((win_cfg.x, win_cfg.y, win_cfg.width, win_cfg.height));
|
||
|
||
let mapped = data.state.window_mapped.get(&win_cfg.name).copied().unwrap_or(true);
|
||
if !mapped {
|
||
continue;
|
||
}
|
||
|
||
if let Err(e) = frame.render_texture_at(
|
||
texture,
|
||
(x, y).into(),
|
||
1,
|
||
1.0,
|
||
Transform::Normal,
|
||
&[data.output_rect],
|
||
&[],
|
||
1.0,
|
||
) {
|
||
if is_egl_context_or_surface_lost(&e.to_string()) {
|
||
error!("EGL context lost while rendering Electron texture");
|
||
context_lost = true;
|
||
return false;
|
||
}
|
||
warn!("Electron texture rendering failed for {}: {e}", win_cfg.name);
|
||
} else {
|
||
rendered_tex_count += 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Log every frame for first 60 frames to diagnose flickering
|
||
if data.total_flips < 60 {
|
||
info!(
|
||
"render_frame #{}: textures_rendered={}, gles_cache={}, windows={}",
|
||
data.total_flips, rendered_tex_count,
|
||
data.gles_tex_cache.len(),
|
||
data.state.config.windows.len(),
|
||
);
|
||
}
|
||
|
||
// Wayland window surfaces are exported to Electron clients via
|
||
// DMABUF/SHM passthrough (below), not rendered directly to the DRM
|
||
// framebuffer. Only render them directly when there is no Electron IPC
|
||
// (i.e. pure compositor mode without a shell).
|
||
if data.state.electron_ipc.is_none() {
|
||
for element in wayland_elements {
|
||
let src = element.src();
|
||
let dst = element.geometry(scale);
|
||
if let Err(e) = element.draw(&mut frame, src, dst, &[data.output_rect], &[]) {
|
||
if is_egl_context_or_surface_lost(&e.to_string()) {
|
||
error!("EGL context lost during space rendering");
|
||
context_lost = true;
|
||
return false;
|
||
}
|
||
trace!("Element rendering failed: {e}");
|
||
}
|
||
}
|
||
}
|
||
|
||
// Render cursor on top of everything
|
||
if !cursor_elements.is_empty() {
|
||
for element in &cursor_elements {
|
||
let src = element.src();
|
||
let dst = element.geometry(scale);
|
||
if let Err(e) = element.draw(&mut frame, src, dst, &[data.output_rect], &[]) {
|
||
trace!("Failed to render cursor element: {e}");
|
||
}
|
||
}
|
||
} else if let Some(ref cur) = themed_cursor {
|
||
let cx = data.state.pointer_location.x as i32 - cur.xhot as i32;
|
||
let cy = data.state.pointer_location.y as i32 - cur.yhot as i32;
|
||
if let Err(e) = frame.render_texture_at(
|
||
&cur.texture,
|
||
(cx, cy).into(),
|
||
1,
|
||
1.0,
|
||
Transform::Normal,
|
||
&[data.output_rect],
|
||
&[],
|
||
1.0,
|
||
) {
|
||
trace!("Failed to render themed cursor: {e}");
|
||
}
|
||
}
|
||
|
||
// Send frame callbacks
|
||
let presented_at = std::time::SystemTime::now()
|
||
.duration_since(std::time::UNIX_EPOCH)
|
||
.unwrap_or_default();
|
||
|
||
for window in data.state.space.elements() {
|
||
window.send_frame(
|
||
&data.state.backend.output,
|
||
presented_at,
|
||
Some(data.frame_time),
|
||
|_, _| Some(data.state.backend.output.clone()),
|
||
);
|
||
}
|
||
|
||
let dirty_surfaces: Vec<_> = data.state.dirty_surfaces.drain().collect();
|
||
for surface in &dirty_surfaces {
|
||
smithay::desktop::utils::send_frames_surface_tree(
|
||
surface,
|
||
&data.state.backend.output,
|
||
presented_at,
|
||
Some(data.frame_time),
|
||
|_, _| Some(data.state.backend.output.clone()),
|
||
);
|
||
}
|
||
|
||
// Export dirty Wayland surfaces to Electron clients via DMABUF/SHM.
|
||
// SHM surfaces are written directly to memfd (ping-pong double-buffered)
|
||
// to avoid an intermediate Vec<u8> copy. When usable damage rects exist,
|
||
// only the affected rows are written for further savings.
|
||
if data.state.electron_ipc.is_some() {
|
||
use smithay::backend::renderer::utils::RendererSurfaceStateUserData;
|
||
use smithay::reexports::wayland_server::Resource;
|
||
use smithay::wayland::compositor::{with_states, Damage, SurfaceAttributes};
|
||
use smithay::wayland::shm::with_buffer_contents;
|
||
use std::os::unix::io::AsRawFd;
|
||
use nix::sys::memfd::MFdFlags;
|
||
use tracing::debug;
|
||
|
||
let pid = std::process::id();
|
||
|
||
for surface in &dirty_surfaces {
|
||
let surface_id = surface.id();
|
||
let (window_name, window_geo) = if let Some(n) =
|
||
data.state.wayland_window_names.get(&surface_id).cloned()
|
||
{
|
||
let sid = surface_id.clone();
|
||
let geo = data.state.space.elements().find_map(|w| {
|
||
let wl_id = w.toplevel()
|
||
.map(|t| t.wl_surface().id())
|
||
.or_else(|| w.x11_surface().and_then(|x| x.wl_surface().map(|s| s.id())));
|
||
if wl_id == Some(sid.clone()) {
|
||
data.state.space.element_geometry(w)
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
(n, geo)
|
||
} else {
|
||
let sid = surface_id.clone();
|
||
let x11_result = data.state.space.elements().find_map(|w| {
|
||
let x11 = w.x11_surface()?;
|
||
let wl = x11.wl_surface()?;
|
||
if wl.id() == sid.clone() {
|
||
let name = data.state.x11_window_names.get(&x11.window_id()).cloned()?;
|
||
let geo = data.state.space.element_geometry(w);
|
||
Some((name, geo))
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
match x11_result {
|
||
Some((n, geo)) => (n, geo),
|
||
None => continue,
|
||
}
|
||
};
|
||
|
||
let Some(_geo) = window_geo else { continue };
|
||
|
||
// Extract Wayland damage rects for partial-update optimization.
|
||
let damage_rects: Vec<crate::state::DamageRect> = with_states(surface, |states| {
|
||
let mut attrs = states.cached_state.get::<SurfaceAttributes>();
|
||
let current = attrs.current();
|
||
current.damage.iter().map(|d| {
|
||
match d {
|
||
Damage::Buffer(rect) => crate::state::DamageRect {
|
||
x: rect.loc.x,
|
||
y: rect.loc.y,
|
||
width: rect.size.w,
|
||
height: rect.size.h,
|
||
},
|
||
Damage::Surface(rect) => {
|
||
let scale = current.buffer_scale;
|
||
crate::state::DamageRect {
|
||
x: rect.loc.x * scale,
|
||
y: rect.loc.y * scale,
|
||
width: rect.size.w * scale,
|
||
height: rect.size.h * scale,
|
||
}
|
||
}
|
||
}
|
||
}).collect()
|
||
});
|
||
|
||
// Try SHM buffer: write directly to mmap'd memfd with BGRA→RGBA conversion
|
||
let shm_meta: Option<(u32, u32, u32, i32)> = with_states(surface, |states| {
|
||
let rss_data = states.data_map.get::<RendererSurfaceStateUserData>()?;
|
||
let rss = rss_data.lock().ok()?;
|
||
let buffer = rss.buffer()?;
|
||
|
||
let mut result: Option<(u32, u32, u32, i32)> = None;
|
||
let _ = with_buffer_contents(buffer, |ptr, len, spec| {
|
||
let src_stride = spec.stride.max(0) as usize;
|
||
let bw = spec.width.max(0) as u32;
|
||
let bh = spec.height.max(0) as u32;
|
||
let pixel_bytes = src_stride * bh as usize;
|
||
if pixel_bytes == 0 || pixel_bytes > len { return; }
|
||
|
||
let dst_stride = (bw as usize) * 4;
|
||
let dst_size = dst_stride * bh as usize;
|
||
|
||
let pair = data.state.window_shm_slots
|
||
.entry(window_name.clone())
|
||
.or_insert_with(|| {
|
||
crate::state::ShmSlotPair::new(dst_size)
|
||
.expect("creating ShmSlotPair")
|
||
});
|
||
|
||
let slot = pair.write_slot_mut();
|
||
if let Err(e) = slot.ensure_size(dst_size) {
|
||
warn!("Failed to resize SHM slot for {}: {}", window_name, e);
|
||
return;
|
||
}
|
||
|
||
let dst = unsafe { slot.as_mut_slice() };
|
||
let src = unsafe { std::slice::from_raw_parts(ptr, pixel_bytes) };
|
||
|
||
let do_partial = !damage_rects.is_empty()
|
||
&& damage_rects.len() < 64;
|
||
|
||
if do_partial {
|
||
for rect in &damage_rects {
|
||
let rx = (rect.x as usize).min(bw as usize);
|
||
let ry = (rect.y as usize).min(bh as usize);
|
||
let rw = (rect.width as usize).min(bw as usize - rx);
|
||
let rh = (rect.height as usize).min(bh as usize - ry);
|
||
for row in ry..ry + rh {
|
||
let src_row_off = row * src_stride + rx * 4;
|
||
let dst_row_off = row * dst_stride + rx * 4;
|
||
for col in 0..rw {
|
||
let si = src_row_off + col * 4;
|
||
let di = dst_row_off + col * 4;
|
||
dst[di] = src[si + 2]; // R
|
||
dst[di + 1] = src[si + 1]; // G
|
||
dst[di + 2] = src[si]; // B
|
||
dst[di + 3] = src[si + 3]; // A
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
for row in 0..bh as usize {
|
||
let src_row_off = row * src_stride;
|
||
let dst_row_off = row * dst_stride;
|
||
for col in 0..bw as usize {
|
||
let si = src_row_off + col * 4;
|
||
let di = dst_row_off + col * 4;
|
||
dst[di] = src[si + 2]; // R
|
||
dst[di + 1] = src[si + 1]; // G
|
||
dst[di + 2] = src[si]; // B
|
||
dst[di + 3] = src[si + 3]; // A
|
||
}
|
||
}
|
||
}
|
||
|
||
// Get the fd of the slot we just wrote to (write_slot_mut already flipped)
|
||
let wrote_idx = 1 - pair.current_write_idx();
|
||
let memfd_raw = std::os::unix::io::AsRawFd::as_raw_fd(&pair.slots[wrote_idx].fd);
|
||
result = Some((bw, bh, bw * 4, memfd_raw));
|
||
});
|
||
result
|
||
});
|
||
|
||
if let Some((bw, bh, stride, memfd_raw)) = shm_meta {
|
||
if let Some(ref ipc) = data.state.electron_ipc {
|
||
if let Err(e) = ipc.broadcast_shm_buffer(
|
||
&window_name, bw, bh, stride,
|
||
pid, memfd_raw,
|
||
&damage_rects,
|
||
) {
|
||
debug!("DRM shm buffer broadcast failed for {}: {}", window_name, e);
|
||
}
|
||
}
|
||
} else if let Some(ref ipc) = data.state.electron_ipc {
|
||
// Try DMABUF (hardware-rendered clients / games)
|
||
let dmabuf_opt = with_states(surface, |states| {
|
||
let rss_data = states.data_map.get::<RendererSurfaceStateUserData>()?;
|
||
let rss = rss_data.lock().ok()?;
|
||
let buffer = rss.buffer()?;
|
||
smithay::wayland::dmabuf::get_dmabuf(buffer).ok().cloned()
|
||
});
|
||
|
||
if let Some(dmabuf) = dmabuf_opt {
|
||
if let Err(e) = ipc.broadcast_dmabuf_frame(&window_name, &dmabuf) {
|
||
debug!("DRM dmabuf broadcast failed for {}: {}", window_name, e);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Send frame callbacks for the cursor surface so the client keeps
|
||
// animating it (e.g. spinning/loading cursors).
|
||
if let smithay::input::pointer::CursorImageStatus::Surface(ref cursor_surface) = data.state.cursor_status {
|
||
smithay::desktop::utils::send_frames_surface_tree(
|
||
cursor_surface,
|
||
&data.state.backend.output,
|
||
presented_at,
|
||
Some(data.frame_time),
|
||
|_, _| Some(data.state.backend.output.clone()),
|
||
);
|
||
}
|
||
|
||
// Finalize the GL frame — flushes all rendering commands to the
|
||
// framebuffer. Without this, the GBM BO may contain stale or
|
||
// incomplete pixel data, causing flicker.
|
||
match frame.finish() {
|
||
Ok(_) => {}
|
||
Err(e) => {
|
||
warn!("Failed to finish frame: {e}");
|
||
if is_egl_context_or_surface_lost(&e.to_string()) {
|
||
error!("EGL context/surface lost while finishing frame");
|
||
context_lost = true;
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Ensure all GPU work targeting this BO completes before the DRM
|
||
// subsystem starts scanning it out. glFlush (done by finish())
|
||
// only queues commands; glFinish blocks until they're done.
|
||
renderer.with_context(|gl| unsafe { gl.Finish() })
|
||
.unwrap_or_else(|e| warn!("glFinish failed: {e}"));
|
||
|
||
// Unbind the render target so the BO is no longer GL-locked.
|
||
drop(target);
|
||
|
||
true
|
||
})();
|
||
|
||
if !render_ok {
|
||
if data.total_flips == 0 {
|
||
warn!("Rendering failed on first frame, skipping page flip");
|
||
}
|
||
data.swapchain.submitted(&slot);
|
||
data.state.damaged_windows.clear();
|
||
if context_lost {
|
||
let _ = reinitialize_renderer(data);
|
||
}
|
||
return;
|
||
}
|
||
|
||
data.state.damaged_windows.clear();
|
||
data.state.dirty_surfaces.clear();
|
||
|
||
// SHM surface exports are now written directly to memfd during the
|
||
// render pass (above), so no post-render copy is needed.
|
||
|
||
// Get or create the DRM framebuffer for this slot.
|
||
// Caching in the slot's userdata avoids a create/destroy cycle every
|
||
// frame and — critically — keeps the GbmFramebuffer alive through
|
||
// the page flip. Previously the FB was dropped at the end of
|
||
// render_frame, calling drmModeRmFB while the flip was still pending.
|
||
// While the kernel holds an internal reference, some DRM drivers
|
||
// misbehave when the userspace handle is removed before the flip
|
||
// completes, causing every-other-frame flicker.
|
||
let fb_handle = if let Some(cached_fb) = slot.userdata().get::<GbmFramebuffer>() {
|
||
*cached_fb.as_ref()
|
||
} else {
|
||
let fb = match framebuffer_from_bo(&data.drm_device_fd, &slot, false) {
|
||
Ok(fb) => fb,
|
||
Err(e) => {
|
||
warn!("Error creating framebuffer: {e}");
|
||
data.swapchain.submitted(&slot);
|
||
return;
|
||
}
|
||
};
|
||
let handle = *fb.as_ref();
|
||
slot.userdata().insert_if_missing(move || fb);
|
||
handle
|
||
};
|
||
|
||
let plane_state = PlaneState {
|
||
handle: data.drm_surface.plane(),
|
||
config: Some(PlaneConfig {
|
||
src: data.src_rect,
|
||
dst: data.output_rect,
|
||
transform: Transform::Normal,
|
||
alpha: 1.0,
|
||
damage_clips: None,
|
||
fb: fb_handle,
|
||
fence: None,
|
||
}),
|
||
};
|
||
|
||
let tex_count = data.gles_tex_cache.len();
|
||
|
||
// Schedule page flip (or full modeset commit for the first frame).
|
||
// smithay's DrmSurface tracks pending state (mode, connectors). When
|
||
// commit_pending() is true (e.g. first frame after surface creation or
|
||
// VT switch), we must use commit() which includes ALLOW_MODESET.
|
||
// page_flip() only does a non-blocking flip without modesetting and will
|
||
// fail with EINVAL if the CRTC doesn't have a mode set yet.
|
||
//
|
||
// IMPORTANT: Even when commit_pending() is false (Ly left the CRTC in a
|
||
// matching mode), the first frame still needs a full modeset commit to
|
||
// ensure CRTC ACTIVE=1 and a valid framebuffer is bound. page_flip alone
|
||
// may "succeed" but produce no output if the CRTC was left without an
|
||
// active framebuffer by the display manager.
|
||
let commit_pending = data.drm_surface.commit_pending();
|
||
let force_initial = data.total_flips == 0;
|
||
let result = if commit_pending || force_initial {
|
||
info!(
|
||
"Performing modeset commit (commit_pending={}, first_frame={}, textures={})",
|
||
commit_pending, force_initial, tex_count
|
||
);
|
||
data.drm_surface.commit([plane_state.clone()], true)
|
||
} else {
|
||
data.drm_surface.page_flip([plane_state.clone()], true)
|
||
};
|
||
|
||
match result {
|
||
Ok(_) => {
|
||
data.consecutive_flip_failures = 0;
|
||
data.vblank_pending = true;
|
||
data.vblank_since = Some(Instant::now());
|
||
data.swapchain.submitted(&slot);
|
||
// Hold the slot as pending until VBlank confirms the flip.
|
||
// current_slot (the active scanout buffer) stays alive until then.
|
||
data.pending_slot = Some(slot);
|
||
data.total_flips += 1;
|
||
if data.total_flips == 1 {
|
||
info!("First successful page flip (commit_pending={})", commit_pending);
|
||
}
|
||
}
|
||
Err(e) => {
|
||
data.consecutive_flip_failures = data.consecutive_flip_failures.saturating_add(1);
|
||
let page_flip_err = e.to_string();
|
||
let would_block = {
|
||
let lowered = page_flip_err.to_ascii_lowercase();
|
||
lowered.contains("wouldblock")
|
||
|| lowered.contains("resource temporarily unavailable")
|
||
|| lowered.contains("eagain")
|
||
};
|
||
|
||
let page_flip_report = format!(
|
||
"DRM page_flip failed (count={}): {}{}",
|
||
data.consecutive_flip_failures,
|
||
page_flip_err,
|
||
if would_block { " [WouldBlock/EAGAIN]" } else { "" }
|
||
);
|
||
warn!("{page_flip_report}. Attempting forced commit.");
|
||
write_flip_failure_report(&page_flip_report);
|
||
|
||
if let Err(commit_err) = data.drm_surface.commit([plane_state], true) {
|
||
error!("Fatal DRM failure: {commit_err}");
|
||
let commit_report = format!(
|
||
"DRM commit failed after page_flip failure (count={}): {}",
|
||
data.consecutive_flip_failures,
|
||
commit_err
|
||
);
|
||
write_flip_failure_report(&commit_report);
|
||
data.vblank_pending = false;
|
||
data.vblank_since = None;
|
||
if data.consecutive_flip_failures >= MAX_CONSECUTIVE_FLIP_FAILURES {
|
||
let report = format!(
|
||
"Exceeded consecutive DRM flip failures ({}). Last page_flip error: {e}; last commit error: {commit_err}. Exiting compositor.",
|
||
data.consecutive_flip_failures
|
||
);
|
||
error!("{report}");
|
||
write_flip_failure_report(&report);
|
||
data.active = false;
|
||
data.state.running = false;
|
||
return;
|
||
}
|
||
} else {
|
||
data.consecutive_flip_failures = 0;
|
||
data.vblank_pending = true;
|
||
data.vblank_since = Some(Instant::now());
|
||
data.swapchain.submitted(&slot);
|
||
data.pending_slot = Some(slot);
|
||
data.total_flips += 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn run_drm(mut config: Config) -> Result<(), anyhow::Error> {
|
||
let (session, session_notifier) = LibSeatSession::new().map_err(|err| {
|
||
anyhow::anyhow!(
|
||
"Failed to create libseat session: {err}. wo requires a seat-managed session to access DRM/input devices safely. Run from a proper login session (logind/seatd)."
|
||
)
|
||
})?;
|
||
info!("LibSeat session created successfully");
|
||
info!(
|
||
seat = %session.seat(),
|
||
libseat_backend = %std::env::var("LIBSEAT_BACKEND").unwrap_or_else(|_| "auto (not set)".into()),
|
||
"libseat environment"
|
||
);
|
||
let mut session = session;
|
||
|
||
// Create the event loop early—before opening any DRM devices.
|
||
// Niri, wlroots, and KWin all register the session notifier on the event
|
||
// loop before attempting to open device fds. Without this, calloop never
|
||
// polls the libseat fd, seat.dispatch() is never called, the EnableSeat
|
||
// message from seatd/logind stays unread, and libseat_open_device()
|
||
// returns EAGAIN for every attempt.
|
||
let mut event_loop: EventLoop<DrmLoopData> =
|
||
EventLoop::try_new().context("creating event loop")?;
|
||
let loop_handle = event_loop.handle();
|
||
let loop_signal = event_loop.get_signal();
|
||
|
||
loop_handle
|
||
.insert_source(session_notifier, |event, _, data: &mut DrmLoopData| {
|
||
use smithay::backend::session::Event;
|
||
let Some(data) = data.0.as_mut() else {
|
||
// Pre-init phase: ActivateSession received before DRM setup is
|
||
// complete. The session is now active; device opening can proceed.
|
||
return;
|
||
};
|
||
match event {
|
||
Event::PauseSession => {
|
||
info!("Session paused (VT switched away). Pausing DRM device and suspending libinput.");
|
||
data.active = false;
|
||
data.vblank_pending = false;
|
||
data.vblank_since = None;
|
||
data.libinput_context.suspend();
|
||
data.drm.pause();
|
||
}
|
||
Event::ActivateSession => {
|
||
info!("Session activated (VT switched back). Activating DRM device and resuming libinput.");
|
||
if let Err(e) = data.drm.activate(false) {
|
||
error!("Failed to activate DRM device on session resume: {e}");
|
||
data.active = false;
|
||
data.vblank_pending = false;
|
||
data.vblank_since = None;
|
||
return;
|
||
}
|
||
if let Err(()) = data.libinput_context.resume() {
|
||
error!("Failed to resume libinput context on session resume");
|
||
// Continue anyway — DRM is active, input may recover on next event
|
||
}
|
||
data.active = true;
|
||
data.vblank_pending = false;
|
||
data.vblank_since = None;
|
||
if let Err(e) = data.display.flush_clients() {
|
||
warn!("Failed to flush Wayland clients on activation: {e}");
|
||
}
|
||
render_frame(data);
|
||
}
|
||
}
|
||
})
|
||
.map_err(|e| anyhow::anyhow!("Error inserting session notifier: {e:?}"))?;
|
||
info!("Session notifier registered for VT switching");
|
||
|
||
// Dispatch the event loop until the seat is active and ready for device
|
||
// requests. This must run even when session.is_active() is already true:
|
||
// LibSeatSession::new() may have set the active flag via an internal
|
||
// dispatch(0), but calloop still needs to poll the libseat fd at least
|
||
// once *after* the notifier is registered before open_device() will
|
||
// succeed. Without this, libseat_open_device() returns EAGAIN immediately.
|
||
if !session.is_active() {
|
||
info!("Waiting for seat activation (EnableSeat from seat manager)...");
|
||
}
|
||
let mut pre_init = DrmLoopData(None);
|
||
let deadline = Instant::now() + Duration::from_secs(5);
|
||
loop {
|
||
let remaining = deadline.saturating_duration_since(Instant::now());
|
||
if remaining.is_zero() {
|
||
break;
|
||
}
|
||
event_loop
|
||
.dispatch(
|
||
Some(remaining.min(Duration::from_millis(200))),
|
||
&mut pre_init,
|
||
)
|
||
.context("dispatching event loop for session activation")?;
|
||
if session.is_active() {
|
||
info!("Session is active, ready to open devices");
|
||
break;
|
||
}
|
||
}
|
||
if !session.is_active() {
|
||
return Err(anyhow::anyhow!(
|
||
"Seat session never became active. \
|
||
Run wo from a proper TTY login session managed by logind or seatd. \
|
||
If another compositor holds the seat, switch to a free VT first."
|
||
));
|
||
}
|
||
|
||
// One unconditional drain after the activation break: the dispatch() call
|
||
// that set is_active()=true may have left additional buffered libseat events
|
||
// unread. A zero-timeout pump clears them so that the first open_device()
|
||
// request sees a clean state and does not get a spurious EAGAIN.
|
||
let _ = event_loop.dispatch(Some(Duration::from_millis(0)), &mut pre_init);
|
||
|
||
let seat_name = session.seat();
|
||
|
||
info!("Using seat: {}", seat_name);
|
||
|
||
let mut display: Display<WoState> = Display::new().context("creating Wayland display")?;
|
||
let dh = display.handle();
|
||
|
||
let configured_gpu = config
|
||
.compositor
|
||
.drm_device
|
||
.as_ref()
|
||
.map(std::path::PathBuf::from);
|
||
let detected_primary_gpu = primary_gpu(&seat_name).ok().flatten();
|
||
|
||
let mut gpu_candidates = Vec::new();
|
||
if let Some(path) = configured_gpu {
|
||
gpu_candidates.push(path);
|
||
} else {
|
||
if let Some(path) = detected_primary_gpu {
|
||
gpu_candidates.push(path);
|
||
}
|
||
for path in enumerate_drm_card_paths() {
|
||
if !gpu_candidates.iter().any(|p| p == &path) {
|
||
gpu_candidates.push(path);
|
||
}
|
||
}
|
||
}
|
||
|
||
if gpu_candidates.is_empty() {
|
||
return Err(anyhow::anyhow!(
|
||
"no DRM card candidates found (checked primary GPU and /dev/dri/card*)"
|
||
));
|
||
}
|
||
|
||
info!(
|
||
candidates = ?gpu_candidates,
|
||
"trying DRM devices in priority order"
|
||
);
|
||
|
||
let mut chosen_gpu_path = None;
|
||
let mut chosen_drm_fd = None;
|
||
let mut open_errors: Vec<String> = Vec::new();
|
||
|
||
// Open DRM in blocking mode. Some drivers can return EAGAIN for
|
||
// O_NONBLOCK opens even when no other compositor/session owns the device.
|
||
// libseat already provides asynchronous behavior and transient EAGAIN
|
||
// handling through the dispatch/retry loop below.
|
||
let open_flags = OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY;
|
||
|
||
// Round-robin retry across all GPU candidates for transient errors.
|
||
//
|
||
// Both EAGAIN and EACCES/EPERM are retried with event-loop pumping:
|
||
//
|
||
// • EAGAIN: libseat's seat manager hasn't finished processing an earlier
|
||
// request — a seat-level condition, not GPU-specific.
|
||
//
|
||
// • EACCES/EPERM: On logind-managed sessions (e.g. launched by Ly, greetd)
|
||
// the session activation inside logind can lag behind libseat's own
|
||
// is_active() flag. logind's TakeDevice D-Bus call returns EACCES until
|
||
// it has fully activated the session on the foreground VT. Pumping the
|
||
// event loop lets the remaining activation messages propagate.
|
||
//
|
||
// After each round the loop dispatches the event loop for 50 ms and retries
|
||
// all still-pending candidates. Budget: MAX_TRANSIENT_ROUNDS × 50 ms ≈ 2 s.
|
||
const MAX_TRANSIENT_ROUNDS: u32 = 40;
|
||
let mut transient_pending: Vec<std::path::PathBuf> = gpu_candidates;
|
||
'outer: for round in 0..=MAX_TRANSIENT_ROUNDS {
|
||
let mut still_transient: Vec<std::path::PathBuf> = Vec::new();
|
||
for gpu_path in transient_pending {
|
||
if round == 0 {
|
||
info!("Opening DRM device: {:?}", gpu_path);
|
||
}
|
||
match session.open(&gpu_path, open_flags) {
|
||
Ok(fd) => {
|
||
if round > 0 {
|
||
info!(gpu = %gpu_path.display(), round, "device opened after transient retries");
|
||
}
|
||
info!(gpu = %gpu_path.display(), "using GPU");
|
||
chosen_gpu_path = Some(gpu_path);
|
||
chosen_drm_fd = Some(fd);
|
||
break 'outer;
|
||
}
|
||
Err(e) => {
|
||
let detail = e.to_string();
|
||
if is_transient_open_error(&detail) {
|
||
if round == 0 {
|
||
let kind = if is_eagain_message(&detail) {
|
||
"EAGAIN"
|
||
} else {
|
||
"permission denied (transient on logind sessions)"
|
||
};
|
||
info!(
|
||
gpu = %gpu_path.display(),
|
||
"libseat returned {kind}; will retry after event loop pump..."
|
||
);
|
||
}
|
||
still_transient.push(gpu_path);
|
||
} else {
|
||
warn!(
|
||
gpu = %gpu_path.display(),
|
||
round,
|
||
"Failed to open DRM device (non-transient): {detail}"
|
||
);
|
||
open_errors.push(format!("{}: {detail}", gpu_path.display()));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
transient_pending = still_transient;
|
||
if transient_pending.is_empty() {
|
||
break;
|
||
}
|
||
if round < MAX_TRANSIENT_ROUNDS {
|
||
let _ = event_loop.dispatch(Some(Duration::from_millis(50)), &mut pre_init);
|
||
} else {
|
||
// Final round exhausted; log diagnostics for each remaining candidate.
|
||
for gpu_path in &transient_pending {
|
||
let perms_hint = match std::fs::metadata(gpu_path) {
|
||
Ok(meta) => {
|
||
use std::os::unix::fs::MetadataExt;
|
||
format!("(device node mode={:#o}, uid={}, gid={})",
|
||
meta.mode(), meta.uid(), meta.gid())
|
||
}
|
||
Err(e) => format!("(could not stat device: {e})"),
|
||
};
|
||
open_errors.push(format!(
|
||
"{}: transient open error persisted after {} retries {}. \
|
||
If this is EACCES, ensure Ly's PAM config includes pam_systemd.so \
|
||
and the logind session is active (`loginctl session-status`)",
|
||
gpu_path.display(),
|
||
MAX_TRANSIENT_ROUNDS,
|
||
perms_hint,
|
||
));
|
||
}
|
||
}
|
||
}
|
||
|
||
let gpu_path = chosen_gpu_path.ok_or_else(|| {
|
||
let detail = if open_errors.is_empty() {
|
||
"no candidates produced a detailed error".to_string()
|
||
} else {
|
||
open_errors.join("; ")
|
||
};
|
||
anyhow::anyhow!(
|
||
"no usable DRM device found. Tried: {detail}. Check for stale holders with: fuser /dev/dri/card*"
|
||
)
|
||
})?;
|
||
let drm_fd = chosen_drm_fd.context("no usable DRM device fd found")?;
|
||
|
||
let drm_device_fd = DrmDeviceFd::new(DeviceFd::from(drm_fd));
|
||
|
||
// DRM master management: DrmDeviceFd::new() already attempts to acquire
|
||
// master internally. On logind-managed sessions (Ly, greetd, etc.) this
|
||
// call typically fails because logind manages master status via TakeDevice.
|
||
// The fd is still usable for modesetting—logind grants master implicitly.
|
||
// Following niri's approach: do NOT explicitly call acquire_master_lock()
|
||
// here. Instead, rely on DrmDevice::pause()/activate() for VT switching,
|
||
// which checks is_privileged() and skips redundant master management on
|
||
// logind sessions.
|
||
|
||
// Create the DRM device. The second parameter (`disable_connectors`) is
|
||
// false following niri's approach: preserve the existing connector state
|
||
// instead of resetting it. Resetting (true) requires DRM master and can
|
||
// fail on logind-managed sessions in unprivileged mode.
|
||
//
|
||
// Atomic vs legacy modesetting is auto-detected by smithay based on driver
|
||
// capability. Use SMITHAY_USE_LEGACY=1 to force legacy mode if needed.
|
||
let (mut drm, _drm_notifier) = DrmDevice::new(drm_device_fd.clone(), false)
|
||
.map_err(|e| anyhow::anyhow!("creating DRM device: {e}"))?;
|
||
info!(
|
||
atomic = drm.is_atomic(),
|
||
"DRM device created"
|
||
);
|
||
|
||
let gbm_device = GbmDevice::new(drm_device_fd.clone()).context("creating GBM device")?;
|
||
|
||
let egl_display =
|
||
unsafe { EGLDisplay::new(gbm_device.clone()) }.context("creating EGL display")?;
|
||
let egl_context = EGLContext::new(&egl_display).context("creating EGL context")?;
|
||
let renderer: GlesRenderer =
|
||
unsafe { GlesRenderer::new(egl_context).context("creating GLES renderer")? };
|
||
|
||
let resources = drm.resource_handles().context("DRM resource handles")?;
|
||
let connector_h = resources
|
||
.connectors()
|
||
.iter()
|
||
.find_map(|&h| {
|
||
let info = drm.get_connector(h, false).ok()?;
|
||
if info.state() == connector::State::Connected {
|
||
Some(h)
|
||
} else {
|
||
None
|
||
}
|
||
})
|
||
.context("no connected DRM connector")?;
|
||
|
||
let connector_info = drm.get_connector(connector_h, true)?;
|
||
let &mode = connector_info
|
||
.modes()
|
||
.iter()
|
||
.max_by_key(|m| (m.mode_type().contains(ModeTypeFlags::PREFERRED), m.size()))
|
||
.context("no DRM mode")?;
|
||
|
||
let crtc_h = connector_info
|
||
.encoders()
|
||
.iter()
|
||
.filter_map(|&encoder_h| drm.get_encoder(encoder_h).ok())
|
||
.find_map(|encoder_info| encoder_info.crtc())
|
||
.or_else(|| {
|
||
connector_info
|
||
.encoders()
|
||
.iter()
|
||
.filter_map(|&encoder_h| drm.get_encoder(encoder_h).ok())
|
||
.flat_map(|encoder_info| resources.filter_crtcs(encoder_info.possible_crtcs()))
|
||
.next()
|
||
})
|
||
.context("no suitable CRTC for connected connector")?;
|
||
|
||
let drm_surface = drm
|
||
.create_surface(crtc_h, mode, &[connector_h])
|
||
.context("creating DRM surface")?;
|
||
|
||
let (w, h) = mode.size();
|
||
let (width, height) = (w as u32, h as u32);
|
||
|
||
let output = Output::new(
|
||
"wo-output".to_string(),
|
||
PhysicalProperties {
|
||
size: (0, 0).into(),
|
||
subpixel: Subpixel::Unknown,
|
||
make: "Wo".into(),
|
||
model: "Virtual".into(),
|
||
},
|
||
);
|
||
|
||
let output_mode = OutputMode {
|
||
size: (width as i32, height as i32).into(),
|
||
refresh: mode.vrefresh() as i32 * 1000,
|
||
};
|
||
|
||
output.change_current_state(
|
||
Some(output_mode),
|
||
Some(Transform::Normal),
|
||
Some(Scale::Integer(1)),
|
||
Some((0, 0).into()),
|
||
);
|
||
output.set_preferred(output_mode);
|
||
output.create_global::<WoState>(&dh);
|
||
|
||
// Get renderer formats before moving renderer into BackendData
|
||
let renderer_formats = renderer.dmabuf_formats();
|
||
|
||
let backend = BackendData {
|
||
output: output.clone(),
|
||
size: (width, height),
|
||
renderer: Some(std::sync::Arc::new(std::sync::Mutex::new(renderer))),
|
||
dmabuf_formats: None,
|
||
};
|
||
let render_node =
|
||
DrmNode::from_path(&gpu_path).context("resolving DRM node for dmabuf feedback")?;
|
||
|
||
// Merge [[root]] entries into config.windows BEFORE WoState::new so that
|
||
// render_frame, window_mapped, and respawn logic all see them.
|
||
for root_cfg in &config.root.clone() {
|
||
let mut full = root_cfg.clone();
|
||
full.x = 0;
|
||
full.y = 0;
|
||
full.width = width;
|
||
full.height = height;
|
||
full.z_order = std::i32::MIN / 2;
|
||
config.windows.push(full);
|
||
}
|
||
|
||
let mut state = WoState::new(&mut display, config.clone(), render_node, backend);
|
||
state.can_switch_vt = true;
|
||
|
||
let (ipc, msg_rx) = ElectronIpc::listen(&config.compositor.ipc_socket).context("IPC socket")?;
|
||
|
||
state.electron_ipc = Some(ipc);
|
||
|
||
// Spawn XWayland for X11 client support (mirrors nested.rs)
|
||
state.xwayland_shell_state = Some(XWaylandShellState::new::<WoState>(&dh));
|
||
|
||
match XWayland::spawn(
|
||
&dh,
|
||
None,
|
||
std::iter::empty::<(String, String)>(),
|
||
true,
|
||
std::process::Stdio::null(),
|
||
std::process::Stdio::inherit(),
|
||
|_| {},
|
||
) {
|
||
Ok((xwayland, xw_client)) => {
|
||
let wm_handle = loop_handle.clone();
|
||
let xw_client = std::rc::Rc::new(std::cell::RefCell::new(Some(xw_client)));
|
||
let xw_client_cb = xw_client.clone();
|
||
|
||
if let Err(e) =
|
||
loop_handle.insert_source(xwayland, move |event, _, drm_data: &mut DrmLoopData| {
|
||
let data = drm_data.0.as_mut().expect("EventLoopData initialized");
|
||
match event {
|
||
XWaylandEvent::Ready {
|
||
x11_socket,
|
||
display_number,
|
||
} => {
|
||
info!("XWayland ready on DISPLAY :{display_number}");
|
||
std::env::set_var("DISPLAY", format!(":{display_number}"));
|
||
crate::state::propagate_environment(&["DISPLAY", "WAYLAND_DISPLAY"]);
|
||
// Broadcast DISPLAY to already-running Electron clients
|
||
if let Some(ref ipc) = data.state.electron_ipc {
|
||
let vars = format!("{{\"DISPLAY\":\":{display_number}\"}}");
|
||
if let Err(e) = ipc.broadcast_env_update(&vars) {
|
||
warn!("Failed to broadcast DISPLAY to Electron: {e:#}");
|
||
}
|
||
}
|
||
if let Some(client) = xw_client_cb.borrow_mut().take() {
|
||
match X11Wm::start_wm(wm_handle.clone(), x11_socket, client) {
|
||
Ok(wm) => {
|
||
data.state.xwm = Some(wm);
|
||
info!("X11 window manager attached");
|
||
}
|
||
Err(e) => error!("X11Wm::start_wm failed: {e:#}"),
|
||
}
|
||
}
|
||
}
|
||
XWaylandEvent::Error => {
|
||
warn!("XWayland server encountered an error during startup");
|
||
}
|
||
}
|
||
})
|
||
{
|
||
error!("Failed to register XWayland event source: {e:#}");
|
||
} else {
|
||
info!("XWayland server starting");
|
||
}
|
||
}
|
||
Err(e) => {
|
||
warn!("XWayland spawn failed (X11 support disabled): {e:#}");
|
||
}
|
||
}
|
||
|
||
std::env::set_var("WAYLAND_DISPLAY", &config.compositor.socket_name);
|
||
// Propagate now; DISPLAY will be re-propagated once XWayland is ready.
|
||
crate::state::propagate_environment(&["WAYLAND_DISPLAY"]);
|
||
|
||
let app_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("electron");
|
||
let electron_render_node = render_node_for_card(&gpu_path);
|
||
|
||
// All windows (including merged [[root]] entries) are now in config.windows.
|
||
for win_cfg in &config.windows {
|
||
match ElectronProcess::spawn(
|
||
win_cfg,
|
||
&config.compositor.electron_path,
|
||
&app_dir,
|
||
&config.compositor.ipc_socket,
|
||
&electron_render_node,
|
||
) {
|
||
Ok(proc) => state.electron_processes.push(proc),
|
||
Err(e) => error!(window = %win_cfg.name, "failed to spawn Electron: {e:#}"),
|
||
}
|
||
}
|
||
|
||
let mut autostart_tasks: Vec<(crate::config::AutostartConfig, std::time::Instant)> = Vec::new();
|
||
let now = std::time::Instant::now();
|
||
for task in &config.autostart {
|
||
let when = now + std::time::Duration::from_millis(task.delay);
|
||
autostart_tasks.push((task.clone(), when));
|
||
}
|
||
let swapchain_format = if renderer_formats
|
||
.iter()
|
||
.any(|fmt| fmt.code == Fourcc::Xrgb8888)
|
||
{
|
||
Fourcc::Xrgb8888
|
||
} else if renderer_formats
|
||
.iter()
|
||
.any(|fmt| fmt.code == Fourcc::Argb8888)
|
||
{
|
||
Fourcc::Argb8888
|
||
} else {
|
||
renderer_formats
|
||
.iter()
|
||
.next()
|
||
.map(|fmt| fmt.code)
|
||
.unwrap_or(Fourcc::Xrgb8888)
|
||
};
|
||
|
||
let mut swapchain_modifiers: Vec<Modifier> = Vec::new();
|
||
for fmt in renderer_formats.iter().filter(|fmt| fmt.code == swapchain_format) {
|
||
if !swapchain_modifiers.contains(&fmt.modifier) {
|
||
swapchain_modifiers.push(fmt.modifier);
|
||
}
|
||
}
|
||
if swapchain_modifiers.is_empty() {
|
||
swapchain_modifiers.push(Modifier::Invalid);
|
||
}
|
||
|
||
info!(
|
||
?swapchain_format,
|
||
modifiers = swapchain_modifiers.len(),
|
||
"using swapchain format/modifiers from renderer"
|
||
);
|
||
|
||
let swapchain: Swapchain<GbmAllocator<DrmDeviceFd>> = Swapchain::new(
|
||
GbmAllocator::new(
|
||
gbm_device,
|
||
GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT,
|
||
),
|
||
width,
|
||
height,
|
||
swapchain_format,
|
||
swapchain_modifiers,
|
||
);
|
||
|
||
let output_rect: Rectangle<i32, Physical> =
|
||
Rectangle::new((0, 0).into(), (width as i32, height as i32).into());
|
||
let src_rect: Rectangle<f64, Buffer> =
|
||
Rectangle::from_size((width as i32, height as i32).into()).to_f64();
|
||
let bg = Color32F::from(config.compositor.background);
|
||
|
||
// Initialize libinput for input events (only when session backend is available)
|
||
let mut libinput_context =
|
||
Libinput::new_with_udev(LibinputSessionInterface::from(session.clone()));
|
||
libinput_context
|
||
.udev_assign_seat(&session.seat())
|
||
.map_err(|()| anyhow::anyhow!("Failed to assign seat to libinput"))?;
|
||
// Keep a clone for suspend/resume on VT switch; the original is consumed by the backend.
|
||
let libinput_context_for_vt = libinput_context.clone();
|
||
let libinput_backend = LibinputInputBackend::new(libinput_context);
|
||
|
||
// Prepare frame pacing using the active DRM mode refresh when available.
|
||
let refresh_hz = (output_mode.refresh as f64) / 1000.0;
|
||
let frame_time = if refresh_hz > 1.0 {
|
||
Duration::from_secs_f64((1.0 / refresh_hz).clamp(1.0 / 240.0, 1.0 / 30.0))
|
||
} else {
|
||
Duration::from_millis(16)
|
||
};
|
||
let last_render = std::time::Instant::now();
|
||
|
||
let wayland_socket =
|
||
ListeningSocket::bind(&config.compositor.socket_name).context("binding Wayland socket")?;
|
||
info!(socket = %config.compositor.socket_name, "Wayland socket bound");
|
||
|
||
// Register the Wayland display fd with calloop so that client messages
|
||
// (surface commits, etc.) wake the event loop. Without this, the loop
|
||
// only wakes on DRM VBlank / libinput / session events and rendering
|
||
// stalls when the VBlank cycle breaks.
|
||
{
|
||
use std::os::unix::io::AsFd;
|
||
let display_fd = display.as_fd().try_clone_to_owned()
|
||
.context("duping Wayland display fd for calloop")?;
|
||
let wayland_display_source = calloop::generic::Generic::new(
|
||
display_fd,
|
||
calloop::Interest::READ,
|
||
calloop::Mode::Level,
|
||
);
|
||
loop_handle
|
||
.insert_source(wayland_display_source, |_, _, _: &mut DrmLoopData| {
|
||
Ok(calloop::PostAction::Continue)
|
||
})
|
||
.map_err(|e| anyhow::anyhow!("Error inserting Wayland display source: {e:?}"))?;
|
||
info!("Wayland display fd registered with calloop");
|
||
}
|
||
|
||
// Register the listening socket fd so new client connections also wake
|
||
// the event loop promptly.
|
||
{
|
||
use std::os::unix::io::AsFd;
|
||
let listen_fd = wayland_socket.as_fd().try_clone_to_owned()
|
||
.context("duping Wayland listening socket fd for calloop")?;
|
||
let listen_source = calloop::generic::Generic::new(
|
||
listen_fd,
|
||
calloop::Interest::READ,
|
||
calloop::Mode::Level,
|
||
);
|
||
loop_handle
|
||
.insert_source(listen_source, |_, _, _: &mut DrmLoopData| {
|
||
Ok(calloop::PostAction::Continue)
|
||
})
|
||
.map_err(|e| anyhow::anyhow!("Error inserting Wayland listen source: {e:?}"))?;
|
||
info!("Wayland listening socket registered with calloop");
|
||
}
|
||
|
||
// Periodic render timer: safety net to restart stalled VBlank cycles.
|
||
// If the VBlank rendering cycle breaks (e.g. transient page-flip failure),
|
||
// this timer ensures the event loop wakes up and render_frame gets a
|
||
// chance to restart the cycle, even with no input or client activity.
|
||
{
|
||
let render_timer = calloop::timer::Timer::from_duration(frame_time);
|
||
loop_handle
|
||
.insert_source(render_timer, move |_deadline, _, _: &mut DrmLoopData| {
|
||
calloop::timer::TimeoutAction::ToDuration(frame_time)
|
||
})
|
||
.map_err(|e| anyhow::anyhow!("Error inserting render timer: {e:?}"))?;
|
||
info!(?frame_time, "Periodic render timer registered");
|
||
}
|
||
|
||
// Move all state into EventLoopData for access in event loop callbacks
|
||
let mut loop_data = EventLoopData {
|
||
display,
|
||
state,
|
||
msg_rx,
|
||
swapchain,
|
||
drm_device_fd: drm_device_fd.clone(),
|
||
drm,
|
||
drm_surface,
|
||
output_rect,
|
||
src_rect,
|
||
bg,
|
||
width,
|
||
height,
|
||
frame_time,
|
||
last_render,
|
||
autostart_tasks,
|
||
first_render: true,
|
||
wayland_socket,
|
||
frame_timestamps: std::collections::HashMap::new(),
|
||
input_events_pending: false,
|
||
session,
|
||
active: true,
|
||
vblank_pending: false,
|
||
vblank_since: None,
|
||
consecutive_flip_failures: 0,
|
||
gles_tex_cache: std::collections::HashMap::new(),
|
||
electron_render_node: electron_render_node,
|
||
total_flips: 0,
|
||
current_slot: None,
|
||
pending_slot: None,
|
||
libinput_context: libinput_context_for_vt,
|
||
};
|
||
|
||
let signal_for_handler = loop_signal.clone();
|
||
ctrlc::set_handler(move || {
|
||
info!("Received Ctrl+C, initiating shutdown");
|
||
signal_for_handler.stop();
|
||
})
|
||
.expect("Error setting Ctrl-C handler");
|
||
|
||
loop_handle
|
||
.insert_source(_drm_notifier, |event, _, drm_data: &mut DrmLoopData| {
|
||
let data = drm_data.0.as_mut().expect("EventLoopData initialized");
|
||
use smithay::backend::drm::DrmEvent;
|
||
if !data.active {
|
||
if let DrmEvent::VBlank(_) = event {
|
||
data.vblank_pending = false;
|
||
data.vblank_since = None;
|
||
data.current_slot = data.pending_slot.take();
|
||
}
|
||
trace!("Ignoring DRM event while session is inactive");
|
||
return;
|
||
}
|
||
if let DrmEvent::VBlank(_) = event {
|
||
// The flip is complete: the pending buffer is now being scanned.
|
||
// Promote it to current (releasing the old scanout buffer).
|
||
data.vblank_pending = false;
|
||
data.vblank_since = None;
|
||
data.current_slot = data.pending_slot.take();
|
||
render_frame(data);
|
||
}
|
||
})
|
||
.map_err(|e| anyhow::anyhow!("Error inserting DRM notifier: {e:?}"))?;
|
||
|
||
// Insert libinput backend into event loop (operate on `loop_data.state`)
|
||
loop_handle
|
||
.insert_source(
|
||
libinput_backend,
|
||
move |event, _, drm_data: &mut DrmLoopData| {
|
||
let data = drm_data.0.as_mut().expect("EventLoopData initialized");
|
||
data.state.process_input_event(event);
|
||
data.input_events_pending = true;
|
||
},
|
||
)
|
||
.map_err(|e| anyhow::anyhow!("Error inserting libinput source: {e:?}"))?;
|
||
|
||
// Kickstart the render loop with an initial frame
|
||
info!("Kickstarting VBlank-driven rendering");
|
||
render_frame(&mut loop_data);
|
||
|
||
// Main event loop - purely event-driven (blocking)
|
||
let mut main_data = DrmLoopData(Some(loop_data));
|
||
event_loop
|
||
.run(None, &mut main_data, |drm_data| {
|
||
let data = drm_data.0.as_mut().expect("EventLoopData initialized");
|
||
if !data.state.running {
|
||
loop_signal.stop();
|
||
return;
|
||
}
|
||
|
||
if let Some(vt) = data.state.pending_vt_switch.take() {
|
||
if let Err(e) = data.session.change_vt(vt as i32) {
|
||
warn!(vt, "Failed to switch VT: {e}");
|
||
} else {
|
||
info!(vt, "Switched to VT");
|
||
}
|
||
}
|
||
|
||
while let Ok(Some(stream)) = data.wayland_socket.accept() {
|
||
if let Err(e) = data.display.handle().insert_client(
|
||
stream,
|
||
std::sync::Arc::new(crate::state::ClientData::default()),
|
||
) {
|
||
error!("Failed to insert Wayland client: {e}");
|
||
}
|
||
}
|
||
if let Err(e) = data.display.dispatch_clients(&mut data.state) {
|
||
error!("Failed to dispatch Wayland clients: {e}");
|
||
}
|
||
if let Err(e) = data.display.flush_clients() {
|
||
error!("Failed to flush Wayland clients: {e}");
|
||
}
|
||
data.state.space.refresh();
|
||
if let Err(e) = data.display.flush_clients() {
|
||
error!("Failed to flush after space refresh: {e}");
|
||
}
|
||
|
||
// Process IPC messages with input prioritization and frame backpressure
|
||
let mut input_batch = Vec::new();
|
||
let mut frames_batch = Vec::new();
|
||
let mut other_batch = Vec::new();
|
||
|
||
while let Ok(msg) = data.msg_rx.try_recv() {
|
||
use crate::electron::ElectronMessage;
|
||
match msg {
|
||
ElectronMessage::ForwardedPointer { .. }
|
||
| ElectronMessage::ForwardedKeyboard { .. }
|
||
| ElectronMessage::ForwardedRelativePointer { .. }
|
||
| ElectronMessage::ForwardedPointerButton { .. }
|
||
| ElectronMessage::ForwardedPointerScroll { .. } => {
|
||
input_batch.push(msg);
|
||
}
|
||
ElectronMessage::Frame(_) => {
|
||
frames_batch.push(msg);
|
||
}
|
||
_ => {
|
||
other_batch.push(msg);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Process input events first for low latency
|
||
for msg in input_batch {
|
||
use crate::electron::ElectronMessage;
|
||
match msg {
|
||
ElectronMessage::ForwardedPointer { window_name, x, y } => {
|
||
data.state.handle_forwarded_pointer_event(&window_name, x, y);
|
||
}
|
||
ElectronMessage::ForwardedKeyboard { window_name, key, pressed, time } => {
|
||
data.state.handle_forwarded_keyboard_event(&window_name, key, pressed, time);
|
||
}
|
||
ElectronMessage::ForwardedRelativePointer { window_name, dx, dy } => {
|
||
data.state.handle_forwarded_relative_pointer_event(&window_name, dx, dy);
|
||
}
|
||
ElectronMessage::ForwardedPointerButton { window_name, x, y, button, pressed, time } => {
|
||
data.state.handle_forwarded_pointer_button(&window_name, x, y, button, pressed, time);
|
||
}
|
||
ElectronMessage::ForwardedPointerScroll { window_name, dx, dy } => {
|
||
data.state.handle_forwarded_pointer_scroll(&window_name, dx, dy);
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
// Coalesce frames: keep only the latest frame per window.
|
||
// Previous approach hard-dropped frames within 8ms which could
|
||
// lose the newest frame when a burst arrived. Now we always
|
||
// import the freshest frame; intermediates are simply overwritten.
|
||
let mut latest_frames = std::collections::HashMap::new();
|
||
for msg in frames_batch {
|
||
if let crate::electron::ElectronMessage::Frame(frame) = msg {
|
||
let name = frame.name.clone();
|
||
latest_frames.insert(name, frame);
|
||
}
|
||
}
|
||
|
||
// Import latest frames (non-blocking: try_lock avoids stalling
|
||
// the event loop when the renderer is busy with a page flip).
|
||
for (_name, frame) in latest_frames {
|
||
let frame_name = frame.name.clone();
|
||
let frame_dims = (frame.width, frame.height);
|
||
if let Some(renderer_arc) = data.state.backend.renderer() {
|
||
match renderer_arc.try_lock() {
|
||
Ok(ref mut renderer) => {
|
||
match import_electron_frame(renderer, frame) {
|
||
Ok((tex, cached)) => {
|
||
let is_first = !data.gles_tex_cache.contains_key(&frame_name);
|
||
data.state.damaged_windows.insert(frame_name.clone());
|
||
data.state.texture_cache.insert_dmabuf(cached);
|
||
data.gles_tex_cache.insert(frame_name.clone(), tex.texture);
|
||
if is_first {
|
||
info!(
|
||
"First DMABUF imported for '{}' ({}x{}, total_textures={})",
|
||
frame_name, frame_dims.0, frame_dims.1,
|
||
data.gles_tex_cache.len(),
|
||
);
|
||
}
|
||
}
|
||
Err(e) => warn!("DMABUF import failed for '{}': {e:#}", frame_name),
|
||
}
|
||
}
|
||
Err(_) => {
|
||
// Renderer is held by the render path; the cached
|
||
// texture from the previous frame is still valid so
|
||
// the display won't flicker. The next event-loop
|
||
// tick will pick up any newer frame.
|
||
trace!("renderer busy, deferring frame import for '{}'", frame_name);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Evict textures for windows no longer in the DMABUF cache
|
||
data.gles_tex_cache.retain(|name, _| data.state.texture_cache.get_dmabuf(name).is_some());
|
||
|
||
// Process other messages
|
||
for msg in other_batch {
|
||
use crate::electron::ElectronMessage;
|
||
match msg {
|
||
ElectronMessage::WindowPosition(pos) => {
|
||
trace!(
|
||
"Window position: {} at ({}, {})",
|
||
pos.window_name, pos.x, pos.y
|
||
);
|
||
data.state.window_positions.insert(
|
||
pos.window_name.clone(),
|
||
(pos.x, pos.y, pos.width, pos.height),
|
||
);
|
||
data.state.metadata_dirty = true;
|
||
}
|
||
ElectronMessage::Syscall(req) => {
|
||
if let Some(ref handler) = data.state.syscall_handler {
|
||
trace!("Syscall from {}: {}", req.window_name, req.syscall_type);
|
||
|
||
if let Ok(syscall_req) =
|
||
serde_json::from_str::<crate::syscall::SyscallRequest>(&req.payload)
|
||
{
|
||
let response = handler.handle(syscall_req);
|
||
let response_json = serde_json::to_string(&response)
|
||
.unwrap_or_else(|_| "{}".to_string());
|
||
|
||
if let Some(ref ipc) = data.state.electron_ipc {
|
||
let _ =
|
||
ipc.send_syscall_response(&req.window_name, &response_json);
|
||
}
|
||
} else {
|
||
warn!("Failed to parse syscall payload: {}", req.payload);
|
||
}
|
||
} else {
|
||
warn!("Syscall requested but syscalls are disabled");
|
||
}
|
||
}
|
||
ElectronMessage::Action(action_msg) => {
|
||
use crate::electron::CompositorAction;
|
||
info!(
|
||
"Action from {}: {:?}",
|
||
action_msg.window_name, action_msg.action
|
||
);
|
||
|
||
match action_msg.action {
|
||
CompositorAction::Quit { code } => {
|
||
info!("Quit requested with code {}", code);
|
||
data.state.running = false;
|
||
}
|
||
CompositorAction::Custom {
|
||
action: act_str,
|
||
payload,
|
||
} => {
|
||
info!("Custom action '{}': <payload>", act_str);
|
||
|
||
let payload_json: Option<serde_json::Value> = payload
|
||
.as_ref()
|
||
.and_then(|p| serde_json::from_str(p).ok());
|
||
|
||
let target_name: Option<String> = payload_json
|
||
.as_ref()
|
||
.and_then(|p| p.get("window"))
|
||
.and_then(|v| v.as_str())
|
||
.map(|s| s.to_string())
|
||
.or_else(|| {
|
||
if action_msg.window_name.starts_with("wayland-") {
|
||
Some(action_msg.window_name.clone())
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
|
||
let wayland_window = target_name.as_ref()
|
||
.and_then(|name| data.state.find_wayland_window(name));
|
||
|
||
if let Some(window) = wayland_window {
|
||
let th = data.state.title_h_for_window(&window);
|
||
match act_str.as_str() {
|
||
"move" => {
|
||
let x = payload_json.as_ref()
|
||
.and_then(|p| p.get("x"))
|
||
.and_then(|v| v.as_i64())
|
||
.unwrap_or(0) as i32;
|
||
let y = payload_json.as_ref()
|
||
.and_then(|p| p.get("y"))
|
||
.and_then(|v| v.as_i64())
|
||
.unwrap_or(0) as i32;
|
||
data.state.space.map_element(window, (x, y + th), false);
|
||
}
|
||
"resize" => {
|
||
let new_w = payload_json.as_ref()
|
||
.and_then(|p| p.get("width"))
|
||
.and_then(|v| v.as_i64())
|
||
.unwrap_or(0) as i32;
|
||
let new_h = payload_json.as_ref()
|
||
.and_then(|p| p.get("height"))
|
||
.and_then(|v| v.as_i64())
|
||
.unwrap_or(0) as i32;
|
||
if let Some(toplevel) = window.toplevel() {
|
||
let content_h = (new_h - th).max(1);
|
||
let (cw, ch) = data.state.clamp_to_size_hints(&window, new_w, content_h);
|
||
toplevel.with_pending_state(|s| {
|
||
s.size = Some((cw, ch).into());
|
||
});
|
||
toplevel.send_configure();
|
||
}
|
||
}
|
||
"close" => {
|
||
if let Some(toplevel) = window.toplevel() {
|
||
toplevel.send_close();
|
||
}
|
||
data.state.space.unmap_elem(&window);
|
||
data.state.all_windows.retain(|w| w != &window);
|
||
if let Some(name) = target_name.as_ref() {
|
||
data.state.window_mapped.insert(name.clone(), false);
|
||
}
|
||
data.state.refocus_topmost_native_window();
|
||
data.state.metadata_dirty = true;
|
||
}
|
||
"focus" => {
|
||
let surface = window
|
||
.toplevel()
|
||
.map(|t| t.wl_surface().clone())
|
||
.or_else(|| {
|
||
window
|
||
.x11_surface()
|
||
.and_then(|x| x.wl_surface().clone())
|
||
});
|
||
|
||
if let Some(surface) = surface {
|
||
if data.state.space.element_location(&window).is_none() {
|
||
use smithay::wayland::compositor::with_states;
|
||
let parent_surface = window.toplevel().and_then(|t| {
|
||
with_states(t.wl_surface(), |states| {
|
||
states.data_map
|
||
.get::<smithay::wayland::shell::xdg::XdgToplevelSurfaceData>()
|
||
.and_then(|d| d.lock().ok().and_then(|data| data.parent.clone()))
|
||
})
|
||
});
|
||
let (ow, oh) = (
|
||
data.state.output_size.0 as i32,
|
||
data.state.output_size.1 as i32,
|
||
);
|
||
let location = if let Some(ref parent_wl) = parent_surface {
|
||
let parent_center = data
|
||
.state
|
||
.space
|
||
.elements()
|
||
.find(|w| {
|
||
w.toplevel()
|
||
.map(|t| t.wl_surface() == parent_wl)
|
||
.unwrap_or(false)
|
||
})
|
||
.map(|parent_win| {
|
||
let loc = data
|
||
.state
|
||
.space
|
||
.element_location(parent_win)
|
||
.unwrap_or_default();
|
||
let geo = parent_win.geometry();
|
||
(
|
||
loc.x + geo.size.w / 2,
|
||
loc.y + geo.size.h / 2,
|
||
)
|
||
})
|
||
.unwrap_or_else(|| (ow / 2, oh / 2));
|
||
let dialog_y = (parent_center.1 - 75)
|
||
.max(handlers::xdg_shell::PANEL_H);
|
||
(parent_center.0 - 200, dialog_y)
|
||
} else {
|
||
let suggest_w = (ow * 2 / 3).max(640).min(1600);
|
||
let suggest_h = (oh * 2 / 3).max(480).min(1000);
|
||
let center_y = ((oh - suggest_h) / 2)
|
||
.max(handlers::xdg_shell::PANEL_H + 20);
|
||
((ow - suggest_w) / 2, center_y)
|
||
};
|
||
let tname = target_name.clone().unwrap();
|
||
data.state.window_mapped.insert(tname, true);
|
||
data.state.space.map_element(window.clone(), location, true);
|
||
data.state.metadata_dirty = true;
|
||
}
|
||
|
||
// Use xdg-activation for xdg toplevels; directly set
|
||
// keyboard focus for X11 windows.
|
||
if window.toplevel().is_some() {
|
||
use smithay::wayland::compositor::with_states;
|
||
let token = XdgActivationToken::from(format!(
|
||
"electron-focus-{}",
|
||
Instant::now().elapsed().as_millis()
|
||
));
|
||
let token_data = XdgActivationTokenData {
|
||
surface: Some(surface.clone()),
|
||
serial: None,
|
||
timestamp: Instant::now(),
|
||
app_id: window.toplevel().and_then(|t| {
|
||
with_states(t.wl_surface(), |states| {
|
||
states
|
||
.data_map
|
||
.get::<smithay::wayland::shell::xdg::XdgToplevelSurfaceData>()
|
||
.and_then(|d| {
|
||
d.lock().ok().and_then(
|
||
|data| data.app_id.clone(),
|
||
)
|
||
})
|
||
})
|
||
}),
|
||
client_id: None,
|
||
user_data: std::sync::Arc::new(
|
||
smithay::utils::user_data::UserDataMap::new(),
|
||
),
|
||
};
|
||
data.state.request_activation(
|
||
token,
|
||
token_data,
|
||
surface.clone(),
|
||
);
|
||
|
||
if let Some(keyboard) = data.state.seat.get_keyboard() {
|
||
let serial = smithay::utils::SERIAL_COUNTER.next_serial();
|
||
keyboard.set_focus(
|
||
&mut data.state,
|
||
Some(surface.clone()),
|
||
serial,
|
||
);
|
||
}
|
||
|
||
if let Some(name) = target_name.as_ref() {
|
||
data.state.keyboard_window_focus = Some(name.clone());
|
||
data.state.pointer_window_focus = Some(name.clone());
|
||
}
|
||
} else if let Some(keyboard) = data.state.seat.get_keyboard() {
|
||
let serial = smithay::utils::SERIAL_COUNTER.next_serial();
|
||
info!("Focus action: setting keyboard focus to X11 window: {:?}", target_name);
|
||
keyboard.set_focus(
|
||
&mut data.state,
|
||
Some(surface.clone()),
|
||
serial,
|
||
);
|
||
|
||
if let Some(name) = target_name.as_ref() {
|
||
data.state.keyboard_window_focus = Some(name.clone());
|
||
data.state.pointer_window_focus = Some(name.clone());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
"minimize" => {
|
||
data.state.space.unmap_elem(&window);
|
||
data.state.window_mapped.insert(target_name.clone().unwrap(), false);
|
||
data.state.refocus_topmost_native_window();
|
||
data.state.metadata_dirty = true;
|
||
}
|
||
"maximize" => {
|
||
if let Some(toplevel) = window.toplevel() {
|
||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State as XdgState;
|
||
use wo::handlers::xdg_shell::PANEL_H;
|
||
let (ow, oh) = (data.state.output_size.0 as i32, data.state.output_size.1 as i32);
|
||
let y_offset = th.max(PANEL_H);
|
||
let content_h = (oh - y_offset).max(1);
|
||
toplevel.with_pending_state(|s| {
|
||
s.states.set(XdgState::Maximized);
|
||
s.size = Some((ow, content_h).into());
|
||
});
|
||
toplevel.send_configure();
|
||
data.state.space.map_element(window.clone(), (0, y_offset), false);
|
||
}
|
||
}
|
||
"pointer_motion" => {
|
||
let x = payload_json.as_ref().and_then(|p| p.get("x")).and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||
let y = payload_json.as_ref().and_then(|p| p.get("y")).and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||
let tname = target_name.as_deref().unwrap_or("");
|
||
data.state.handle_forwarded_pointer_event(tname, x, y);
|
||
}
|
||
"pointer_button" => {
|
||
let x = payload_json.as_ref().and_then(|p| p.get("x")).and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||
let y = payload_json.as_ref().and_then(|p| p.get("y")).and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||
let button = payload_json.as_ref().and_then(|p| p.get("button")).and_then(|v| v.as_u64()).unwrap_or(272) as u32;
|
||
let pressed = payload_json.as_ref().and_then(|p| p.get("pressed")).and_then(|v| v.as_bool()).unwrap_or(false);
|
||
let tname = target_name.as_deref().unwrap_or("");
|
||
data.state.handle_forwarded_pointer_button(tname, x, y, button, pressed, 0);
|
||
}
|
||
"pointer_scroll" => {
|
||
let dx = payload_json.as_ref().and_then(|p| p.get("dx")).and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||
let dy = payload_json.as_ref().and_then(|p| p.get("dy")).and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||
let tname = target_name.as_deref().unwrap_or("");
|
||
data.state.handle_forwarded_pointer_scroll(tname, dx, dy);
|
||
}
|
||
"pointer_leave" => {
|
||
use smithay::input::pointer::MotionEvent;
|
||
if let Some(pointer) = data.state.seat.get_pointer() {
|
||
let serial = smithay::utils::SERIAL_COUNTER.next_serial();
|
||
let loc = data.state.pointer_location;
|
||
pointer.motion(&mut data.state, None, &MotionEvent {
|
||
location: loc,
|
||
serial,
|
||
time: 0,
|
||
});
|
||
pointer.frame(&mut data.state);
|
||
}
|
||
}
|
||
"keyboard_key" => {
|
||
let keycode = payload_json
|
||
.as_ref()
|
||
.and_then(|p| p.get("keycode"))
|
||
.and_then(|v| v.as_u64())
|
||
.unwrap_or(0) as u32;
|
||
let pressed = payload_json
|
||
.as_ref()
|
||
.and_then(|p| p.get("pressed"))
|
||
.and_then(|v| v.as_bool())
|
||
.unwrap_or(false);
|
||
let key_state = if pressed {
|
||
smithay::backend::input::KeyState::Pressed
|
||
} else {
|
||
smithay::backend::input::KeyState::Released
|
||
};
|
||
let xkb_keycode: u32 = keycode + 8;
|
||
let time = std::time::Instant::now()
|
||
.duration_since(data.last_render)
|
||
.as_millis() as u32;
|
||
if let Some(keyboard) = data.state.seat.get_keyboard() {
|
||
let serial = smithay::utils::SERIAL_COUNTER.next_serial();
|
||
keyboard.input::<(), _>(
|
||
&mut data.state,
|
||
xkb_keycode.into(),
|
||
key_state,
|
||
serial,
|
||
time,
|
||
|_, _, _| smithay::input::keyboard::FilterResult::Forward,
|
||
);
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
} else {
|
||
// Electron window actions (existing logic)
|
||
match act_str.as_str() {
|
||
"focus" => {
|
||
data.state
|
||
.window_mapped
|
||
.insert(action_msg.window_name.clone(), true);
|
||
// Give keyboard and pointer logical focus to this window
|
||
data.state.keyboard_window_focus = Some(action_msg.window_name.clone());
|
||
data.state.pointer_window_focus = Some(action_msg.window_name.clone());
|
||
data.state.metadata_dirty = true;
|
||
}
|
||
"map" => {
|
||
data.state
|
||
.window_mapped
|
||
.insert(action_msg.window_name.clone(), true);
|
||
data.state.metadata_dirty = true;
|
||
}
|
||
"unmap" | "minimize" => {
|
||
data.state
|
||
.window_mapped
|
||
.insert(action_msg.window_name.clone(), false);
|
||
// Clear any frontend keyboard/pointer focus targeting this window
|
||
if data.state.keyboard_window_focus.as_deref() == Some(&action_msg.window_name) {
|
||
data.state.keyboard_window_focus = None;
|
||
}
|
||
if data.state.pointer_window_focus.as_deref() == Some(&action_msg.window_name) {
|
||
data.state.pointer_window_focus = None;
|
||
}
|
||
data.state.metadata_dirty = true;
|
||
}
|
||
"close" => {
|
||
if let Some(idx) = data
|
||
.state
|
||
.electron_processes
|
||
.iter()
|
||
.position(|p| p.name == action_msg.window_name)
|
||
{
|
||
let mut proc =
|
||
data.state.electron_processes.remove(idx);
|
||
proc.kill();
|
||
}
|
||
data.state.texture_cache.remove(&action_msg.window_name);
|
||
data.state.window_positions.remove(&action_msg.window_name);
|
||
data.state.window_mapped.remove(&action_msg.window_name);
|
||
// Clear any stored focus referring to this window
|
||
if data.state.keyboard_window_focus.as_deref() == Some(&action_msg.window_name) {
|
||
data.state.keyboard_window_focus = None;
|
||
}
|
||
if data.state.pointer_window_focus.as_deref() == Some(&action_msg.window_name) {
|
||
data.state.pointer_window_focus = None;
|
||
}
|
||
if let Some(ref ipc) = data.state.electron_ipc {
|
||
let _ = ipc
|
||
.clients
|
||
.lock()
|
||
.unwrap()
|
||
.remove(&action_msg.window_name);
|
||
}
|
||
data.state.metadata_dirty = true;
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
if !data.state.running {
|
||
loop_signal.stop();
|
||
return;
|
||
}
|
||
|
||
// Execute any due autostart tasks (scheduled at startup)
|
||
if !data.autostart_tasks.is_empty() {
|
||
let now = std::time::Instant::now();
|
||
let display_ready = std::env::var("DISPLAY").is_ok();
|
||
let mut pending: Vec<(crate::config::AutostartConfig, std::time::Instant)> =
|
||
Vec::new();
|
||
for (task, when) in data.autostart_tasks.drain(..) {
|
||
if when <= now {
|
||
let cmd = task.command.clone();
|
||
|
||
// Defer non-builtin commands until DISPLAY is set
|
||
// (XWayland ready). X11 apps like Steam need it.
|
||
if !display_ready
|
||
&& !cmd.starts_with("wo://")
|
||
{
|
||
pending.push((task, now + std::time::Duration::from_millis(200)));
|
||
continue;
|
||
}
|
||
|
||
// Handle simple wo://window/<name> built-in action
|
||
if let Some(name) = cmd.strip_prefix("wo://window/") {
|
||
if let Some(win_cfg) =
|
||
data.state.config.windows.iter().find(|w| w.name == name)
|
||
{
|
||
match ElectronProcess::spawn(
|
||
win_cfg,
|
||
&data.state.config.compositor.electron_path,
|
||
&Path::new(env!("CARGO_MANIFEST_DIR")).join("electron"),
|
||
&data.state.config.compositor.ipc_socket,
|
||
&data.electron_render_node,
|
||
) {
|
||
Ok(proc) => data.state.electron_processes.push(proc),
|
||
Err(e) => {
|
||
error!(window = %name, "autostart spawn failed: {e:#}")
|
||
}
|
||
}
|
||
} else {
|
||
warn!("autostart: window config not found: {}", name);
|
||
}
|
||
} else if cmd == "wo://portal/start" {
|
||
let portal_bin = std::env::current_exe()
|
||
.ok()
|
||
.and_then(|p| p.parent().map(|d| d.join("wo-portal")))
|
||
.unwrap_or_else(|| std::path::PathBuf::from("wo-portal"));
|
||
match std::process::Command::new(&portal_bin).spawn() {
|
||
Ok(_) => info!("Spawned portal service: {:?}", portal_bin),
|
||
Err(e) => error!("Failed to spawn portal: {e:#}"),
|
||
}
|
||
} else if let Some(path) = cmd.strip_prefix("file://") {
|
||
// Execute script file (path expected absolute or relative)
|
||
let p = path.to_string();
|
||
if task.restart {
|
||
std::thread::spawn(move || loop {
|
||
match std::process::Command::new(&p).spawn() {
|
||
Ok(mut child) => {
|
||
let _ = child.wait();
|
||
}
|
||
Err(e) => {
|
||
eprintln!("autostart exec failed {}: {e}", p);
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
let _ = std::process::Command::new(&p).spawn();
|
||
}
|
||
} else {
|
||
// Generic command: if contains whitespace, run via shell
|
||
if cmd.contains(' ') {
|
||
if task.restart {
|
||
let c = cmd.clone();
|
||
std::thread::spawn(move || loop {
|
||
match std::process::Command::new("sh")
|
||
.arg("-c")
|
||
.arg(&c)
|
||
.spawn()
|
||
{
|
||
Ok(mut child) => {
|
||
let _ = child.wait();
|
||
}
|
||
Err(e) => {
|
||
eprintln!("autostart shell failed {}: {e}", c);
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
let _ = std::process::Command::new("sh")
|
||
.arg("-c")
|
||
.arg(&cmd)
|
||
.spawn();
|
||
}
|
||
} else {
|
||
// Simple binary invocation
|
||
if task.restart {
|
||
let c = cmd.clone();
|
||
std::thread::spawn(move || loop {
|
||
match std::process::Command::new(&c).spawn() {
|
||
Ok(mut child) => {
|
||
let _ = child.wait();
|
||
}
|
||
Err(e) => {
|
||
eprintln!("autostart exec failed {}: {e}", c);
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
let _ = std::process::Command::new(&cmd).spawn();
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
pending.push((task, when));
|
||
}
|
||
}
|
||
data.autostart_tasks = pending;
|
||
}
|
||
|
||
// Poll for exited Electron helper processes (non-blocking) and mark metadata dirty
|
||
if !data.state.electron_processes.is_empty() {
|
||
for idx in (0..data.state.electron_processes.len()).rev() {
|
||
if let Some(pid_u32) = data.state.electron_processes.get(idx).map(|p| p.child.id()) {
|
||
let pid = pid_u32 as u32;
|
||
let npid = nix::unistd::Pid::from_raw(pid as i32);
|
||
match waitpid(Some(npid), Some(WaitPidFlag::WNOHANG)) {
|
||
Ok(WaitStatus::StillAlive) => {}
|
||
Ok(status) => {
|
||
let name = data.state.electron_processes[idx].name.clone();
|
||
info!(window = %name, pid = pid, "Electron process exited: {:?}", status);
|
||
let mut proc = data.state.electron_processes.remove(idx);
|
||
let _ = proc.child.try_wait();
|
||
data.state.metadata_dirty = true;
|
||
}
|
||
Err(e) => {
|
||
warn!("waitpid failed for electron pid {}: {e}", pid);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Send window metadata update to all clients if dirty
|
||
if data.state.metadata_dirty {
|
||
let mut metadata_list = Vec::new();
|
||
|
||
// Electron-managed windows
|
||
for win_cfg in &data.state.config.windows {
|
||
let (x, y, w, h) = data
|
||
.state
|
||
.window_positions
|
||
.get(&win_cfg.name)
|
||
.copied()
|
||
.unwrap_or((win_cfg.x, win_cfg.y, win_cfg.width, win_cfg.height));
|
||
|
||
let mapped = data.state
|
||
.window_mapped
|
||
.get(&win_cfg.name)
|
||
.copied()
|
||
.unwrap_or(true);
|
||
|
||
let is_focused = data.state.pointer_window_focus.as_ref() == Some(&win_cfg.name);
|
||
|
||
metadata_list.push(serde_json::json!({
|
||
"name": win_cfg.name,
|
||
"x": x,
|
||
"y": y,
|
||
"width": w,
|
||
"height": h,
|
||
"z_order": win_cfg.z_order,
|
||
"focused": is_focused,
|
||
"mapped": mapped,
|
||
"source": "electron",
|
||
"ssd": true,
|
||
}));
|
||
}
|
||
|
||
// Native Wayland windows from the all_windows tracking list.
|
||
// Snapshot to avoid borrow issues during parent lookups.
|
||
let wayland_windows: Vec<_> = data.state.all_windows.clone();
|
||
for window in wayland_windows.iter() {
|
||
let window_name = match data.state.wayland_window_name(window) {
|
||
Some(n) => n.clone(),
|
||
None => continue,
|
||
};
|
||
|
||
let title = if let Some(x11) = window.x11_surface() {
|
||
x11.title().to_string()
|
||
} else {
|
||
window.toplevel()
|
||
.and_then(|t| {
|
||
smithay::wayland::compositor::with_states(t.wl_surface(), |states| {
|
||
states.data_map
|
||
.get::<smithay::wayland::shell::xdg::XdgToplevelSurfaceData>()
|
||
.and_then(|d| d.lock().ok().and_then(|data| data.title.clone()))
|
||
})
|
||
})
|
||
.unwrap_or_else(|| window_name.clone())
|
||
};
|
||
|
||
let app_id = if let Some(x11) = window.x11_surface() {
|
||
Some(x11.class().to_string())
|
||
} else {
|
||
window.toplevel()
|
||
.and_then(|t| {
|
||
smithay::wayland::compositor::with_states(t.wl_surface(), |states| {
|
||
states.data_map
|
||
.get::<smithay::wayland::shell::xdg::XdgToplevelSurfaceData>()
|
||
.and_then(|d| d.lock().ok().and_then(|data| data.app_id.clone()))
|
||
})
|
||
})
|
||
};
|
||
|
||
let loc = data.state.space.element_location(window).unwrap_or_default();
|
||
let geo = window.geometry();
|
||
|
||
let bbox_w = geo.size.w + geo.loc.x.max(0) * 2;
|
||
let bbox_h = geo.size.h + geo.loc.y.max(0) * 2;
|
||
|
||
let th = data.state.title_h_for_window(window);
|
||
let is_ssd = th > 0;
|
||
|
||
let (parent_surface, parent_x11_id) = if let Some(x11) = window.x11_surface() {
|
||
(None, x11.is_transient_for())
|
||
} else {
|
||
(window.toplevel().and_then(|t| {
|
||
smithay::wayland::compositor::with_states(t.wl_surface(), |states| {
|
||
states.data_map
|
||
.get::<smithay::wayland::shell::xdg::XdgToplevelSurfaceData>()
|
||
.and_then(|d| d.lock().ok().and_then(|data| data.parent.clone()))
|
||
})
|
||
}), None)
|
||
};
|
||
|
||
let parent_name = if let Some(parent_wl) = parent_surface.as_ref() {
|
||
use smithay::reexports::wayland_server::Resource;
|
||
data.state.wayland_window_names.get(&parent_wl.id()).cloned()
|
||
} else if let Some(parent_id) = parent_x11_id {
|
||
data.state.x11_window_names.get(&parent_id).cloned()
|
||
} else {
|
||
None
|
||
};
|
||
|
||
let is_dialog = parent_name.is_some();
|
||
|
||
let is_focused = if let Some(x11) = window.x11_surface() {
|
||
x11.wl_surface()
|
||
.map(|s| data.state.surface_has_keyboard_focus(&s))
|
||
.unwrap_or(false)
|
||
} else {
|
||
window.toplevel()
|
||
.map(|t| data.state.surface_has_keyboard_focus(t.wl_surface()))
|
||
.unwrap_or(false)
|
||
};
|
||
|
||
let mapped = data.state.space.element_location(window).is_some();
|
||
|
||
let space_idx = data.state.space.elements()
|
||
.position(|w| w == window)
|
||
.map(|pos| 1000 - pos as i32)
|
||
.unwrap_or(0);
|
||
|
||
let source = if window.x11_surface().is_some() {
|
||
"x11"
|
||
} else {
|
||
"wayland"
|
||
};
|
||
|
||
metadata_list.push(serde_json::json!({
|
||
"name": window_name,
|
||
"title": title,
|
||
"app_id": app_id,
|
||
"x": loc.x - geo.loc.x.max(0),
|
||
"y": loc.y - th - geo.loc.y.max(0),
|
||
"width": bbox_w,
|
||
"height": bbox_h + th,
|
||
"z_order": space_idx,
|
||
"focused": is_focused,
|
||
"source": source,
|
||
"mapped": mapped,
|
||
"ssd": is_ssd,
|
||
"dialog": is_dialog,
|
||
"parent_name": parent_name,
|
||
}));
|
||
}
|
||
|
||
if !metadata_list.is_empty() {
|
||
let metadata_msg = serde_json::json!({
|
||
"type": "metadata",
|
||
"windows": metadata_list,
|
||
});
|
||
|
||
if let Some(ref ipc) = data.state.electron_ipc {
|
||
let _ = ipc.broadcast_metadata(&metadata_msg.to_string());
|
||
}
|
||
}
|
||
data.state.metadata_dirty = false;
|
||
}
|
||
|
||
if data.active {
|
||
render_frame(data);
|
||
}
|
||
})
|
||
.context("running event loop")?;
|
||
|
||
info!("DRM compositor exiting cleanly");
|
||
Ok(())
|
||
}
|