Again
This commit is contained in:
parent
4fe557c536
commit
42b111410c
10 changed files with 440 additions and 142 deletions
84
electron/dist/main.js
vendored
84
electron/dist/main.js
vendored
|
|
@ -45,7 +45,8 @@ var MAGIC = {
|
||||||
FORWARD_POINTER: 1464815685,
|
FORWARD_POINTER: 1464815685,
|
||||||
FORWARD_KEYBOARD: 1464814405,
|
FORWARD_KEYBOARD: 1464814405,
|
||||||
FORWARD_RELATIVE_POINTER: 1464816197,
|
FORWARD_RELATIVE_POINTER: 1464816197,
|
||||||
POINTER_LOCK_REQUEST: 1464815692
|
POINTER_LOCK_REQUEST: 1464815692,
|
||||||
|
ENV_UPDATE: 1464812885
|
||||||
};
|
};
|
||||||
function stringToBuffer(str) {
|
function stringToBuffer(str) {
|
||||||
return Buffer.from(str, "utf-8");
|
return Buffer.from(str, "utf-8");
|
||||||
|
|
@ -1143,8 +1144,8 @@ function connectToCompositor() {
|
||||||
if (compositorRxBuffer.length < 8)
|
if (compositorRxBuffer.length < 8)
|
||||||
break;
|
break;
|
||||||
const nameLen = compositorRxBuffer.readUInt32LE(4);
|
const nameLen = compositorRxBuffer.readUInt32LE(4);
|
||||||
const headerSize = 8 + nameLen + 20;
|
const baseHeader = 8 + nameLen + 20 + 4;
|
||||||
if (compositorRxBuffer.length < headerSize)
|
if (compositorRxBuffer.length < baseHeader)
|
||||||
break;
|
break;
|
||||||
let off = 8;
|
let off = 8;
|
||||||
const windowName = compositorRxBuffer.slice(off, off + nameLen).toString("utf8");
|
const windowName = compositorRxBuffer.slice(off, off + nameLen).toString("utf8");
|
||||||
|
|
@ -1158,30 +1159,69 @@ function connectToCompositor() {
|
||||||
const pid = compositorRxBuffer.readUInt32LE(off);
|
const pid = compositorRxBuffer.readUInt32LE(off);
|
||||||
off += 4;
|
off += 4;
|
||||||
const fd = compositorRxBuffer.readUInt32LE(off);
|
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 {
|
try {
|
||||||
const extFd = getOrOpenShmFd(windowName, pid, fd);
|
const extFd = getOrOpenShmFd(windowName, pid, fd);
|
||||||
const size = sbStride * sbHeight;
|
const fullSize = sbStride * sbHeight;
|
||||||
const safeBuffer = getReusableShmBuffer(windowName, size);
|
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;
|
let totalRead = 0;
|
||||||
while (totalRead < size) {
|
while (totalRead < readLen) {
|
||||||
const bytesRead = fs.readSync(extFd, safeBuffer, totalRead, size - totalRead, totalRead);
|
const bytesRead = fs.readSync(extFd, safeBuffer, readOffset + totalRead, readLen - totalRead, readOffset + totalRead);
|
||||||
if (bytesRead <= 0)
|
if (bytesRead <= 0)
|
||||||
break;
|
break;
|
||||||
totalRead += bytesRead;
|
totalRead += bytesRead;
|
||||||
}
|
}
|
||||||
if (totalRead === size) {
|
}
|
||||||
|
} 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, {
|
surfaceBufferPending.set(windowName, {
|
||||||
name: windowName,
|
name: windowName,
|
||||||
width: sbWidth,
|
width: sbWidth,
|
||||||
height: sbHeight,
|
height: sbHeight,
|
||||||
stride: sbStride,
|
stride: sbStride,
|
||||||
pixels: safeBuffer
|
pixels: safeBuffer,
|
||||||
|
damageRects: hasUsableRects ? damageRects : undefined
|
||||||
});
|
});
|
||||||
if (!surfaceBufferTimer) {
|
if (!surfaceBufferTimer) {
|
||||||
surfaceBufferTimer = setTimeout(flushSurfaceBuffers, 10);
|
surfaceBufferTimer = setTimeout(flushSurfaceBuffers, 10);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const cached = shmFdCache.get(windowName);
|
const cached = shmFdCache.get(windowName);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
|
|
@ -1257,6 +1297,28 @@ function connectToCompositor() {
|
||||||
}
|
}
|
||||||
continue;
|
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");
|
debugLog("[Wo] UNKNOWN magic 0x" + magic.toString(16).padStart(8, "0") + " bufLen=" + compositorRxBuffer.length + " — stream may be corrupted");
|
||||||
compositorRxBuffer = compositorRxBuffer.slice(1);
|
compositorRxBuffer = compositorRxBuffer.slice(1);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ let damageBuffer: DamageBuffer | null = null;
|
||||||
let nativeDmabuf: any = null;
|
let nativeDmabuf: any = null;
|
||||||
let compositorRxBuffer = Buffer.alloc(0);
|
let compositorRxBuffer = Buffer.alloc(0);
|
||||||
// Per-window surface buffer throttle (~30fps max delivery to renderer)
|
// 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;
|
let surfaceBufferTimer: NodeJS.Timeout | null = null;
|
||||||
const shmReadBufferCache = new Map<string, Buffer>();
|
const shmReadBufferCache = new Map<string, Buffer>();
|
||||||
const shmFdCache = new Map<string, { pid: number; fd: number; extFd: number }>();
|
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 (magic === MAGIC.SHM_BUFFER) {
|
||||||
if (compositorRxBuffer.length < 8) break;
|
if (compositorRxBuffer.length < 8) break;
|
||||||
const nameLen = compositorRxBuffer.readUInt32LE(4);
|
const nameLen = compositorRxBuffer.readUInt32LE(4);
|
||||||
const headerSize = 8 + nameLen + 20;
|
// Minimum header before we know num_rects
|
||||||
if (compositorRxBuffer.length < headerSize) break;
|
const baseHeader = 8 + nameLen + 20 + 4; // +4 for num_rects field
|
||||||
|
if (compositorRxBuffer.length < baseHeader) break;
|
||||||
let off = 8;
|
let off = 8;
|
||||||
const windowName = compositorRxBuffer.slice(off, off + nameLen).toString('utf8');
|
const windowName = compositorRxBuffer.slice(off, off + nameLen).toString('utf8');
|
||||||
off += nameLen;
|
off += nameLen;
|
||||||
|
|
@ -1053,32 +1054,71 @@ function connectToCompositor(): Promise<void> {
|
||||||
const sbHeight = compositorRxBuffer.readUInt32LE(off); off += 4;
|
const sbHeight = compositorRxBuffer.readUInt32LE(off); off += 4;
|
||||||
const sbStride = compositorRxBuffer.readUInt32LE(off); off += 4;
|
const sbStride = compositorRxBuffer.readUInt32LE(off); off += 4;
|
||||||
const pid = compositorRxBuffer.readUInt32LE(off); off += 4;
|
const pid = compositorRxBuffer.readUInt32LE(off); off += 4;
|
||||||
const fd = compositorRxBuffer.readUInt32LE(off);
|
const fd = compositorRxBuffer.readUInt32LE(off); off += 4;
|
||||||
compositorRxBuffer = compositorRxBuffer.slice(headerSize);
|
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 {
|
try {
|
||||||
const extFd = getOrOpenShmFd(windowName, pid, fd);
|
const extFd = getOrOpenShmFd(windowName, pid, fd);
|
||||||
const size = sbStride * sbHeight;
|
const fullSize = sbStride * sbHeight;
|
||||||
const safeBuffer = getReusableShmBuffer(windowName, size);
|
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;
|
let totalRead = 0;
|
||||||
while (totalRead < size) {
|
while (totalRead < readLen) {
|
||||||
const bytesRead = fs.readSync(extFd, safeBuffer, totalRead, size - totalRead, totalRead);
|
const bytesRead = fs.readSync(extFd, safeBuffer, readOffset + totalRead, readLen - totalRead, readOffset + totalRead);
|
||||||
if (bytesRead <= 0) break;
|
if (bytesRead <= 0) break;
|
||||||
totalRead += bytesRead;
|
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, {
|
surfaceBufferPending.set(windowName, {
|
||||||
name: windowName,
|
name: windowName,
|
||||||
width: sbWidth,
|
width: sbWidth,
|
||||||
height: sbHeight,
|
height: sbHeight,
|
||||||
stride: sbStride,
|
stride: sbStride,
|
||||||
pixels: safeBuffer,
|
pixels: safeBuffer,
|
||||||
|
damageRects: hasUsableRects ? damageRects : undefined,
|
||||||
});
|
});
|
||||||
if (!surfaceBufferTimer) {
|
if (!surfaceBufferTimer) {
|
||||||
surfaceBufferTimer = setTimeout(flushSurfaceBuffers, 10);
|
surfaceBufferTimer = setTimeout(flushSurfaceBuffers, 10);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const cached = shmFdCache.get(windowName);
|
const cached = shmFdCache.get(windowName);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,8 @@ const compositorAPI: CompositorAPI = {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Surface buffer: Wayland window pixel content from compositor
|
// Surface buffer: Wayland window pixel content from compositor
|
||||||
onSurfaceBuffer: (callback: (data: { name: string; width: number; height: number; stride: number; pixels: Buffer }) => void) => {
|
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 }) =>
|
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);
|
callback(data);
|
||||||
ipcRenderer.on('wo:surface-buffer', handler);
|
ipcRenderer.on('wo:surface-buffer', handler);
|
||||||
return () => ipcRenderer.removeListener('wo:surface-buffer', handler);
|
return () => ipcRenderer.removeListener('wo:surface-buffer', handler);
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ export type {
|
||||||
WindowMetadataEvent,
|
WindowMetadataEvent,
|
||||||
FocusChangeEvent,
|
FocusChangeEvent,
|
||||||
DamageFramePayload,
|
DamageFramePayload,
|
||||||
|
DamageRect,
|
||||||
|
DamagePatch,
|
||||||
RendererDamageHelper,
|
RendererDamageHelper,
|
||||||
RendererDamageHelperOptions,
|
RendererDamageHelperOptions,
|
||||||
} from './types';
|
} 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;
|
height: number;
|
||||||
stride: number;
|
stride: number;
|
||||||
pixels: Uint8Array;
|
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)
|
/// 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>
|
/// The compositor PID and fd number let Electron open /proc/<pid>/fd/<fd>
|
||||||
/// to mmap pixels without copying.
|
/// 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(
|
pub fn send_shm_buffer(
|
||||||
&self,
|
&self,
|
||||||
window_name: &str,
|
window_name: &str,
|
||||||
|
|
@ -235,9 +238,11 @@ impl ElectronClientConnection {
|
||||||
stride: u32,
|
stride: u32,
|
||||||
pid: u32,
|
pid: u32,
|
||||||
memfd_fd: i32,
|
memfd_fd: i32,
|
||||||
|
damage_rects: &[crate::state::DamageRect],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let name_bytes = window_name.as_bytes();
|
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 buf = vec![0u8; header_len];
|
||||||
let mut off = 0;
|
let mut off = 0;
|
||||||
buf[off..off + 4].copy_from_slice(&MAGIC_SHM_BUFFER.to_le_bytes());
|
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());
|
buf[off..off + 4].copy_from_slice(&pid.to_le_bytes());
|
||||||
off += 4;
|
off += 4;
|
||||||
buf[off..off + 4].copy_from_slice(&(memfd_fd as u32).to_le_bytes());
|
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)
|
self.write_bytes_nonblocking(&buf)
|
||||||
}
|
}
|
||||||
|
|
@ -627,7 +645,10 @@ impl ElectronIpc {
|
||||||
.with_context(|| format!("binding IPC socket {socket_path}"))?;
|
.with_context(|| format!("binding IPC socket {socket_path}"))?;
|
||||||
info!(socket = socket_path, "IPC socket listening");
|
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 tx2 = tx.clone();
|
||||||
let clients = Arc::new(Mutex::new(HashMap::new()));
|
let clients = Arc::new(Mutex::new(HashMap::new()));
|
||||||
let clients2 = clients.clone();
|
let clients2 = clients.clone();
|
||||||
|
|
@ -777,6 +798,7 @@ impl ElectronIpc {
|
||||||
stride: u32,
|
stride: u32,
|
||||||
pid: u32,
|
pid: u32,
|
||||||
memfd_fd: i32,
|
memfd_fd: i32,
|
||||||
|
damage_rects: &[crate::state::DamageRect],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let snapshot: Vec<_> = {
|
let snapshot: Vec<_> = {
|
||||||
let clients = self.clients.lock().unwrap();
|
let clients = self.clients.lock().unwrap();
|
||||||
|
|
@ -784,7 +806,7 @@ impl ElectronIpc {
|
||||||
};
|
};
|
||||||
// clients Mutex released — safe to do I/O.
|
// clients Mutex released — safe to do I/O.
|
||||||
for client in &snapshot {
|
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);
|
debug!("shm buffer to '{}' dropped: {e:#}", client.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -408,10 +408,19 @@ impl WoState {
|
||||||
|
|
||||||
if let Some(ref win_name) = under_window {
|
if let Some(ref win_name) = under_window {
|
||||||
if button_state == ButtonState::Pressed {
|
if button_state == ButtonState::Pressed {
|
||||||
|
// 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());
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref ipc) = self.electron_ipc {
|
if let Some(ref ipc) = self.electron_ipc {
|
||||||
|
|
@ -520,23 +529,34 @@ impl WoState {
|
||||||
None => return,
|
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() {
|
let pointer = match self.seat.get_pointer() {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
let serial = SERIAL_COUNTER.next_serial();
|
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
|
let focus = window
|
||||||
.surface_under(Point::<f64, Logical>::from((x, y)), WindowSurfaceType::ALL)
|
.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(
|
pointer.motion(
|
||||||
self,
|
self,
|
||||||
|
|
|
||||||
125
src/main.rs
125
src/main.rs
|
|
@ -83,6 +83,9 @@ struct EventLoopData {
|
||||||
autostart_tasks: Vec<(crate::config::AutostartConfig, std::time::Instant)>,
|
autostart_tasks: Vec<(crate::config::AutostartConfig, std::time::Instant)>,
|
||||||
first_render: bool,
|
first_render: bool,
|
||||||
wayland_socket: ListeningSocket,
|
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>,
|
frame_timestamps: std::collections::HashMap<String, Instant>,
|
||||||
gles_tex_cache: std::collections::HashMap<String, smithay::backend::renderer::gles::GlesTexture>,
|
gles_tex_cache: std::collections::HashMap<String, smithay::backend::renderer::gles::GlesTexture>,
|
||||||
input_events_pending: bool,
|
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() {
|
if data.state.electron_ipc.is_some() {
|
||||||
use smithay::backend::renderer::utils::RendererSurfaceStateUserData;
|
use smithay::backend::renderer::utils::RendererSurfaceStateUserData;
|
||||||
use smithay::reexports::wayland_server::Resource;
|
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 smithay::wayland::shm::with_buffer_contents;
|
||||||
use tracing::debug;
|
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 {
|
for surface in &dirty_surfaces {
|
||||||
let surface_id = surface.id();
|
let surface_id = surface.id();
|
||||||
|
|
@ -766,6 +771,33 @@ fn render_frame(data: &mut EventLoopData) {
|
||||||
|
|
||||||
let Some(_geo) = window_geo else { continue };
|
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)
|
// Try SHM buffer first (software-rendered clients)
|
||||||
let pixels_opt: Option<(u32, u32, u32, Vec<u8>)> =
|
let pixels_opt: Option<(u32, u32, u32, Vec<u8>)> =
|
||||||
with_states(surface, |states| {
|
with_states(surface, |states| {
|
||||||
|
|
@ -792,7 +824,14 @@ fn render_frame(data: &mut EventLoopData) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some((bw, bh, stride, pixels)) = pixels_opt {
|
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 {
|
} else if let Some(ref ipc) = data.state.electron_ipc {
|
||||||
// Try DMABUF (hardware-rendered clients / games)
|
// Try DMABUF (hardware-rendered clients / games)
|
||||||
let dmabuf_opt = with_states(surface, |states| {
|
let dmabuf_opt = with_states(surface, |states| {
|
||||||
|
|
@ -867,48 +906,66 @@ fn render_frame(data: &mut EventLoopData) {
|
||||||
data.state.damaged_windows.clear();
|
data.state.damaged_windows.clear();
|
||||||
data.state.dirty_surfaces.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 std::os::unix::io::AsRawFd;
|
||||||
use nix::sys::memfd::MFdFlags;
|
use nix::sys::memfd::MFdFlags;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
let exports = std::mem::take(&mut data.state.pending_shm_exports);
|
let exports = std::mem::take(&mut data.state.pending_shm_exports);
|
||||||
for (window_name, w, h, stride, pixels) in exports {
|
for export in exports {
|
||||||
let memfd_fd = data.state
|
let pair = data.state
|
||||||
.window_shm_buffers
|
.window_shm_buffers
|
||||||
.entry(window_name.clone())
|
.entry(export.window_name.clone())
|
||||||
.or_insert_with(|| {
|
.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(),
|
std::ffi::CStr::from_bytes_with_nul(b"wayland_window\0").unwrap(),
|
||||||
MFdFlags::MFD_CLOEXEC,
|
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;
|
// Pick the write slot (alternates each frame)
|
||||||
let stat = nix::sys::stat::fstat(&*memfd_fd).unwrap();
|
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 {
|
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");
|
.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 {
|
if let Some(ref ipc) = data.state.electron_ipc {
|
||||||
let pid = std::process::id();
|
let pid = std::process::id();
|
||||||
if let Err(e) = ipc.broadcast_shm_buffer(
|
if let Err(e) = ipc.broadcast_shm_buffer(
|
||||||
&window_name,
|
&export.window_name,
|
||||||
w,
|
export.width,
|
||||||
h,
|
export.height,
|
||||||
stride,
|
export.stride,
|
||||||
pid,
|
pid,
|
||||||
memfd_fd.as_raw_fd() as i32,
|
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)
|
// Coalesce frames: keep only the latest frame per window.
|
||||||
let now = Instant::now();
|
// 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();
|
let mut latest_frames = std::collections::HashMap::new();
|
||||||
for msg in frames_batch {
|
for msg in frames_batch {
|
||||||
if let crate::electron::ElectronMessage::Frame(frame) = msg {
|
if let crate::electron::ElectronMessage::Frame(frame) = msg {
|
||||||
let name = frame.name.clone();
|
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);
|
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 {
|
for (_name, frame) in latest_frames {
|
||||||
let frame_name = frame.name.clone();
|
let frame_name = frame.name.clone();
|
||||||
let frame_dims = (frame.width, frame.height);
|
let frame_dims = (frame.width, frame.height);
|
||||||
if let Some(renderer_arc) = data.state.backend.renderer() {
|
if let Some(renderer_arc) = data.state.backend.renderer() {
|
||||||
match renderer_arc.lock() {
|
match renderer_arc.try_lock() {
|
||||||
Ok(ref mut renderer) => {
|
Ok(ref mut renderer) => {
|
||||||
match import_electron_frame(renderer, frame) {
|
match import_electron_frame(renderer, frame) {
|
||||||
Ok((tex, cached)) => {
|
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!("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);
|
drop(target);
|
||||||
|
|
||||||
// Export dirty Wayland surfaces via offscreen rendering to SHM
|
// 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::reexports::wayland_server::Resource;
|
||||||
|
use smithay::wayland::compositor::{with_states, Damage, SurfaceAttributes};
|
||||||
|
|
||||||
let dirty: Vec<_> = state.dirty_surfaces.drain().collect();
|
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 };
|
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
|
// 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_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;
|
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)
|
// 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::backend::renderer::utils::RendererSurfaceStateUserData;
|
||||||
use smithay::wayland::compositor::with_states;
|
|
||||||
use smithay::wayland::shm::with_buffer_contents;
|
use smithay::wayland::shm::with_buffer_contents;
|
||||||
|
|
||||||
let pixels_opt: Option<(u32, u32, u32, Vec<u8>)> =
|
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 {
|
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 {
|
} else if let Some(ref ipc) = state.electron_ipc {
|
||||||
// If no SHM pixels, check if it's a DMABUF (optimal for games)
|
// If no SHM pixels, check if it's a DMABUF (optimal for games)
|
||||||
let dmabuf_opt = with_states(&surface, |states| {
|
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 {
|
if did_bind {
|
||||||
use std::os::unix::io::AsRawFd;
|
use std::os::unix::io::AsRawFd;
|
||||||
|
|
||||||
let exports = std::mem::take(&mut state.pending_shm_exports);
|
let exports = std::mem::take(&mut state.pending_shm_exports);
|
||||||
for (window_name, w, h, stride, pixels) in exports {
|
for export in exports {
|
||||||
let memfd_fd = state
|
let pair = state
|
||||||
.window_shm_buffers
|
.window_shm_buffers
|
||||||
.entry(window_name.clone())
|
.entry(export.window_name.clone())
|
||||||
.or_insert_with(|| {
|
.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(),
|
std::ffi::CStr::from_bytes_with_nul(b"wayland_window\0").unwrap(),
|
||||||
MFdFlags::MFD_CLOEXEC,
|
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 idx = state.window_shm_write_idx
|
||||||
let stat = nix::sys::stat::fstat(&*memfd_fd).unwrap();
|
.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 {
|
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");
|
.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 {
|
if let Some(ref ipc) = state.electron_ipc {
|
||||||
let pid = std::process::id();
|
let pid = std::process::id();
|
||||||
if let Err(e) = ipc.broadcast_shm_buffer(
|
if let Err(e) = ipc.broadcast_shm_buffer(
|
||||||
&window_name,
|
&export.window_name,
|
||||||
w,
|
export.width,
|
||||||
h,
|
export.height,
|
||||||
stride,
|
export.stride,
|
||||||
pid,
|
pid,
|
||||||
memfd_fd.as_raw_fd() as i32,
|
memfd_fd.as_raw_fd() as i32,
|
||||||
|
&export.damage_rects,
|
||||||
) {
|
) {
|
||||||
debug!(
|
debug!(
|
||||||
"nested shm buffer broadcast failed for {}: {}",
|
"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())
|
.and_then(|v| v.as_f64())
|
||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
let geo = window.geometry();
|
// Derive pointer_location from the window's
|
||||||
let surface_x = px;
|
// compositor-space position so focus math is
|
||||||
let surface_y = py;
|
// 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(
|
let under = window.surface_under(
|
||||||
(surface_x, surface_y),
|
(px, py),
|
||||||
smithay::desktop::WindowSurfaceType::ALL,
|
smithay::desktop::WindowSurfaceType::ALL,
|
||||||
);
|
);
|
||||||
let focus = under.map(|(s, off)| {
|
let focus = under.map(|(s, off)| {
|
||||||
let surface_global: Point<f64, Logical> = (
|
let focus_pos: Point<f64, Logical> = (
|
||||||
loc.x as f64 - geo.loc.x.max(0) as f64 + off.x as f64,
|
loc.x as f64 + off.x as f64,
|
||||||
loc.y as f64 - geo.loc.y.max(0) as f64 + off.y as f64,
|
loc.y as f64 + off.y as f64,
|
||||||
)
|
)
|
||||||
.into();
|
.into();
|
||||||
(s, surface_global)
|
(s, focus_pos)
|
||||||
});
|
});
|
||||||
let time = event_start_time.elapsed().as_millis() as u32;
|
let time = event_start_time.elapsed().as_millis() as u32;
|
||||||
if let Some(pointer) = state.seat.get_pointer() {
|
if let Some(pointer) = state.seat.get_pointer() {
|
||||||
|
|
@ -1418,12 +1463,39 @@ fn process_electron_message(
|
||||||
.and_then(|v| v.as_f64())
|
.and_then(|v| v.as_f64())
|
||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
let geo = window.geometry();
|
// Update pointer_location consistently
|
||||||
let surface_x = px;
|
state.pointer_location = (
|
||||||
let surface_y = py;
|
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 {
|
if pressed {
|
||||||
let surface = window
|
let surface = window
|
||||||
.toplevel()
|
.toplevel()
|
||||||
|
|
@ -1444,7 +1516,6 @@ fn process_electron_message(
|
||||||
} else {
|
} else {
|
||||||
smithay::backend::input::ButtonState::Released
|
smithay::backend::input::ButtonState::Released
|
||||||
};
|
};
|
||||||
let time = event_start_time.elapsed().as_millis() as u32;
|
|
||||||
if let Some(pointer) = state.seat.get_pointer() {
|
if let Some(pointer) = state.seat.get_pointer() {
|
||||||
let serial = smithay::utils::SERIAL_COUNTER.next_serial();
|
let serial = smithay::utils::SERIAL_COUNTER.next_serial();
|
||||||
pointer.button(
|
pointer.button(
|
||||||
|
|
|
||||||
29
src/state.rs
29
src/state.rs
|
|
@ -36,6 +36,25 @@ use crate::{
|
||||||
use portal::WoPortal;
|
use portal::WoPortal;
|
||||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
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)]
|
#[derive(Default)]
|
||||||
pub struct ClientData {
|
pub struct ClientData {
|
||||||
pub compositor_state: CompositorClientState,
|
pub compositor_state: CompositorClientState,
|
||||||
|
|
@ -107,10 +126,13 @@ pub struct WoState {
|
||||||
pub metadata_dirty: bool,
|
pub metadata_dirty: bool,
|
||||||
/// Wayland surfaces that committed a new buffer and need offscreen capture.
|
/// Wayland surfaces that committed a new buffer and need offscreen capture.
|
||||||
pub dirty_surfaces: std::collections::HashSet<WlSurface>,
|
pub dirty_surfaces: std::collections::HashSet<WlSurface>,
|
||||||
/// Persistent SHM file descriptors (memfds) for each window.
|
/// Ping-pong SHM memfd pairs per window. Index 0 is being written by
|
||||||
pub window_shm_buffers: std::collections::HashMap<String, std::os::unix::io::OwnedFd>,
|
/// 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.
|
/// 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 grab_state: Option<crate::handlers::xdg_shell::GrabState>,
|
||||||
pub cursor_theme_manager: CursorThemeManager,
|
pub cursor_theme_manager: CursorThemeManager,
|
||||||
|
|
||||||
|
|
@ -307,6 +329,7 @@ impl WoState {
|
||||||
metadata_dirty: true,
|
metadata_dirty: true,
|
||||||
dirty_surfaces: std::collections::HashSet::new(),
|
dirty_surfaces: std::collections::HashSet::new(),
|
||||||
window_shm_buffers: std::collections::HashMap::new(),
|
window_shm_buffers: std::collections::HashMap::new(),
|
||||||
|
window_shm_write_idx: std::collections::HashMap::new(),
|
||||||
pending_shm_exports: Vec::new(),
|
pending_shm_exports: Vec::new(),
|
||||||
grab_state: None,
|
grab_state: None,
|
||||||
cursor_theme_manager,
|
cursor_theme_manager,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue