Again
This commit is contained in:
parent
4fe557c536
commit
42b111410c
10 changed files with 440 additions and 142 deletions
108
electron/dist/main.js
vendored
108
electron/dist/main.js
vendored
|
|
@ -45,7 +45,8 @@ var MAGIC = {
|
|||
FORWARD_POINTER: 1464815685,
|
||||
FORWARD_KEYBOARD: 1464814405,
|
||||
FORWARD_RELATIVE_POINTER: 1464816197,
|
||||
POINTER_LOCK_REQUEST: 1464815692
|
||||
POINTER_LOCK_REQUEST: 1464815692,
|
||||
ENV_UPDATE: 1464812885
|
||||
};
|
||||
function stringToBuffer(str) {
|
||||
return Buffer.from(str, "utf-8");
|
||||
|
|
@ -1143,8 +1144,8 @@ function connectToCompositor() {
|
|||
if (compositorRxBuffer.length < 8)
|
||||
break;
|
||||
const nameLen = compositorRxBuffer.readUInt32LE(4);
|
||||
const headerSize = 8 + nameLen + 20;
|
||||
if (compositorRxBuffer.length < headerSize)
|
||||
const baseHeader = 8 + nameLen + 20 + 4;
|
||||
if (compositorRxBuffer.length < baseHeader)
|
||||
break;
|
||||
let off = 8;
|
||||
const windowName = compositorRxBuffer.slice(off, off + nameLen).toString("utf8");
|
||||
|
|
@ -1158,29 +1159,68 @@ function connectToCompositor() {
|
|||
const pid = compositorRxBuffer.readUInt32LE(off);
|
||||
off += 4;
|
||||
const fd = compositorRxBuffer.readUInt32LE(off);
|
||||
compositorRxBuffer = compositorRxBuffer.slice(headerSize);
|
||||
off += 4;
|
||||
const numRects = compositorRxBuffer.readUInt32LE(off);
|
||||
off += 4;
|
||||
const fullHeader = baseHeader + numRects * 16;
|
||||
if (compositorRxBuffer.length < fullHeader)
|
||||
break;
|
||||
const damageRects = [];
|
||||
for (let i = 0;i < numRects; i++) {
|
||||
damageRects.push({
|
||||
x: compositorRxBuffer.readUInt32LE(off),
|
||||
y: compositorRxBuffer.readUInt32LE(off + 4),
|
||||
width: compositorRxBuffer.readUInt32LE(off + 8),
|
||||
height: compositorRxBuffer.readUInt32LE(off + 12)
|
||||
});
|
||||
off += 16;
|
||||
}
|
||||
compositorRxBuffer = compositorRxBuffer.slice(fullHeader);
|
||||
try {
|
||||
const extFd = getOrOpenShmFd(windowName, pid, fd);
|
||||
const size = sbStride * sbHeight;
|
||||
const safeBuffer = getReusableShmBuffer(windowName, size);
|
||||
let totalRead = 0;
|
||||
while (totalRead < size) {
|
||||
const bytesRead = fs.readSync(extFd, safeBuffer, totalRead, size - totalRead, totalRead);
|
||||
if (bytesRead <= 0)
|
||||
break;
|
||||
totalRead += bytesRead;
|
||||
}
|
||||
if (totalRead === size) {
|
||||
surfaceBufferPending.set(windowName, {
|
||||
name: windowName,
|
||||
width: sbWidth,
|
||||
height: sbHeight,
|
||||
stride: sbStride,
|
||||
pixels: safeBuffer
|
||||
});
|
||||
if (!surfaceBufferTimer) {
|
||||
surfaceBufferTimer = setTimeout(flushSurfaceBuffers, 10);
|
||||
const fullSize = sbStride * sbHeight;
|
||||
const safeBuffer = getReusableShmBuffer(windowName, fullSize);
|
||||
const hasUsableRects = damageRects.length > 0 && damageRects.length < 64;
|
||||
if (hasUsableRects) {
|
||||
let minY = sbHeight, maxY = 0;
|
||||
for (const r of damageRects) {
|
||||
const ry = Math.max(0, r.y);
|
||||
const ryEnd = Math.min(sbHeight, r.y + r.height);
|
||||
if (ry < minY)
|
||||
minY = ry;
|
||||
if (ryEnd > maxY)
|
||||
maxY = ryEnd;
|
||||
}
|
||||
if (minY < maxY) {
|
||||
const readOffset = minY * sbStride;
|
||||
const readLen = (maxY - minY) * sbStride;
|
||||
let totalRead = 0;
|
||||
while (totalRead < readLen) {
|
||||
const bytesRead = fs.readSync(extFd, safeBuffer, readOffset + totalRead, readLen - totalRead, readOffset + totalRead);
|
||||
if (bytesRead <= 0)
|
||||
break;
|
||||
totalRead += bytesRead;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let totalRead = 0;
|
||||
while (totalRead < fullSize) {
|
||||
const bytesRead = fs.readSync(extFd, safeBuffer, totalRead, fullSize - totalRead, totalRead);
|
||||
if (bytesRead <= 0)
|
||||
break;
|
||||
totalRead += bytesRead;
|
||||
}
|
||||
}
|
||||
surfaceBufferPending.set(windowName, {
|
||||
name: windowName,
|
||||
width: sbWidth,
|
||||
height: sbHeight,
|
||||
stride: sbStride,
|
||||
pixels: safeBuffer,
|
||||
damageRects: hasUsableRects ? damageRects : undefined
|
||||
});
|
||||
if (!surfaceBufferTimer) {
|
||||
surfaceBufferTimer = setTimeout(flushSurfaceBuffers, 10);
|
||||
}
|
||||
} catch (err) {
|
||||
const cached = shmFdCache.get(windowName);
|
||||
|
|
@ -1257,6 +1297,28 @@ function connectToCompositor() {
|
|||
}
|
||||
continue;
|
||||
}
|
||||
if (magic === MAGIC.ENV_UPDATE) {
|
||||
if (compositorRxBuffer.length < 8)
|
||||
break;
|
||||
const jsonLen = compositorRxBuffer.readUInt32LE(4);
|
||||
if (compositorRxBuffer.length < 8 + jsonLen)
|
||||
break;
|
||||
const jsonStr = compositorRxBuffer.slice(8, 8 + jsonLen).toString("utf8");
|
||||
compositorRxBuffer = compositorRxBuffer.slice(8 + jsonLen);
|
||||
try {
|
||||
const vars = JSON.parse(jsonStr);
|
||||
for (const [key, value] of Object.entries(vars)) {
|
||||
process.env[key] = value;
|
||||
debugLog(`[Wo] env update: ${key}=${value}`);
|
||||
}
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send("wo:env-update", vars);
|
||||
}
|
||||
} catch (err) {
|
||||
debugLog("[Wo] ENV_UPDATE parse error:", err);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
debugLog("[Wo] UNKNOWN magic 0x" + magic.toString(16).padStart(8, "0") + " bufLen=" + compositorRxBuffer.length + " — stream may be corrupted");
|
||||
compositorRxBuffer = compositorRxBuffer.slice(1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ let damageBuffer: DamageBuffer | null = null;
|
|||
let nativeDmabuf: any = null;
|
||||
let compositorRxBuffer = Buffer.alloc(0);
|
||||
// Per-window surface buffer throttle (~30fps max delivery to renderer)
|
||||
const surfaceBufferPending = new Map<string, { name: string; width: number; height: number; stride: number; pixels: Buffer }>();
|
||||
const surfaceBufferPending = new Map<string, { name: string; width: number; height: number; stride: number; pixels: Buffer; damageRects?: Array<{x: number; y: number; width: number; height: number}> }>();
|
||||
let surfaceBufferTimer: NodeJS.Timeout | null = null;
|
||||
const shmReadBufferCache = new Map<string, Buffer>();
|
||||
const shmFdCache = new Map<string, { pid: number; fd: number; extFd: number }>();
|
||||
|
|
@ -1044,8 +1044,9 @@ function connectToCompositor(): Promise<void> {
|
|||
if (magic === MAGIC.SHM_BUFFER) {
|
||||
if (compositorRxBuffer.length < 8) break;
|
||||
const nameLen = compositorRxBuffer.readUInt32LE(4);
|
||||
const headerSize = 8 + nameLen + 20;
|
||||
if (compositorRxBuffer.length < headerSize) break;
|
||||
// Minimum header before we know num_rects
|
||||
const baseHeader = 8 + nameLen + 20 + 4; // +4 for num_rects field
|
||||
if (compositorRxBuffer.length < baseHeader) break;
|
||||
let off = 8;
|
||||
const windowName = compositorRxBuffer.slice(off, off + nameLen).toString('utf8');
|
||||
off += nameLen;
|
||||
|
|
@ -1053,31 +1054,70 @@ function connectToCompositor(): Promise<void> {
|
|||
const sbHeight = compositorRxBuffer.readUInt32LE(off); off += 4;
|
||||
const sbStride = compositorRxBuffer.readUInt32LE(off); off += 4;
|
||||
const pid = compositorRxBuffer.readUInt32LE(off); off += 4;
|
||||
const fd = compositorRxBuffer.readUInt32LE(off);
|
||||
compositorRxBuffer = compositorRxBuffer.slice(headerSize);
|
||||
const fd = compositorRxBuffer.readUInt32LE(off); off += 4;
|
||||
const numRects = compositorRxBuffer.readUInt32LE(off); off += 4;
|
||||
const fullHeader = baseHeader + numRects * 16;
|
||||
if (compositorRxBuffer.length < fullHeader) break;
|
||||
|
||||
const damageRects: Array<{x: number; y: number; width: number; height: number}> = [];
|
||||
for (let i = 0; i < numRects; i++) {
|
||||
damageRects.push({
|
||||
x: compositorRxBuffer.readUInt32LE(off),
|
||||
y: compositorRxBuffer.readUInt32LE(off + 4),
|
||||
width: compositorRxBuffer.readUInt32LE(off + 8),
|
||||
height: compositorRxBuffer.readUInt32LE(off + 12),
|
||||
});
|
||||
off += 16;
|
||||
}
|
||||
compositorRxBuffer = compositorRxBuffer.slice(fullHeader);
|
||||
|
||||
try {
|
||||
const extFd = getOrOpenShmFd(windowName, pid, fd);
|
||||
const size = sbStride * sbHeight;
|
||||
const safeBuffer = getReusableShmBuffer(windowName, size);
|
||||
let totalRead = 0;
|
||||
while (totalRead < size) {
|
||||
const bytesRead = fs.readSync(extFd, safeBuffer, totalRead, size - totalRead, totalRead);
|
||||
if (bytesRead <= 0) break;
|
||||
totalRead += bytesRead;
|
||||
const fullSize = sbStride * sbHeight;
|
||||
const safeBuffer = getReusableShmBuffer(windowName, fullSize);
|
||||
|
||||
// Partial read: only read damaged rows from memfd.
|
||||
// Empty damageRects = full frame update (always do full read).
|
||||
// Also do full read on first frame for this window (buffer is uninitialized).
|
||||
const hasUsableRects = damageRects.length > 0 && damageRects.length < 64;
|
||||
if (hasUsableRects) {
|
||||
let minY = sbHeight, maxY = 0;
|
||||
for (const r of damageRects) {
|
||||
const ry = Math.max(0, r.y);
|
||||
const ryEnd = Math.min(sbHeight, r.y + r.height);
|
||||
if (ry < minY) minY = ry;
|
||||
if (ryEnd > maxY) maxY = ryEnd;
|
||||
}
|
||||
if (minY < maxY) {
|
||||
const readOffset = minY * sbStride;
|
||||
const readLen = (maxY - minY) * sbStride;
|
||||
let totalRead = 0;
|
||||
while (totalRead < readLen) {
|
||||
const bytesRead = fs.readSync(extFd, safeBuffer, readOffset + totalRead, readLen - totalRead, readOffset + totalRead);
|
||||
if (bytesRead <= 0) break;
|
||||
totalRead += bytesRead;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Full read (no rects, too many rects, or first frame)
|
||||
let totalRead = 0;
|
||||
while (totalRead < fullSize) {
|
||||
const bytesRead = fs.readSync(extFd, safeBuffer, totalRead, fullSize - totalRead, totalRead);
|
||||
if (bytesRead <= 0) break;
|
||||
totalRead += bytesRead;
|
||||
}
|
||||
}
|
||||
|
||||
if (totalRead === size) {
|
||||
surfaceBufferPending.set(windowName, {
|
||||
name: windowName,
|
||||
width: sbWidth,
|
||||
height: sbHeight,
|
||||
stride: sbStride,
|
||||
pixels: safeBuffer,
|
||||
});
|
||||
if (!surfaceBufferTimer) {
|
||||
surfaceBufferTimer = setTimeout(flushSurfaceBuffers, 10);
|
||||
}
|
||||
surfaceBufferPending.set(windowName, {
|
||||
name: windowName,
|
||||
width: sbWidth,
|
||||
height: sbHeight,
|
||||
stride: sbStride,
|
||||
pixels: safeBuffer,
|
||||
damageRects: hasUsableRects ? damageRects : undefined,
|
||||
});
|
||||
if (!surfaceBufferTimer) {
|
||||
surfaceBufferTimer = setTimeout(flushSurfaceBuffers, 10);
|
||||
}
|
||||
} catch (err) {
|
||||
const cached = shmFdCache.get(windowName);
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@ const compositorAPI: CompositorAPI = {
|
|||
},
|
||||
|
||||
// Surface buffer: Wayland window pixel content from compositor
|
||||
onSurfaceBuffer: (callback: (data: { name: string; width: number; height: number; stride: number; pixels: Buffer }) => void) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, data: { name: string; width: number; height: number; stride: number; pixels: Buffer }) =>
|
||||
onSurfaceBuffer: (callback: (data: { name: string; width: number; height: number; stride: number; pixels: Buffer; damageRects?: Array<{x: number; y: number; width: number; height: number}> }) => void) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, data: { name: string; width: number; height: number; stride: number; pixels: Buffer; damageRects?: Array<{x: number; y: number; width: number; height: number}> }) =>
|
||||
callback(data);
|
||||
ipcRenderer.on('wo:surface-buffer', handler);
|
||||
return () => ipcRenderer.removeListener('wo:surface-buffer', handler);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ export type {
|
|||
WindowMetadataEvent,
|
||||
FocusChangeEvent,
|
||||
DamageFramePayload,
|
||||
DamageRect,
|
||||
DamagePatch,
|
||||
RendererDamageHelper,
|
||||
RendererDamageHelperOptions,
|
||||
} from './types';
|
||||
|
|
|
|||
1
electron/types/types.d.ts
vendored
1
electron/types/types.d.ts
vendored
|
|
@ -302,6 +302,7 @@ export interface SurfaceBufferData {
|
|||
height: number;
|
||||
stride: number;
|
||||
pixels: Uint8Array;
|
||||
damageRects?: DamageRect[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -227,6 +227,9 @@ impl ElectronClientConnection {
|
|||
/// Wire format: magic(4) + name_len(4) + name(N) + width(4) + height(4) + stride(4) + pid(4) + fd(4)
|
||||
/// The compositor PID and fd number let Electron open /proc/<pid>/fd/<fd>
|
||||
/// to mmap pixels without copying.
|
||||
/// Wire format:
|
||||
/// magic(4) + name_len(4) + name(N) + width(4) + height(4) + stride(4) + pid(4) + fd(4)
|
||||
/// + num_rects(4) + [x(4) + y(4) + w(4) + h(4)] * num_rects
|
||||
pub fn send_shm_buffer(
|
||||
&self,
|
||||
window_name: &str,
|
||||
|
|
@ -235,9 +238,11 @@ impl ElectronClientConnection {
|
|||
stride: u32,
|
||||
pid: u32,
|
||||
memfd_fd: i32,
|
||||
damage_rects: &[crate::state::DamageRect],
|
||||
) -> Result<()> {
|
||||
let name_bytes = window_name.as_bytes();
|
||||
let header_len = 4 + 4 + name_bytes.len() + 4 + 4 + 4 + 4 + 4;
|
||||
let num_rects = damage_rects.len() as u32;
|
||||
let header_len = 4 + 4 + name_bytes.len() + 4 + 4 + 4 + 4 + 4 + 4 + (damage_rects.len() * 16);
|
||||
let mut buf = vec![0u8; header_len];
|
||||
let mut off = 0;
|
||||
buf[off..off + 4].copy_from_slice(&MAGIC_SHM_BUFFER.to_le_bytes());
|
||||
|
|
@ -255,6 +260,19 @@ impl ElectronClientConnection {
|
|||
buf[off..off + 4].copy_from_slice(&pid.to_le_bytes());
|
||||
off += 4;
|
||||
buf[off..off + 4].copy_from_slice(&(memfd_fd as u32).to_le_bytes());
|
||||
off += 4;
|
||||
buf[off..off + 4].copy_from_slice(&num_rects.to_le_bytes());
|
||||
off += 4;
|
||||
for rect in damage_rects {
|
||||
buf[off..off + 4].copy_from_slice(&(rect.x as u32).to_le_bytes());
|
||||
off += 4;
|
||||
buf[off..off + 4].copy_from_slice(&(rect.y as u32).to_le_bytes());
|
||||
off += 4;
|
||||
buf[off..off + 4].copy_from_slice(&(rect.width as u32).to_le_bytes());
|
||||
off += 4;
|
||||
buf[off..off + 4].copy_from_slice(&(rect.height as u32).to_le_bytes());
|
||||
off += 4;
|
||||
}
|
||||
|
||||
self.write_bytes_nonblocking(&buf)
|
||||
}
|
||||
|
|
@ -627,7 +645,10 @@ impl ElectronIpc {
|
|||
.with_context(|| format!("binding IPC socket {socket_path}"))?;
|
||||
info!(socket = socket_path, "IPC socket listening");
|
||||
|
||||
let (tx, rx) = mpsc::channel(8);
|
||||
// 64 slots to avoid frame/input starvation under load. The old
|
||||
// value of 8 caused silent frame drops when games submitted at
|
||||
// >60 FPS while input events competed for the same channel.
|
||||
let (tx, rx) = mpsc::channel(64);
|
||||
let tx2 = tx.clone();
|
||||
let clients = Arc::new(Mutex::new(HashMap::new()));
|
||||
let clients2 = clients.clone();
|
||||
|
|
@ -777,6 +798,7 @@ impl ElectronIpc {
|
|||
stride: u32,
|
||||
pid: u32,
|
||||
memfd_fd: i32,
|
||||
damage_rects: &[crate::state::DamageRect],
|
||||
) -> Result<()> {
|
||||
let snapshot: Vec<_> = {
|
||||
let clients = self.clients.lock().unwrap();
|
||||
|
|
@ -784,7 +806,7 @@ impl ElectronIpc {
|
|||
};
|
||||
// clients Mutex released — safe to do I/O.
|
||||
for client in &snapshot {
|
||||
if let Err(e) = client.send_shm_buffer(window_name, width, height, stride, pid, memfd_fd) {
|
||||
if let Err(e) = client.send_shm_buffer(window_name, width, height, stride, pid, memfd_fd, damage_rects) {
|
||||
debug!("shm buffer to '{}' dropped: {e:#}", client.name);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -408,10 +408,19 @@ impl WoState {
|
|||
|
||||
if let Some(ref win_name) = under_window {
|
||||
if button_state == ButtonState::Pressed {
|
||||
self.keyboard_window_focus = Some(win_name.clone());
|
||||
// Clear Wayland keyboard focus so Electron receives keys.
|
||||
let keyboard = self.seat.get_keyboard().unwrap();
|
||||
keyboard.set_focus(self, None, serial);
|
||||
// Only set keyboard focus to the Electron window and clear
|
||||
// Wayland focus if we don't already have a native window
|
||||
// focused via the forwarded path. If the user clicked on a
|
||||
// native window canvas inside Electron/comraw, the forwarded
|
||||
// pointer_button action will arrive shortly and set the
|
||||
// correct Wayland surface focus. Clearing it here would
|
||||
// cause focus to bounce (clear → set → clear → set).
|
||||
let has_native_focus = self.seat.get_keyboard()
|
||||
.map(|kb| kb.current_focus().is_some())
|
||||
.unwrap_or(false);
|
||||
if !has_native_focus {
|
||||
self.keyboard_window_focus = Some(win_name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref ipc) = self.electron_ipc {
|
||||
|
|
@ -520,23 +529,34 @@ impl WoState {
|
|||
None => return,
|
||||
};
|
||||
|
||||
// Do NOT overwrite self.pointer_location here. The hardware input
|
||||
// handler (on_pointer_motion / on_pointer_motion_abs) already set it
|
||||
// to the authoritative screen position. Recomputing it from
|
||||
// element_location + geo offsets uses a different coordinate system
|
||||
// and causes the position to ping-pong between the hardware value
|
||||
// and the recomputed value, producing visible rubberbanding.
|
||||
|
||||
let pointer = match self.seat.get_pointer() {
|
||||
Some(p) => p,
|
||||
None => return,
|
||||
};
|
||||
let serial = SERIAL_COUNTER.next_serial();
|
||||
|
||||
// Coordinates (x, y) are already relative to the window/surface origin.
|
||||
// (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));
|
||||
|
||||
let focus = window
|
||||
.surface_under(Point::<f64, Logical>::from((x, y)), WindowSurfaceType::ALL)
|
||||
.map(|(s, p)| (s, p.to_f64()));
|
||||
.map(|(s, offset)| {
|
||||
let focus_pos: Point<f64, Logical> = (
|
||||
win_loc.x as f64 + offset.x as f64,
|
||||
win_loc.y as f64 + offset.y as f64,
|
||||
).into();
|
||||
(s, focus_pos)
|
||||
});
|
||||
|
||||
pointer.motion(
|
||||
self,
|
||||
|
|
|
|||
125
src/main.rs
125
src/main.rs
|
|
@ -83,6 +83,9 @@ struct EventLoopData {
|
|||
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,
|
||||
|
|
@ -718,15 +721,17 @@ fn render_frame(data: &mut EventLoopData) {
|
|||
);
|
||||
}
|
||||
|
||||
// Export dirty Wayland surfaces to Electron clients via DMABUF/SHM
|
||||
// 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.
|
||||
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;
|
||||
use smithay::wayland::compositor::{with_states, Damage, SurfaceAttributes};
|
||||
use smithay::wayland::shm::with_buffer_contents;
|
||||
use tracing::debug;
|
||||
|
||||
let mut exported_pixels: Vec<(String, u32, u32, u32, Vec<u8>)> = Vec::new();
|
||||
let mut exported_pixels: Vec<crate::state::ShmExport> = Vec::new();
|
||||
|
||||
for surface in &dirty_surfaces {
|
||||
let surface_id = surface.id();
|
||||
|
|
@ -766,6 +771,33 @@ 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<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) => {
|
||||
// Surface-coordinate damage; apply buffer_scale
|
||||
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 first (software-rendered clients)
|
||||
let pixels_opt: Option<(u32, u32, u32, Vec<u8>)> =
|
||||
with_states(surface, |states| {
|
||||
|
|
@ -792,7 +824,14 @@ fn render_frame(data: &mut EventLoopData) {
|
|||
});
|
||||
|
||||
if let Some((bw, bh, stride, pixels)) = pixels_opt {
|
||||
exported_pixels.push((window_name, bw, bh, stride, pixels));
|
||||
exported_pixels.push(crate::state::ShmExport {
|
||||
window_name,
|
||||
width: bw,
|
||||
height: bh,
|
||||
stride,
|
||||
pixels,
|
||||
damage_rects,
|
||||
});
|
||||
} else if let Some(ref ipc) = data.state.electron_ipc {
|
||||
// Try DMABUF (hardware-rendered clients / games)
|
||||
let dmabuf_opt = with_states(surface, |states| {
|
||||
|
|
@ -867,48 +906,66 @@ 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
|
||||
// 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 (window_name, w, h, stride, pixels) in exports {
|
||||
let memfd_fd = data.state
|
||||
for export in exports {
|
||||
let pair = data.state
|
||||
.window_shm_buffers
|
||||
.entry(window_name.clone())
|
||||
.entry(export.window_name.clone())
|
||||
.or_insert_with(|| {
|
||||
nix::sys::memfd::memfd_create(
|
||||
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")
|
||||
.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 size = pixels.len() as i64;
|
||||
let stat = nix::sys::stat::fstat(&*memfd_fd).unwrap();
|
||||
// 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::ftruncate(memfd_fd, size).expect("truncating memfd");
|
||||
}
|
||||
|
||||
nix::unistd::lseek(&*memfd_fd, 0, nix::unistd::Whence::SeekSet)
|
||||
nix::unistd::lseek(memfd_fd, 0, nix::unistd::Whence::SeekSet)
|
||||
.expect("seeking memfd");
|
||||
nix::unistd::write(&*memfd_fd, &pixels).expect("writing to 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(
|
||||
&window_name,
|
||||
w,
|
||||
h,
|
||||
stride,
|
||||
&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 {}: {}", window_name, e);
|
||||
debug!("DRM shm buffer broadcast failed for {}: {}", export.window_name, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Flip to the other buffer for next frame
|
||||
*idx = 1 - *idx;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1803,31 +1860,25 @@ fn run_drm(mut config: Config) -> Result<(), anyhow::Error> {
|
|||
}
|
||||
}
|
||||
|
||||
// Process frames with backpressure (8ms throttle per window)
|
||||
let now = Instant::now();
|
||||
// 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();
|
||||
|
||||
if let Some(last_ts) = data.frame_timestamps.get(&name) {
|
||||
if now.duration_since(*last_ts) < Duration::from_millis(8) {
|
||||
trace!("Dropping frame {} due to backpressure", name);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
data.frame_timestamps.insert(name.clone(), now);
|
||||
latest_frames.insert(name, frame);
|
||||
}
|
||||
}
|
||||
|
||||
// Import latest frames
|
||||
// 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.lock() {
|
||||
match renderer_arc.try_lock() {
|
||||
Ok(ref mut renderer) => {
|
||||
match import_electron_frame(renderer, frame) {
|
||||
Ok((tex, cached)) => {
|
||||
|
|
@ -1846,7 +1897,13 @@ fn run_drm(mut config: Config) -> Result<(), anyhow::Error> {
|
|||
Err(e) => warn!("DMABUF import failed for '{}': {e:#}", frame_name),
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("Failed to lock renderer: {e}"),
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
153
src/nested.rs
153
src/nested.rs
|
|
@ -747,9 +747,10 @@ pub fn run_nested(config: Config) -> Result<()> {
|
|||
drop(target);
|
||||
|
||||
// Export dirty Wayland surfaces via offscreen rendering to SHM
|
||||
let mut exported_pixels: Vec<(String, u32, u32, u32, Vec<u8>)> = Vec::new();
|
||||
let mut exported_pixels: Vec<crate::state::ShmExport> = Vec::new();
|
||||
{
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
use smithay::wayland::compositor::{with_states, Damage, SurfaceAttributes};
|
||||
|
||||
let dirty: Vec<_> = state.dirty_surfaces.drain().collect();
|
||||
|
||||
|
|
@ -788,10 +789,33 @@ pub fn run_nested(config: Config) -> Result<()> {
|
|||
|
||||
let Some(geo) = window_geo else { continue };
|
||||
|
||||
// Extract Wayland damage rects for partial-update optimization.
|
||||
// Empty damage = full frame (surfaces in dirty list always get exported).
|
||||
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()
|
||||
});
|
||||
|
||||
// Calculate bbox (bounding box) for CSD windows with shadows/decorations
|
||||
// geo.loc is the offset from surface origin to content area
|
||||
// For libadwaita: geo.loc is typically (10, 10) for 10px shadows
|
||||
// bbox needs to include the decorations, so expand by geo.loc on both sides
|
||||
let bbox_w = (geo.size.w + geo.loc.x.max(0) * 2) as u32;
|
||||
let bbox_h = (geo.size.h + geo.loc.y.max(0) * 2) as u32;
|
||||
|
||||
|
|
@ -801,9 +825,7 @@ pub fn run_nested(config: Config) -> Result<()> {
|
|||
}
|
||||
|
||||
// Read SHM buffer directly (for software-rendered clients)
|
||||
// TODO: Implement GL offscreen rendering for DMABUF/hardware clients
|
||||
use smithay::backend::renderer::utils::RendererSurfaceStateUserData;
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shm::with_buffer_contents;
|
||||
|
||||
let pixels_opt: Option<(u32, u32, u32, Vec<u8>)> =
|
||||
|
|
@ -831,7 +853,14 @@ pub fn run_nested(config: Config) -> Result<()> {
|
|||
});
|
||||
|
||||
if let Some((bw, bh, stride, pixels)) = pixels_opt {
|
||||
exported_pixels.push((window_name, bw, bh, stride, pixels));
|
||||
exported_pixels.push(crate::state::ShmExport {
|
||||
window_name,
|
||||
width: bw,
|
||||
height: bh,
|
||||
stride,
|
||||
pixels,
|
||||
damage_rects,
|
||||
});
|
||||
} else if let Some(ref ipc) = state.electron_ipc {
|
||||
// If no SHM pixels, check if it's a DMABUF (optimal for games)
|
||||
let dmabuf_opt = with_states(&surface, |states| {
|
||||
|
|
@ -970,49 +999,64 @@ pub fn run_nested(config: Config) -> Result<()> {
|
|||
});
|
||||
}
|
||||
|
||||
// Write exported pixel buffers to SHM and broadcast to Electron
|
||||
// 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 (window_name, w, h, stride, pixels) in exports {
|
||||
let memfd_fd = state
|
||||
for export in exports {
|
||||
let pair = state
|
||||
.window_shm_buffers
|
||||
.entry(window_name.clone())
|
||||
.entry(export.window_name.clone())
|
||||
.or_insert_with(|| {
|
||||
nix::sys::memfd::memfd_create(
|
||||
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")
|
||||
.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 size = pixels.len() as i64;
|
||||
let stat = nix::sys::stat::fstat(&*memfd_fd).unwrap();
|
||||
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::ftruncate(memfd_fd, size).expect("truncating memfd");
|
||||
}
|
||||
|
||||
nix::unistd::lseek(&*memfd_fd, 0, nix::unistd::Whence::SeekSet)
|
||||
nix::unistd::lseek(memfd_fd, 0, nix::unistd::Whence::SeekSet)
|
||||
.expect("seeking memfd");
|
||||
nix::unistd::write(&*memfd_fd, &pixels).expect("writing to 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(
|
||||
&window_name,
|
||||
w,
|
||||
h,
|
||||
stride,
|
||||
&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 {}: {}",
|
||||
window_name, e
|
||||
export.window_name, e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
*idx = 1 - *idx;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1356,25 +1400,26 @@ fn process_electron_message(
|
|||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let geo = window.geometry();
|
||||
let surface_x = px;
|
||||
let surface_y = py;
|
||||
// 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 = (
|
||||
loc.x as f64 + px,
|
||||
loc.y as f64 + py,
|
||||
).into();
|
||||
|
||||
// Do NOT overwrite state.pointer_location.
|
||||
// The hardware input handler already set it
|
||||
// to the authoritative position; recomputing
|
||||
// from element_location + geo causes ping-pong.
|
||||
let under = window.surface_under(
|
||||
(surface_x, surface_y),
|
||||
(px, py),
|
||||
smithay::desktop::WindowSurfaceType::ALL,
|
||||
);
|
||||
let focus = under.map(|(s, off)| {
|
||||
let surface_global: Point<f64, Logical> = (
|
||||
loc.x as f64 - geo.loc.x.max(0) as f64 + off.x as f64,
|
||||
loc.y as f64 - geo.loc.y.max(0) as f64 + off.y as f64,
|
||||
let focus_pos: Point<f64, Logical> = (
|
||||
loc.x as f64 + off.x as f64,
|
||||
loc.y as f64 + off.y as f64,
|
||||
)
|
||||
.into();
|
||||
(s, surface_global)
|
||||
(s, focus_pos)
|
||||
});
|
||||
let time = event_start_time.elapsed().as_millis() as u32;
|
||||
if let Some(pointer) = state.seat.get_pointer() {
|
||||
|
|
@ -1418,12 +1463,39 @@ fn process_electron_message(
|
|||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let geo = window.geometry();
|
||||
let surface_x = px;
|
||||
let surface_y = py;
|
||||
// Update pointer_location consistently
|
||||
state.pointer_location = (
|
||||
loc.x as f64 + px,
|
||||
loc.y as f64 + py,
|
||||
).into();
|
||||
|
||||
// Send motion first so focus is correct for the button
|
||||
let under = window.surface_under(
|
||||
(px, py),
|
||||
smithay::desktop::WindowSurfaceType::ALL,
|
||||
);
|
||||
let focus = under.map(|(s, off)| {
|
||||
let focus_pos: Point<f64, Logical> = (
|
||||
loc.x as f64 + off.x as f64,
|
||||
loc.y as f64 + off.y as f64,
|
||||
).into();
|
||||
(s, focus_pos)
|
||||
});
|
||||
let time = event_start_time.elapsed().as_millis() as u32;
|
||||
if let Some(pointer) = state.seat.get_pointer() {
|
||||
let serial = smithay::utils::SERIAL_COUNTER.next_serial();
|
||||
pointer.motion(
|
||||
state,
|
||||
focus.clone(),
|
||||
&smithay::input::pointer::MotionEvent {
|
||||
location: state.pointer_location,
|
||||
serial,
|
||||
time,
|
||||
},
|
||||
);
|
||||
pointer.frame(state);
|
||||
}
|
||||
|
||||
// Do NOT overwrite state.pointer_location
|
||||
// (same rationale as pointer_motion above).
|
||||
if pressed {
|
||||
let surface = window
|
||||
.toplevel()
|
||||
|
|
@ -1444,7 +1516,6 @@ fn process_electron_message(
|
|||
} else {
|
||||
smithay::backend::input::ButtonState::Released
|
||||
};
|
||||
let time = event_start_time.elapsed().as_millis() as u32;
|
||||
if let Some(pointer) = state.seat.get_pointer() {
|
||||
let serial = smithay::utils::SERIAL_COUNTER.next_serial();
|
||||
pointer.button(
|
||||
|
|
|
|||
29
src/state.rs
29
src/state.rs
|
|
@ -36,6 +36,25 @@ use crate::{
|
|||
use portal::WoPortal;
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
|
||||
/// A damage rectangle in buffer coordinates.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DamageRect {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub width: i32,
|
||||
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<u8>,
|
||||
pub damage_rects: Vec<DamageRect>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ClientData {
|
||||
pub compositor_state: CompositorClientState,
|
||||
|
|
@ -107,10 +126,13 @@ pub struct WoState {
|
|||
pub metadata_dirty: bool,
|
||||
/// Wayland surfaces that committed a new buffer and need offscreen capture.
|
||||
pub dirty_surfaces: std::collections::HashSet<WlSurface>,
|
||||
/// Persistent SHM file descriptors (memfds) for each window.
|
||||
pub window_shm_buffers: std::collections::HashMap<String, std::os::unix::io::OwnedFd>,
|
||||
/// 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<String, [std::os::unix::io::OwnedFd; 2]>,
|
||||
/// Tracks which of the two memfds is the current write target (0 or 1).
|
||||
pub window_shm_write_idx: std::collections::HashMap<String, usize>,
|
||||
/// Buffered pixel exports from offscreen rendering, written to SHM after render block.
|
||||
pub pending_shm_exports: Vec<(String, u32, u32, u32, Vec<u8>)>,
|
||||
pub pending_shm_exports: Vec<ShmExport>,
|
||||
pub grab_state: Option<crate::handlers::xdg_shell::GrabState>,
|
||||
pub cursor_theme_manager: CursorThemeManager,
|
||||
|
||||
|
|
@ -307,6 +329,7 @@ impl WoState {
|
|||
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(),
|
||||
grab_state: None,
|
||||
cursor_theme_manager,
|
||||
|
|
|
|||
Loading…
Reference in a new issue