Lazily enable HW accel for Electron windows

This commit is contained in:
Stephen Hellings 2026-03-10 18:01:55 -04:00
parent ca8c1246ce
commit 8b640d9f91
2 changed files with 154 additions and 76 deletions

206
electron/dist/main.js vendored
View file

@ -214,6 +214,15 @@ function debugLog(...args) {
console.log(...args); console.log(...args);
} }
debugLog("[Wo] Electron starting, NODE_VERSION=", process.version); debugLog("[Wo] Electron starting, NODE_VERSION=", process.version);
app.commandLine.appendSwitch("ignore-gpu-blocklist");
app.commandLine.appendSwitch("enable-gpu-rasterization");
app.commandLine.appendSwitch("enable-zero-copy");
app.commandLine.appendSwitch("enable-native-gpu-memory-buffers");
app.commandLine.appendSwitch("disable-software-rasterizer");
app.commandLine.appendSwitch("enable-features", "CanvasOopRasterization,Vulkan,VaapiVideoDecoder,VaapiVideoEncoder");
app.commandLine.appendSwitch("use-gl", "egl");
app.commandLine.appendSwitch("enable-accelerated-video-decode");
debugLog("[Wo] GPU acceleration flags applied");
var IPC_SOCKET = process.env.WO_IPC_SOCKET || "/run/user/1000/wo-ipc.sock"; var IPC_SOCKET = process.env.WO_IPC_SOCKET || "/run/user/1000/wo-ipc.sock";
debugLog("[Wo] WO_WINDOW_CONFIG env =", process.env.WO_WINDOW_CONFIG); debugLog("[Wo] WO_WINDOW_CONFIG env =", process.env.WO_WINDOW_CONFIG);
var WINDOW_CONFIG = JSON.parse(process.env.WO_WINDOW_CONFIG || "{}"); var WINDOW_CONFIG = JSON.parse(process.env.WO_WINDOW_CONFIG || "{}");
@ -237,11 +246,41 @@ var windowPositionUpdateTimer = null;
var dmabufSender = null; var dmabufSender = null;
var damageBuffer = null; var damageBuffer = null;
var nativeDmabuf = null; var nativeDmabuf = null;
var compositorRxBuffer = Buffer.alloc(0); var compositorRxBuffer = Buffer.alloc(64 * 1024);
var rxWriteOffset = 0;
var rxReadOffset = 0;
var surfaceBufferPending = new Map; var surfaceBufferPending = new Map;
var surfaceBufferTimer = null; var surfaceBufferFlushScheduled = false;
var shmReadBufferCache = new Map; var shmReadBufferCache = new Map;
var shmFdCache = new Map; var shmFdCache = new Map;
function rxAppend(chunk) {
const needed = rxWriteOffset + chunk.length;
if (needed > compositorRxBuffer.length) {
if (rxReadOffset > 0) {
compositorRxBuffer.copy(compositorRxBuffer, 0, rxReadOffset, rxWriteOffset);
rxWriteOffset -= rxReadOffset;
rxReadOffset = 0;
}
if (rxWriteOffset + chunk.length > compositorRxBuffer.length) {
const next = Buffer.alloc(Math.max(compositorRxBuffer.length * 2, rxWriteOffset + chunk.length));
compositorRxBuffer.copy(next, 0, 0, rxWriteOffset);
compositorRxBuffer = next;
}
}
chunk.copy(compositorRxBuffer, rxWriteOffset);
rxWriteOffset += chunk.length;
}
function rxAvailable() {
return rxWriteOffset - rxReadOffset;
}
function rxConsume(n) {
rxReadOffset += n;
if (rxReadOffset > compositorRxBuffer.length / 2) {
compositorRxBuffer.copy(compositorRxBuffer, 0, rxReadOffset, rxWriteOffset);
rxWriteOffset -= rxReadOffset;
rxReadOffset = 0;
}
}
function closeShmFdCache() { function closeShmFdCache() {
for (const entry of shmFdCache.values()) { for (const entry of shmFdCache.values()) {
try { try {
@ -275,7 +314,7 @@ function getReusableShmBuffer(windowName, size) {
return next; return next;
} }
function flushSurfaceBuffers() { function flushSurfaceBuffers() {
surfaceBufferTimer = null; surfaceBufferFlushScheduled = false;
if (!mainWindow || mainWindow.isDestroyed()) { if (!mainWindow || mainWindow.isDestroyed()) {
surfaceBufferPending.clear(); surfaceBufferPending.clear();
return; return;
@ -998,23 +1037,23 @@ function connectToCompositor() {
ipcDataLastLog = now; ipcDataLastLog = now;
} }
const dataT0 = Date.now(); const dataT0 = Date.now();
compositorRxBuffer = Buffer.concat([compositorRxBuffer, chunk]); rxAppend(chunk);
while (compositorRxBuffer.length >= 4) { while (rxAvailable() >= 4) {
const magic = compositorRxBuffer.readUInt32LE(0); const magic = compositorRxBuffer.readUInt32LE(rxReadOffset);
if (magic === MAGIC.FRAME_ACK) { if (magic === MAGIC.FRAME_ACK) {
if (compositorRxBuffer.length < 12) if (rxAvailable() < 12)
break; break;
const seq = compositorRxBuffer.readBigUInt64LE(4).toString(); const seq = compositorRxBuffer.readBigUInt64LE(rxReadOffset + 4).toString();
inFlightFrameSeqs.delete(seq); inFlightFrameSeqs.delete(seq);
compositorRxBuffer = compositorRxBuffer.slice(12); rxConsume(12);
continue; continue;
} }
if (magic === MAGIC.MOUSE_MOVE) { if (magic === MAGIC.MOUSE_MOVE) {
if (compositorRxBuffer.length < 20) if (rxAvailable() < 20)
break; break;
const x = compositorRxBuffer.readDoubleLE(4); const x = compositorRxBuffer.readDoubleLE(rxReadOffset + 4);
const y = compositorRxBuffer.readDoubleLE(12); const y = compositorRxBuffer.readDoubleLE(rxReadOffset + 12);
compositorRxBuffer = compositorRxBuffer.slice(20); rxConsume(20);
pointerX = Math.round(x); pointerX = Math.round(x);
pointerY = Math.round(y); pointerY = Math.round(y);
if (!inputFlushTimer) { if (!inputFlushTimer) {
@ -1028,11 +1067,11 @@ function connectToCompositor() {
continue; continue;
} }
if (magic === MAGIC.MOUSE_BUTTON) { if (magic === MAGIC.MOUSE_BUTTON) {
if (compositorRxBuffer.length < 16) if (rxAvailable() < 16)
break; break;
const button = compositorRxBuffer.readUInt32LE(4); const button = compositorRxBuffer.readUInt32LE(rxReadOffset + 4);
const pressed = compositorRxBuffer.readUInt32LE(8) !== 0; const pressed = compositorRxBuffer.readUInt32LE(rxReadOffset + 8) !== 0;
compositorRxBuffer = compositorRxBuffer.slice(16); rxConsume(16);
if (pendingMouseMove && mainWindow && !mainWindow.isDestroyed()) { if (pendingMouseMove && mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.sendInputEvent({ mainWindow.webContents.sendInputEvent({
type: "mouseMove", type: "mouseMove",
@ -1058,11 +1097,11 @@ function connectToCompositor() {
continue; continue;
} }
if (magic === MAGIC.KEYBOARD) { if (magic === MAGIC.KEYBOARD) {
if (compositorRxBuffer.length < 16) if (rxAvailable() < 16)
break; break;
const key = compositorRxBuffer.readUInt32LE(4); const key = compositorRxBuffer.readUInt32LE(rxReadOffset + 4);
const pressed = compositorRxBuffer.readUInt32LE(8) !== 0; const pressed = compositorRxBuffer.readUInt32LE(rxReadOffset + 8) !== 0;
compositorRxBuffer = compositorRxBuffer.slice(16); rxConsume(16);
const modKey = MODIFIER_KEYCODES[key]; const modKey = MODIFIER_KEYCODES[key];
if (modKey) { if (modKey) {
modifierState[modKey] = pressed; modifierState[modKey] = pressed;
@ -1090,11 +1129,11 @@ function connectToCompositor() {
continue; continue;
} }
if (magic === MAGIC.SCROLL) { if (magic === MAGIC.SCROLL) {
if (compositorRxBuffer.length < 16) if (rxAvailable() < 16)
break; break;
const vertical = compositorRxBuffer.readInt32LE(4); const vertical = compositorRxBuffer.readInt32LE(rxReadOffset + 4);
const horizontal = compositorRxBuffer.readInt32LE(8); const horizontal = compositorRxBuffer.readInt32LE(rxReadOffset + 8);
compositorRxBuffer = compositorRxBuffer.slice(16); rxConsume(16);
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.sendInputEvent({ mainWindow.webContents.sendInputEvent({
type: "mouseWheel", type: "mouseWheel",
@ -1108,27 +1147,27 @@ function connectToCompositor() {
continue; continue;
} }
if (magic === MAGIC.FOCUS_CHANGE) { if (magic === MAGIC.FOCUS_CHANGE) {
if (compositorRxBuffer.length < 12) if (rxAvailable() < 12)
break; break;
const nameLen = compositorRxBuffer.readUInt32LE(4); const nameLen = compositorRxBuffer.readUInt32LE(rxReadOffset + 4);
if (compositorRxBuffer.length < 12 + nameLen) if (rxAvailable() < 12 + nameLen)
break; break;
const focused = compositorRxBuffer.readUInt32LE(8) !== 0; const focused = compositorRxBuffer.readUInt32LE(rxReadOffset + 8) !== 0;
const windowName = compositorRxBuffer.slice(12, 12 + nameLen).toString("utf8"); const windowName = compositorRxBuffer.slice(rxReadOffset + 12, rxReadOffset + 12 + nameLen).toString("utf8");
compositorRxBuffer = compositorRxBuffer.slice(12 + nameLen); rxConsume(12 + nameLen);
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("wo:focus-change", { window: windowName, focused }); mainWindow.webContents.send("wo:focus-change", { window: windowName, focused });
} }
continue; continue;
} }
if (magic === MAGIC.WINDOW_META) { if (magic === MAGIC.WINDOW_META) {
if (compositorRxBuffer.length < 8) if (rxAvailable() < 8)
break; break;
const payloadLen = compositorRxBuffer.readUInt32LE(4); const payloadLen = compositorRxBuffer.readUInt32LE(rxReadOffset + 4);
if (compositorRxBuffer.length < 8 + payloadLen) if (rxAvailable() < 8 + payloadLen)
break; break;
const metadata = compositorRxBuffer.slice(8, 8 + payloadLen).toString("utf8"); const metadata = compositorRxBuffer.slice(rxReadOffset + 8, rxReadOffset + 8 + payloadLen).toString("utf8");
compositorRxBuffer = compositorRxBuffer.slice(8 + payloadLen); rxConsume(8 + payloadLen);
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("wo:window-metadata", { type: "windowMetadata", metadata }); mainWindow.webContents.send("wo:window-metadata", { type: "windowMetadata", metadata });
try { try {
@ -1142,13 +1181,13 @@ function connectToCompositor() {
continue; continue;
} }
if (magic === MAGIC.SHM_BUFFER) { if (magic === MAGIC.SHM_BUFFER) {
if (compositorRxBuffer.length < 8) if (rxAvailable() < 8)
break; break;
const nameLen = compositorRxBuffer.readUInt32LE(4); const nameLen = compositorRxBuffer.readUInt32LE(rxReadOffset + 4);
const baseHeader = 8 + nameLen + 20 + 4; const baseHeader = 8 + nameLen + 20 + 4;
if (compositorRxBuffer.length < baseHeader) if (rxAvailable() < baseHeader)
break; break;
let off = 8; let off = rxReadOffset + 8;
const windowName = compositorRxBuffer.slice(off, off + nameLen).toString("utf8"); const windowName = compositorRxBuffer.slice(off, off + nameLen).toString("utf8");
off += nameLen; off += nameLen;
const sbWidth = compositorRxBuffer.readUInt32LE(off); const sbWidth = compositorRxBuffer.readUInt32LE(off);
@ -1164,7 +1203,7 @@ function connectToCompositor() {
const numRects = compositorRxBuffer.readUInt32LE(off); const numRects = compositorRxBuffer.readUInt32LE(off);
off += 4; off += 4;
const fullHeader = baseHeader + numRects * 16; const fullHeader = baseHeader + numRects * 16;
if (compositorRxBuffer.length < fullHeader) if (rxAvailable() < fullHeader)
break; break;
const damageRects = []; const damageRects = [];
for (let i = 0;i < numRects; i++) { for (let i = 0;i < numRects; i++) {
@ -1176,7 +1215,7 @@ function connectToCompositor() {
}); });
off += 16; off += 16;
} }
compositorRxBuffer = compositorRxBuffer.slice(fullHeader); rxConsume(fullHeader);
try { try {
const extFd = getOrOpenShmFd(windowName, pid, fd); const extFd = getOrOpenShmFd(windowName, pid, fd);
const fullSize = sbStride * sbHeight; const fullSize = sbStride * sbHeight;
@ -1220,8 +1259,9 @@ function connectToCompositor() {
pixels: safeBuffer, pixels: safeBuffer,
damageRects: hasUsableRects ? damageRects : undefined damageRects: hasUsableRects ? damageRects : undefined
}); });
if (!surfaceBufferTimer) { if (!surfaceBufferFlushScheduled) {
surfaceBufferTimer = setTimeout(flushSurfaceBuffers, 10); surfaceBufferFlushScheduled = true;
setImmediate(flushSurfaceBuffers);
} }
} catch (err) { } catch (err) {
const cached = shmFdCache.get(windowName); const cached = shmFdCache.get(windowName);
@ -1236,13 +1276,13 @@ function connectToCompositor() {
continue; continue;
} }
if (magic === MAGIC.DMABUF_FRAME) { if (magic === MAGIC.DMABUF_FRAME) {
if (compositorRxBuffer.length < 8) if (rxAvailable() < 8)
break; break;
const nameLen = compositorRxBuffer.readUInt32LE(4); const nameLen = compositorRxBuffer.readUInt32LE(rxReadOffset + 4);
const numPlanesOffset = 8 + nameLen + 12; const numPlanesOffset = 8 + nameLen + 12;
if (compositorRxBuffer.length < numPlanesOffset + 4) if (rxAvailable() < numPlanesOffset + 4)
break; break;
let off = 8; let off = rxReadOffset + 8;
const dmabufName = compositorRxBuffer.slice(off, off + nameLen).toString("utf8"); const dmabufName = compositorRxBuffer.slice(off, off + nameLen).toString("utf8");
off += nameLen; off += nameLen;
const dmabufW = compositorRxBuffer.readUInt32LE(off); const dmabufW = compositorRxBuffer.readUInt32LE(off);
@ -1253,8 +1293,8 @@ function connectToCompositor() {
off += 4; off += 4;
const numPlanes = compositorRxBuffer.readUInt32LE(off); const numPlanes = compositorRxBuffer.readUInt32LE(off);
off += 4; off += 4;
const totalSize = off + numPlanes * 24; const totalSize = off - rxReadOffset + numPlanes * 24;
if (compositorRxBuffer.length < totalSize) if (rxAvailable() < totalSize)
break; break;
try { try {
if (nativeDmabuf && ipcSocket) { if (nativeDmabuf && ipcSocket) {
@ -1281,31 +1321,31 @@ function connectToCompositor() {
} catch (err) { } catch (err) {
debugLog(`[Wo] DMABUF import failed for ${dmabufName}:`, err); debugLog(`[Wo] DMABUF import failed for ${dmabufName}:`, err);
} }
compositorRxBuffer = compositorRxBuffer.slice(totalSize); rxConsume(totalSize);
continue; continue;
} }
if (magic === MAGIC.POINTER_LOCK_REQUEST) { if (magic === MAGIC.POINTER_LOCK_REQUEST) {
if (compositorRxBuffer.length < 12) if (rxAvailable() < 12)
break; break;
const nameLen = compositorRxBuffer.readUInt32LE(4); const nameLen = compositorRxBuffer.readUInt32LE(rxReadOffset + 4);
if (compositorRxBuffer.length < 12 + nameLen) if (rxAvailable() < 12 + nameLen)
break; break;
const lock = compositorRxBuffer.readUInt32LE(8) !== 0; const lock = compositorRxBuffer.readUInt32LE(rxReadOffset + 8) !== 0;
const windowName = compositorRxBuffer.slice(12, 12 + nameLen).toString("utf8"); const windowName = compositorRxBuffer.slice(rxReadOffset + 12, rxReadOffset + 12 + nameLen).toString("utf8");
compositorRxBuffer = compositorRxBuffer.slice(12 + nameLen); rxConsume(12 + nameLen);
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("wo:pointer-lock-request", { window: windowName, lock }); mainWindow.webContents.send("wo:pointer-lock-request", { window: windowName, lock });
} }
continue; continue;
} }
if (magic === MAGIC.ENV_UPDATE) { if (magic === MAGIC.ENV_UPDATE) {
if (compositorRxBuffer.length < 8) if (rxAvailable() < 8)
break; break;
const jsonLen = compositorRxBuffer.readUInt32LE(4); const jsonLen = compositorRxBuffer.readUInt32LE(rxReadOffset + 4);
if (compositorRxBuffer.length < 8 + jsonLen) if (rxAvailable() < 8 + jsonLen)
break; break;
const jsonStr = compositorRxBuffer.slice(8, 8 + jsonLen).toString("utf8"); const jsonStr = compositorRxBuffer.slice(rxReadOffset + 8, rxReadOffset + 8 + jsonLen).toString("utf8");
compositorRxBuffer = compositorRxBuffer.slice(8 + jsonLen); rxConsume(8 + jsonLen);
try { try {
const vars = JSON.parse(jsonStr); const vars = JSON.parse(jsonStr);
for (const [key, value] of Object.entries(vars)) { for (const [key, value] of Object.entries(vars)) {
@ -1321,18 +1361,18 @@ function connectToCompositor() {
continue; continue;
} }
if (magic === MAGIC.SCREENCOPY_EVENT) { if (magic === MAGIC.SCREENCOPY_EVENT) {
if (compositorRxBuffer.length < 12) if (rxAvailable() < 12)
break; break;
const active = compositorRxBuffer.readUInt32LE(4) !== 0; const active = compositorRxBuffer.readUInt32LE(rxReadOffset + 4) !== 0;
const clientCount = compositorRxBuffer.readUInt32LE(8); const clientCount = compositorRxBuffer.readUInt32LE(rxReadOffset + 8);
compositorRxBuffer = compositorRxBuffer.slice(12); rxConsume(12);
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("wo:screencopy-event", { active, clientCount }); mainWindow.webContents.send("wo:screencopy-event", { active, clientCount });
} }
continue; 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=" + rxAvailable() + " — stream may be corrupted");
compositorRxBuffer = compositorRxBuffer.slice(1); rxConsume(1);
} }
const dataElapsed = Date.now() - dataT0; const dataElapsed = Date.now() - dataT0;
if (dataElapsed > 50) { if (dataElapsed > 50) {
@ -1716,7 +1756,9 @@ async function createWindow() {
contextIsolation: true, contextIsolation: true,
preload: preloadPath, preload: preloadPath,
nodeIntegration: false, nodeIntegration: false,
offscreen: !CLIENT_MODE offscreen: !CLIENT_MODE,
webgl: true,
backgroundThrottling: false
}, },
show: CLIENT_MODE show: CLIENT_MODE
}); });
@ -1757,7 +1799,7 @@ async function createWindow() {
} }
}, frameIntervalMs); }, frameIntervalMs);
let skippedCount = 0; let skippedCount = 0;
mainWindow.webContents.on("paint", (_event, _dirty, image) => { mainWindow.webContents.on("paint", (_event, dirty, image) => {
if (!dmabufSender || !ipcConnected) { if (!dmabufSender || !ipcConnected) {
skippedCount++; skippedCount++;
debugLog("[Wo] SKIPPED paint (dmabufSender=" + !!dmabufSender + " ipcConnected=" + ipcConnected + ")"); debugLog("[Wo] SKIPPED paint (dmabufSender=" + !!dmabufSender + " ipcConnected=" + ipcConnected + ")");
@ -1778,11 +1820,33 @@ async function createWindow() {
damageBuffer = new DamageBuffer(size.width, size.height); damageBuffer = new DamageBuffer(size.width, size.height);
} }
const pixels = image.getBitmap(); const pixels = image.getBitmap();
const pixelData = new Uint8Array(pixels.buffer, pixels.byteOffset, pixels.byteLength);
const isFullFrame = dirty.x === 0 && dirty.y === 0 && dirty.width === size.width && dirty.height === size.height;
if (isFullFrame) {
applyDamagePayload(damageBuffer, { applyDamagePayload(damageBuffer, {
width: size.width, width: size.width,
height: size.height, height: size.height,
fullFrame: new Uint8Array(pixels.buffer, pixels.byteOffset, pixels.byteLength) fullFrame: pixelData
}); });
} else {
const stride = size.width * 4;
const patchStride = dirty.width * 4;
const patchData = new Uint8Array(dirty.width * dirty.height * 4);
for (let row = 0;row < dirty.height; row++) {
const srcOff = (dirty.y + row) * stride + dirty.x * 4;
const dstOff = row * patchStride;
patchData.set(pixelData.subarray(srcOff, srcOff + patchStride), dstOff);
}
applyDamagePayload(damageBuffer, {
width: size.width,
height: size.height,
patches: [{
rect: { x: dirty.x, y: dirty.y, width: dirty.width, height: dirty.height },
rgba: patchData,
stride: patchStride
}]
});
}
try { try {
const seq = dmabufSender.send(damageBuffer); const seq = dmabufSender.send(damageBuffer);
inFlightFrameSeqs.add(seq.toString()); inFlightFrameSeqs.add(seq.toString());

View file

@ -32,6 +32,18 @@ function debugLog(...args: any[]) {
debugLog('[Wo] Electron starting, NODE_VERSION=', process.version); debugLog('[Wo] Electron starting, NODE_VERSION=', process.version);
// GPU acceleration & rendering performance flags (must be set before app 'ready')
app.commandLine.appendSwitch('ignore-gpu-blocklist');
app.commandLine.appendSwitch('enable-gpu-rasterization');
app.commandLine.appendSwitch('enable-zero-copy');
app.commandLine.appendSwitch('enable-native-gpu-memory-buffers');
app.commandLine.appendSwitch('disable-software-rasterizer');
app.commandLine.appendSwitch('enable-features',
'CanvasOopRasterization,Vulkan,VaapiVideoDecoder,VaapiVideoEncoder');
app.commandLine.appendSwitch('use-gl', 'egl');
app.commandLine.appendSwitch('enable-accelerated-video-decode');
debugLog('[Wo] GPU acceleration flags applied');
type ClientWindowInfo = { type ClientWindowInfo = {
id: string; id: string;
title: string; title: string;
@ -1717,7 +1729,9 @@ async function createWindow() {
contextIsolation: true, contextIsolation: true,
preload: preloadPath, preload: preloadPath,
nodeIntegration: false, nodeIntegration: false,
offscreen: !CLIENT_MODE offscreen: !CLIENT_MODE,
webgl: true,
backgroundThrottling: false,
}, },
show: CLIENT_MODE, show: CLIENT_MODE,
}); });