diff --git a/nohup.out b/nohup.out new file mode 100644 index 0000000..f60cc39 --- /dev/null +++ b/nohup.out @@ -0,0 +1,6 @@ +Discord 0.0.128 +[7453:0309/220751.877593:ERROR:ui/ozone/platform/x11/ozone_platform_x11.cc:250] Missing X server or $DISPLAY +[7453:0309/220751.877647:ERROR:ui/aura/env.cc:257] The platform failed to initialize. Exiting. +Discord 0.0.128 +[7931:0309/220902.615647:ERROR:ui/ozone/platform/x11/ozone_platform_x11.cc:250] Missing X server or $DISPLAY +[7931:0309/220902.615700:ERROR:ui/aura/env.cc:257] The platform failed to initialize. Exiting. diff --git a/src/handlers/input.rs b/src/handlers/input.rs index e0679ac..3d814a2 100644 --- a/src/handlers/input.rs +++ b/src/handlers/input.rs @@ -155,12 +155,15 @@ impl WoState { let delta = (event.delta_x(), event.delta_y()); let current = self.pointer_location; - self.pointer_location = ( + let new_pos: smithay::utils::Point = ( (current.x + delta.0).clamp(0.0, self.output_size.0 as f64), (current.y + delta.1).clamp(0.0, self.output_size.1 as f64), ) .into(); + // Hardware is the sole source of truth for pointer position. + self.pointer_location = new_pos; + // Process active grab (interactive move/resize) before normal dispatch if let Some(ref grab) = self.grab_state.clone() { match grab { @@ -232,10 +235,13 @@ impl WoState { } fn on_pointer_motion_abs(&mut self, event: B::PointerMotionAbsoluteEvent) { - self.pointer_location = event + let new_pos: smithay::utils::Point = event .position_transformed((self.output_size.0 as i32, self.output_size.1 as i32).into()) .into(); + // Hardware is the sole source of truth for pointer position. + self.pointer_location = new_pos; + // Process active grab (interactive move/resize) before normal dispatch if let Some(ref grab) = self.grab_state.clone() { match grab { @@ -342,20 +348,10 @@ impl WoState { }; let serial = SERIAL_COUNTER.next_serial(); - // When the cursor is over an Electron-managed window, the compositor - // must NOT touch Smithay's pointer state at all. Electron/comraw owns - // the cursor: it renders the cursor sprite, tracks position inside its - // canvas, and — when the cursor is over an embedded native Wayland/X11 - // window — sends a ForwardedPointer IPC back to the compositor. That - // forwarded path (handle_forwarded_pointer_event) is the ONLY code that - // should call pointer.motion() for native windows. - // - // Previously this block called pointer.motion(None) to "clear" native - // focus, but Smithay interprets that as a real motion event and sends - // wl_pointer.leave to any focused client. When Electron's forwarded - // event arrives a moment later and calls pointer.motion(Some(focus)), - // Smithay sends wl_pointer.enter — so the client sees - // leave→enter→leave→enter every frame, causing cursor rubberbanding. + // When the cursor is over an Electron-managed window, the client + // (comraw) owns event dispatching. The compositor sends the position + // via IPC (above) but does NOT call pointer.motion() — the client's + // forwarded pointer_motion action handles that. if under_electron.is_some() && self.electron_ipc.is_some() { return; } @@ -515,8 +511,12 @@ impl WoState { } } - /// Forward a pointer motion event received from the Electron web UI to the + /// Forward a pointer motion event from the client (comraw/Electron) to the /// Wayland surface under the given window-local coordinates. + /// + /// The client controls event dispatch to windows it renders. However, the + /// compositor remains the sole authority on `pointer_location` (hardware + /// input) — this function does NOT overwrite it. pub fn handle_forwarded_pointer_event(&mut self, window_name: &str, x: f64, y: f64) { use smithay::{ desktop::WindowSurfaceType, @@ -535,18 +535,16 @@ impl WoState { }; let serial = SERIAL_COUNTER.next_serial(); - // (x, y) from the web UI are relative to the rendered surface buffer - // origin (includes CSD decorations/shadows). - // - // Derive a consistent pointer_location from the window's compositor- - // space position so that focus math works correctly. The hardware - // pointer_location is meaningless here because it lives in the host - // compositor's coordinate space (nested mode) while our Space uses - // its own internal coordinates. let win_loc = self.space.element_location(&window).unwrap_or_default(); - let global_x = win_loc.x as f64 + x; - let global_y = win_loc.y as f64 + y; - self.pointer_location = Point::from((global_x, global_y)); + + // Compute a synthetic global location from the client-provided + // surface-local coords. This is used ONLY for the MotionEvent so + // Smithay derives the correct wl_pointer surface-local coordinates. + // pointer_location itself is NOT modified. + let dispatch_location: Point = ( + win_loc.x as f64 + x, + win_loc.y as f64 + y, + ).into(); let focus = window .surface_under(Point::::from((x, y)), WindowSurfaceType::ALL) @@ -562,7 +560,7 @@ impl WoState { self, focus, &MotionEvent { - location: self.pointer_location, + location: dispatch_location, serial, time: 0, }, diff --git a/src/main.rs b/src/main.rs index 194dfa0..2e11052 100644 --- a/src/main.rs +++ b/src/main.rs @@ -106,6 +106,10 @@ struct EventLoopData { /// confirmation. Without this, the CRTC's active scanout buffer could be /// freed prematurely, causing every-other-frame flicker. pending_slot: Option>, + /// 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` for the DRM calloop event @@ -517,31 +521,34 @@ fn render_frame(data: &mut EventLoopData) { let renderer: &mut GlesRenderer = &mut renderer_guard; - // Prepare Wayland surface elements before starting the frame + // 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> = Vec::new(); - 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> = render_elements_from_surface_tree( - renderer, - &surface, - (location.x, location.y), - scale, - 1.0, - Kind::Unspecified, - ); + 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())); - for element in elements { - wayland_elements.push(element); + if let Some(surface) = surface { + let elements: Vec> = render_elements_from_surface_tree( + renderer, + &surface, + (location.x, location.y), + scale, + 1.0, + Kind::Unspecified, + ); + + wayland_elements.extend(elements); } } } @@ -722,16 +729,19 @@ fn render_frame(data: &mut EventLoopData) { } // Export dirty Wayland surfaces to Electron clients via DMABUF/SHM. - // Uses Wayland damage rects to skip unchanged surfaces entirely and to - // avoid full-buffer reads when only a small region changed. + // SHM surfaces are written directly to memfd (ping-pong double-buffered) + // to avoid an intermediate Vec 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 mut exported_pixels: Vec = Vec::new(); + let pid = std::process::id(); for surface in &dirty_surfaces { let surface_id = surface.id(); @@ -772,7 +782,6 @@ fn render_frame(data: &mut EventLoopData) { let Some(_geo) = window_geo else { continue }; // Extract Wayland damage rects for partial-update optimization. - // Empty damage = full frame (surfaces in dirty_surfaces always get exported). let damage_rects: Vec = with_states(surface, |states| { let mut attrs = states.cached_state.get::(); let current = attrs.current(); @@ -785,7 +794,6 @@ fn render_frame(data: &mut EventLoopData) { height: rect.size.h, }, Damage::Surface(rect) => { - // Surface-coordinate damage; apply buffer_scale let scale = current.buffer_scale; crate::state::DamageRect { x: rect.loc.x * scale, @@ -798,40 +806,97 @@ fn render_frame(data: &mut EventLoopData) { }).collect() }); - // Try SHM buffer first (software-rendered clients) - let pixels_opt: Option<(u32, u32, u32, Vec)> = - with_states(surface, |states| { - let rss_data = states.data_map.get::()?; - let rss = rss_data.lock().ok()?; - let buffer = rss.buffer()?; + // Prepare memfd pair for this window (ping-pong double buffer) + let pair = data.state.window_shm_buffers + .entry(window_name.clone()) + .or_insert_with(|| { + let a = nix::sys::memfd::memfd_create( + std::ffi::CStr::from_bytes_with_nul(b"wayland_window\0").unwrap(), + MFdFlags::MFD_CLOEXEC, + ).expect("creating memfd A"); + let b = nix::sys::memfd::memfd_create( + std::ffi::CStr::from_bytes_with_nul(b"wayland_window\0").unwrap(), + MFdFlags::MFD_CLOEXEC, + ).expect("creating memfd B"); + [a, b] + }); + let idx = data.state.window_shm_write_idx + .entry(window_name.clone()) + .or_insert(0); + let memfd_fd = &pair[*idx]; - let mut result: Option<(u32, u32, u32, Vec)> = None; - let _ = with_buffer_contents(buffer, |ptr, len, spec| { - let pixel_bytes = - (spec.stride.max(0) * spec.height.max(0)) as usize; - if pixel_bytes > 0 && pixel_bytes <= len { - let slice = - unsafe { std::slice::from_raw_parts(ptr, pixel_bytes) }; - result = Some(( - spec.width as u32, - spec.height as u32, - spec.stride as u32, - slice.to_vec(), - )); + // Try SHM buffer: write directly to memfd (no intermediate Vec) + let shm_meta: Option<(u32, u32, u32)> = with_states(surface, |states| { + let rss_data = states.data_map.get::()?; + let rss = rss_data.lock().ok()?; + let buffer = rss.buffer()?; + + let mut result: Option<(u32, u32, u32)> = None; + let _ = with_buffer_contents(buffer, |ptr, len, spec| { + let pixel_bytes = + (spec.stride.max(0) * spec.height.max(0)) as usize; + if pixel_bytes == 0 || pixel_bytes > len { return; } + + let slice = + unsafe { std::slice::from_raw_parts(ptr, pixel_bytes) }; + + // Ensure memfd is correctly sized + let size = pixel_bytes as i64; + let cur_size = nix::sys::stat::fstat(memfd_fd) + .map(|s| s.st_size).unwrap_or(0); + if cur_size != size { + let _ = nix::unistd::ftruncate(memfd_fd, size); + } + + // Partial-damage write: when damage rects cover a small + // area and the memfd was already the correct size (meaning + // the previous full frame is still valid for undamaged + // regions), write only the damaged rows. + let usable_damage = !damage_rects.is_empty() + && damage_rects.len() < 64 + && cur_size == size; + + if usable_damage { + let stride = spec.stride as usize; + let height = spec.height as usize; + for rect in &damage_rects { + let y0 = (rect.y.max(0) as usize).min(height); + let y1 = ((rect.y + rect.height).max(0) as usize).min(height); + if y0 >= y1 { continue; } + let byte_start = y0 * stride; + let byte_end = y1 * stride; + let _ = nix::unistd::lseek( + memfd_fd, byte_start as i64, + nix::unistd::Whence::SeekSet, + ); + let _ = nix::unistd::write(memfd_fd, &slice[byte_start..byte_end]); } - }); - result - }); + } else { + let _ = nix::unistd::lseek(memfd_fd, 0, nix::unistd::Whence::SeekSet); + let _ = nix::unistd::write(memfd_fd, slice); + } - if let Some((bw, bh, stride, pixels)) = pixels_opt { - exported_pixels.push(crate::state::ShmExport { - window_name, - width: bw, - height: bh, - stride, - pixels, - damage_rects, + result = Some(( + spec.width as u32, + spec.height as u32, + spec.stride as u32, + )); }); + result + }); + + if let Some((bw, bh, stride)) = 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_fd.as_raw_fd() as i32, + &damage_rects, + ) { + debug!("DRM shm buffer broadcast failed for {}: {}", window_name, e); + } + } + // Flip to the other buffer for next frame + *idx = 1 - *idx; } else if let Some(ref ipc) = data.state.electron_ipc { // Try DMABUF (hardware-rendered clients / games) let dmabuf_opt = with_states(surface, |states| { @@ -848,8 +913,6 @@ fn render_frame(data: &mut EventLoopData) { } } } - - data.state.pending_shm_exports = exported_pixels; } // Send frame callbacks for the cursor surface so the client keeps @@ -906,68 +969,8 @@ fn render_frame(data: &mut EventLoopData) { data.state.damaged_windows.clear(); data.state.dirty_surfaces.clear(); - // Write exported SHM pixel buffers to memfd and broadcast to Electron. - // Uses ping-pong double buffering: write to one memfd while Electron - // reads from the other, eliminating the read/write race condition. - { - use std::os::unix::io::AsRawFd; - use nix::sys::memfd::MFdFlags; - use tracing::debug; - - let exports = std::mem::take(&mut data.state.pending_shm_exports); - for export in exports { - let pair = data.state - .window_shm_buffers - .entry(export.window_name.clone()) - .or_insert_with(|| { - let a = nix::sys::memfd::memfd_create( - std::ffi::CStr::from_bytes_with_nul(b"wayland_window\0").unwrap(), - MFdFlags::MFD_CLOEXEC, - ) - .expect("creating memfd A"); - let b = nix::sys::memfd::memfd_create( - std::ffi::CStr::from_bytes_with_nul(b"wayland_window\0").unwrap(), - MFdFlags::MFD_CLOEXEC, - ) - .expect("creating memfd B"); - [a, b] - }); - - // Pick the write slot (alternates each frame) - let idx = data.state.window_shm_write_idx - .entry(export.window_name.clone()) - .or_insert(0); - let memfd_fd = &pair[*idx]; - - let size = export.pixels.len() as i64; - let stat = nix::sys::stat::fstat(memfd_fd).unwrap(); - if stat.st_size != size { - nix::unistd::ftruncate(memfd_fd, size).expect("truncating memfd"); - } - - nix::unistd::lseek(memfd_fd, 0, nix::unistd::Whence::SeekSet) - .expect("seeking memfd"); - nix::unistd::write(memfd_fd, &export.pixels).expect("writing to memfd"); - - if let Some(ref ipc) = data.state.electron_ipc { - let pid = std::process::id(); - if let Err(e) = ipc.broadcast_shm_buffer( - &export.window_name, - export.width, - export.height, - export.stride, - pid, - memfd_fd.as_raw_fd() as i32, - &export.damage_rects, - ) { - debug!("DRM shm buffer broadcast failed for {}: {}", export.window_name, e); - } - } - - // Flip to the other buffer for next frame - *idx = 1 - *idx; - } - } + // 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 @@ -1133,14 +1136,15 @@ fn run_drm(mut config: Config) -> Result<(), anyhow::Error> { }; match event { Event::PauseSession => { - info!("Session paused (VT switched away). Pausing DRM device and halting rendering."); + 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 rendering."); + 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; @@ -1148,6 +1152,10 @@ fn run_drm(mut config: Config) -> Result<(), anyhow::Error> { 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; @@ -1627,6 +1635,8 @@ fn run_drm(mut config: Config) -> Result<(), anyhow::Error> { 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. @@ -1727,6 +1737,7 @@ fn run_drm(mut config: Config) -> Result<(), anyhow::Error> { total_flips: 0, current_slot: None, pending_slot: None, + libinput_context: libinput_context_for_vt, }; let signal_for_handler = loop_signal.clone(); diff --git a/src/nested.rs b/src/nested.rs index 16e1e69..27100a4 100644 --- a/src/nested.rs +++ b/src/nested.rs @@ -1400,11 +1400,11 @@ fn process_electron_message( .and_then(|v| v.as_f64()) .unwrap_or(0.0); - // Derive pointer_location from the window's - // compositor-space position so focus math is - // consistent (the hardware pointer lives in host - // compositor space and is not meaningful here). - state.pointer_location = ( + // Client dictates event dispatch; compositor + // owns pointer_location (hardware). Compute + // a synthetic location for Smithay without + // overwriting pointer_location. + let dispatch_location: Point = ( loc.x as f64 + px, loc.y as f64 + py, ).into(); @@ -1428,7 +1428,7 @@ fn process_electron_message( state, focus, &smithay::input::pointer::MotionEvent { - location: state.pointer_location, + location: dispatch_location, serial, time, },