diff --git a/src/main.rs b/src/main.rs index 2e11052..3a9fba9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -806,97 +806,94 @@ fn render_frame(data: &mut EventLoopData) { }).collect() }); - // 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]; - - // Try SHM buffer: write directly to memfd (no intermediate Vec) - let shm_meta: Option<(u32, u32, u32)> = with_states(surface, |states| { + // 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::()?; let rss = rss_data.lock().ok()?; let buffer = rss.buffer()?; - let mut result: Option<(u32, u32, u32)> = None; + let mut result: Option<(u32, u32, u32, i32)> = None; let _ = with_buffer_contents(buffer, |ptr, len, spec| { - let pixel_bytes = - (spec.stride.max(0) * spec.height.max(0)) as usize; + 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 slice = - unsafe { std::slice::from_raw_parts(ptr, pixel_bytes) }; + let dst_stride = (bw as usize) * 4; + let dst_size = dst_stride * bh as usize; - // 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); + 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; } - // 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; + let dst = unsafe { slot.as_mut_slice() }; + let src = unsafe { std::slice::from_raw_parts(ptr, pixel_bytes) }; - if usable_damage { - let stride = spec.stride as usize; - let height = spec.height as usize; + let do_partial = !damage_rects.is_empty() + && damage_rects.len() < 64; + + if do_partial { 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]); + 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 { - let _ = nix::unistd::lseek(memfd_fd, 0, nix::unistd::Whence::SeekSet); - let _ = nix::unistd::write(memfd_fd, slice); + 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 + } + } } - result = Some(( - spec.width as u32, - spec.height as u32, - spec.stride as u32, - )); + // 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)) = shm_meta { + 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_fd.as_raw_fd() as i32, + pid, memfd_raw, &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| { diff --git a/src/nested.rs b/src/nested.rs index 27100a4..05c48d6 100644 --- a/src/nested.rs +++ b/src/nested.rs @@ -746,14 +746,27 @@ pub fn run_nested(config: Config) -> Result<()> { drop(target); - // Export dirty Wayland surfaces via offscreen rendering to SHM - let mut exported_pixels: Vec = Vec::new(); + // Export dirty Wayland surfaces directly to mmap'd memfds (zero intermediate copy). + // BGRA→RGBA conversion happens during the copy so the client can skip it. { use smithay::reexports::wayland_server::Resource; use smithay::wayland::compositor::{with_states, Damage, SurfaceAttributes}; + use std::os::unix::io::AsRawFd; let dirty: Vec<_> = state.dirty_surfaces.drain().collect(); + // Collect exports into a Vec so we can send IPC after releasing surface borrows. + struct ShmExportMeta { + window_name: String, + width: u32, + height: u32, + /// Output stride (always width*4, tight packing in RGBA). + stride: u32, + memfd_raw_fd: i32, + damage_rects: Vec, + } + let mut shm_exports: Vec = Vec::new(); + for surface in dirty { let surface_id = surface.id(); let (window_name, window_geo) = if let Some(n) = @@ -824,41 +837,132 @@ pub fn run_nested(config: Config) -> Result<()> { continue; } - // Read SHM buffer directly (for software-rendered clients) + // ── Direct SHM→mmap copy with BGRA→RGBA conversion ────────── use smithay::backend::renderer::utils::RendererSurfaceStateUserData; use smithay::wayland::shm::with_buffer_contents; - 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()?; + let wrote_ok: bool = with_states(&surface, |states| { + let rss_data = match states.data_map.get::() { + Some(d) => d, + None => return false, + }; + let rss = match rss_data.lock() { + Ok(r) => r, + Err(_) => return false, + }; + let buffer = match rss.buffer() { + Some(b) => b, + None => return false, + }; - 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(), - )); + let mut ok = false; + 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; + } + + // Output stride: tightly packed RGBA (width * 4) + let dst_stride = (bw as usize) * 4; + let dst_size = dst_stride * bh as usize; + + // Get or create the mmap'd slot pair for this window. + let pair = 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; + } + + // SAFETY: ping-pong guarantees Electron reads the other slot. + let dst = unsafe { slot.as_mut_slice() }; + let src = unsafe { std::slice::from_raw_parts(ptr, pixel_bytes) }; + + // Determine whether to do a partial (damage-rect) or full copy. + let do_partial = !damage_rects.is_empty() + && damage_rects.len() < 64; + + if do_partial { + // Partial update: only copy damaged rows with BGRA→RGBA swap + 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; + // BGRA → RGBA: swap B↔R + 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 + } + } } - }); - result + } else { + // Full frame: copy all rows with BGRA→RGBA swap + 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 + } + } + } + ok = true; + }); + ok + }); + + if wrote_ok { + // The slot pair was already flipped by write_slot_mut(); the + // "previous" write slot is at index (current_write_idx) since + // write_slot_mut already toggled it. + let pair = state.window_shm_slots.get(&window_name).unwrap(); + // After write_slot_mut() the write_idx was flipped, so the + // slot we just wrote to is at `1 - pair.write_idx`. + let wrote_idx = 1 - pair.current_write_idx(); + let memfd_raw = pair.slots[wrote_idx].fd.as_raw_fd(); + + let bw_h: (u32, u32) = with_states(&surface, |states| { + let rss_data = states.data_map.get::(); + rss_data + .and_then(|d| d.lock().ok()) + .and_then(|rss| rss.buffer().map(|b| { + let mut r = (0u32, 0u32); + let _ = with_buffer_contents(&b, |_ptr, _len, spec| { + r = (spec.width.max(0) as u32, spec.height.max(0) as u32); + }); + r + })) + .unwrap_or((w, h)) }); - if let Some((bw, bh, stride, pixels)) = pixels_opt { - exported_pixels.push(crate::state::ShmExport { + shm_exports.push(ShmExportMeta { window_name, - width: bw, - height: bh, - stride, - pixels, + width: bw_h.0, + height: bw_h.1, + stride: bw_h.0 * 4, + memfd_raw_fd: memfd_raw, damage_rects, }); } else if let Some(ref ipc) = state.electron_ipc { @@ -931,10 +1035,6 @@ pub fn run_nested(config: Config) -> Result<()> { )?; frame.clear(Color32F::TRANSPARENT, &[output_rect])?; - // Draw each element in the surface tree - // Elements are positioned relative to surface origin (0, 0) - // For CSD windows, we DON'T offset - surface origin should already - // include the decoration area, and geometry() positions are correct for element in &elements { let src = element.src(); let dst = element.geometry(scale); @@ -949,8 +1049,6 @@ pub fn run_nested(config: Config) -> Result<()> { continue; } - // Read back pixels from offscreen texture - // Skip GL rendering for now - only export SHM buffers warn!( "Skipping GL offscreen render for {} - only SHM buffers supported", window_name @@ -960,8 +1058,27 @@ pub fn run_nested(config: Config) -> Result<()> { } } - // Store exported pixels for memfd write after render block - state.pending_shm_exports = exported_pixels; + // Broadcast SHM metadata to Electron (pixels already in mmap'd memfds). + if let Some(ref ipc) = state.electron_ipc { + let pid = std::process::id(); + for meta in &shm_exports { + if let Err(e) = ipc.broadcast_shm_buffer( + &meta.window_name, + meta.width, + meta.height, + meta.stride, + pid, + meta.memfd_raw_fd, + &meta.damage_rects, + ) { + debug!( + "nested shm buffer broadcast failed for {}: {}", + meta.window_name, e + ); + } + } + } + state.dirty_surfaces.clear(); } // 'render block ends here, backend borrow is released @@ -999,66 +1116,7 @@ pub fn run_nested(config: Config) -> Result<()> { }); } - // Write exported pixel buffers to SHM and broadcast to Electron. - // Uses ping-pong double-buffered memfds (same as DRM path). - if did_bind { - use std::os::unix::io::AsRawFd; - - let exports = std::mem::take(&mut state.pending_shm_exports); - for export in exports { - let pair = 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] - }); - - let idx = 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) = 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!( - "nested shm buffer broadcast failed for {}: {}", - export.window_name, e - ); - } - } - - *idx = 1 - *idx; - } - } + // SHM exports already written directly to mmap'd memfds and broadcast above. let render_total_ms = render_t_start.elapsed().as_millis(); if render_total_ms > 50 { diff --git a/src/state.rs b/src/state.rs index 9bbb836..de9f871 100644 --- a/src/state.rs +++ b/src/state.rs @@ -45,14 +45,141 @@ pub struct DamageRect { pub height: i32, } -/// Buffered SHM pixel export with per-surface damage information. -pub struct ShmExport { - pub window_name: String, - pub width: u32, - pub height: u32, - pub stride: u32, - pub pixels: Vec, - pub damage_rects: Vec, +/// mmap-backed memfd slot for zero-copy SHM export. +/// +/// Instead of copying pixels into a `Vec` and later `write()`-ing to a memfd, +/// we keep the memfd persistently mmap'd so the compositor can `memcpy` +/// directly from the Wayland SHM pool into the mapped region — eliminating +/// two copies per frame. Electron reads the same memfd via +/// `/proc//fd/` without any additional data movement. +pub struct MappedShmSlot { + pub fd: std::os::unix::io::OwnedFd, + ptr: *mut u8, + len: usize, +} + +// SAFETY: The mmap pointer is only accessed on the compositor main thread. +unsafe impl Send for MappedShmSlot {} + +impl MappedShmSlot { + /// Create a new slot backed by a memfd of `size` bytes. + pub fn new(size: usize) -> anyhow::Result { + use anyhow::Context; + let fd = nix::sys::memfd::memfd_create( + std::ffi::CStr::from_bytes_with_nul(b"wayland_window\0").unwrap(), + nix::sys::memfd::MFdFlags::MFD_CLOEXEC, + ) + .context("creating memfd for MappedShmSlot")?; + nix::unistd::ftruncate(&fd, size as i64).context("ftruncate memfd")?; + + let ptr = unsafe { + libc::mmap( + std::ptr::null_mut(), + size, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_SHARED, + std::os::unix::io::AsRawFd::as_raw_fd(&fd), + 0, + ) + }; + if ptr == libc::MAP_FAILED { + anyhow::bail!("mmap failed for MappedShmSlot"); + } + + Ok(Self { fd, ptr: ptr as *mut u8, len: size }) + } + + /// Resize the slot, remapping if the new size differs. + pub fn ensure_size(&mut self, new_size: usize) -> anyhow::Result<()> { + use anyhow::Context; + if new_size == self.len { + return Ok(()); + } + // munmap old + unsafe { libc::munmap(self.ptr as *mut libc::c_void, self.len) }; + nix::unistd::ftruncate(&self.fd, new_size as i64).context("ftruncate resize")?; + let ptr = unsafe { + libc::mmap( + std::ptr::null_mut(), + new_size, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_SHARED, + std::os::unix::io::AsRawFd::as_raw_fd(&self.fd), + 0, + ) + }; + if ptr == libc::MAP_FAILED { + anyhow::bail!("mmap failed on resize"); + } + self.ptr = ptr as *mut u8; + self.len = new_size; + Ok(()) + } + + /// Get a mutable slice over the mapped region. + /// + /// # Safety + /// Caller must ensure no concurrent reader is accessing the slot + /// (the ping-pong scheme guarantees this). + #[inline] + pub unsafe fn as_mut_slice(&mut self) -> &mut [u8] { + std::slice::from_raw_parts_mut(self.ptr, self.len) + } + + /// Copy `src` into the mapped region starting at byte offset `offset`. + /// # Safety + /// Same as `as_mut_slice`. + #[inline] + pub unsafe fn write_at(&mut self, offset: usize, src: &[u8]) { + debug_assert!(offset + src.len() <= self.len); + std::ptr::copy_nonoverlapping(src.as_ptr(), self.ptr.add(offset), src.len()); + } + + /// Write a single pixel (4 bytes) at the given byte offset. + #[inline] + pub unsafe fn write_pixel(&mut self, offset: usize, pixel: [u8; 4]) { + debug_assert!(offset + 4 <= self.len); + std::ptr::copy_nonoverlapping(pixel.as_ptr(), self.ptr.add(offset), 4); + } + + pub fn len(&self) -> usize { + self.len + } +} + +impl Drop for MappedShmSlot { + fn drop(&mut self) { + if self.len > 0 { + unsafe { libc::munmap(self.ptr as *mut libc::c_void, self.len) }; + } + } +} + +/// Ping-pong pair of mmap'd memfd slots for a window. +pub struct ShmSlotPair { + pub slots: [MappedShmSlot; 2], + pub write_idx: usize, +} + +impl ShmSlotPair { + pub fn new(size: usize) -> anyhow::Result { + Ok(Self { + slots: [MappedShmSlot::new(size)?, MappedShmSlot::new(size)?], + write_idx: 0, + }) + } + + /// Get the current write slot (mutably) and flip the index for next frame. + pub fn write_slot_mut(&mut self) -> &mut MappedShmSlot { + let idx = self.write_idx; + self.write_idx = 1 - self.write_idx; + &mut self.slots[idx] + } + + /// Peek the current write slot index (before flip). + pub fn current_write_idx(&self) -> usize { + self.write_idx + } } #[derive(Default)] @@ -126,13 +253,8 @@ pub struct WoState { pub metadata_dirty: bool, /// Wayland surfaces that committed a new buffer and need offscreen capture. pub dirty_surfaces: std::collections::HashSet, - /// Ping-pong SHM memfd pairs per window. Index 0 is being written by - /// the compositor while Electron reads index 1 (and vice versa). - pub window_shm_buffers: std::collections::HashMap, - /// Tracks which of the two memfds is the current write target (0 or 1). - pub window_shm_write_idx: std::collections::HashMap, - /// Buffered pixel exports from offscreen rendering, written to SHM after render block. - pub pending_shm_exports: Vec, + /// Ping-pong mmap'd memfd pairs per window for zero-copy SHM export. + pub window_shm_slots: std::collections::HashMap, pub grab_state: Option, pub cursor_theme_manager: CursorThemeManager, @@ -328,9 +450,7 @@ impl WoState { damaged_windows: std::collections::HashSet::new(), metadata_dirty: true, dirty_surfaces: std::collections::HashSet::new(), - window_shm_buffers: std::collections::HashMap::new(), - window_shm_write_idx: std::collections::HashMap::new(), - pending_shm_exports: Vec::new(), + window_shm_slots: std::collections::HashMap::new(), grab_state: None, cursor_theme_manager, debug_new_toplevel: false,