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);
}
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";
debugLog("[Wo] WO_WINDOW_CONFIG env =", 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 damageBuffer = 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 surfaceBufferTimer = null;
var surfaceBufferFlushScheduled = false;
var shmReadBufferCache = 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() {
for (const entry of shmFdCache.values()) {
try {
@ -275,7 +314,7 @@ function getReusableShmBuffer(windowName, size) {
return next;
}
function flushSurfaceBuffers() {
surfaceBufferTimer = null;
surfaceBufferFlushScheduled = false;
if (!mainWindow || mainWindow.isDestroyed()) {
surfaceBufferPending.clear();
return;
@ -998,23 +1037,23 @@ function connectToCompositor() {
ipcDataLastLog = now;
}
const dataT0 = Date.now();
compositorRxBuffer = Buffer.concat([compositorRxBuffer, chunk]);
while (compositorRxBuffer.length >= 4) {
const magic = compositorRxBuffer.readUInt32LE(0);
rxAppend(chunk);
while (rxAvailable() >= 4) {
const magic = compositorRxBuffer.readUInt32LE(rxReadOffset);
if (magic === MAGIC.FRAME_ACK) {
if (compositorRxBuffer.length < 12)
if (rxAvailable() < 12)
break;
const seq = compositorRxBuffer.readBigUInt64LE(4).toString();
const seq = compositorRxBuffer.readBigUInt64LE(rxReadOffset + 4).toString();
inFlightFrameSeqs.delete(seq);
compositorRxBuffer = compositorRxBuffer.slice(12);
rxConsume(12);
continue;
}
if (magic === MAGIC.MOUSE_MOVE) {
if (compositorRxBuffer.length < 20)
if (rxAvailable() < 20)
break;
const x = compositorRxBuffer.readDoubleLE(4);
const y = compositorRxBuffer.readDoubleLE(12);
compositorRxBuffer = compositorRxBuffer.slice(20);
const x = compositorRxBuffer.readDoubleLE(rxReadOffset + 4);
const y = compositorRxBuffer.readDoubleLE(rxReadOffset + 12);
rxConsume(20);
pointerX = Math.round(x);
pointerY = Math.round(y);
if (!inputFlushTimer) {
@ -1028,11 +1067,11 @@ function connectToCompositor() {
continue;
}
if (magic === MAGIC.MOUSE_BUTTON) {
if (compositorRxBuffer.length < 16)
if (rxAvailable() < 16)
break;
const button = compositorRxBuffer.readUInt32LE(4);
const pressed = compositorRxBuffer.readUInt32LE(8) !== 0;
compositorRxBuffer = compositorRxBuffer.slice(16);
const button = compositorRxBuffer.readUInt32LE(rxReadOffset + 4);
const pressed = compositorRxBuffer.readUInt32LE(rxReadOffset + 8) !== 0;
rxConsume(16);
if (pendingMouseMove && mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.sendInputEvent({
type: "mouseMove",
@ -1058,11 +1097,11 @@ function connectToCompositor() {
continue;
}
if (magic === MAGIC.KEYBOARD) {
if (compositorRxBuffer.length < 16)
if (rxAvailable() < 16)
break;
const key = compositorRxBuffer.readUInt32LE(4);
const pressed = compositorRxBuffer.readUInt32LE(8) !== 0;
compositorRxBuffer = compositorRxBuffer.slice(16);
const key = compositorRxBuffer.readUInt32LE(rxReadOffset + 4);
const pressed = compositorRxBuffer.readUInt32LE(rxReadOffset + 8) !== 0;
rxConsume(16);
const modKey = MODIFIER_KEYCODES[key];
if (modKey) {
modifierState[modKey] = pressed;
@ -1090,11 +1129,11 @@ function connectToCompositor() {
continue;
}
if (magic === MAGIC.SCROLL) {
if (compositorRxBuffer.length < 16)
if (rxAvailable() < 16)
break;
const vertical = compositorRxBuffer.readInt32LE(4);
const horizontal = compositorRxBuffer.readInt32LE(8);
compositorRxBuffer = compositorRxBuffer.slice(16);
const vertical = compositorRxBuffer.readInt32LE(rxReadOffset + 4);
const horizontal = compositorRxBuffer.readInt32LE(rxReadOffset + 8);
rxConsume(16);
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.sendInputEvent({
type: "mouseWheel",
@ -1108,27 +1147,27 @@ function connectToCompositor() {
continue;
}
if (magic === MAGIC.FOCUS_CHANGE) {
if (compositorRxBuffer.length < 12)
if (rxAvailable() < 12)
break;
const nameLen = compositorRxBuffer.readUInt32LE(4);
if (compositorRxBuffer.length < 12 + nameLen)
const nameLen = compositorRxBuffer.readUInt32LE(rxReadOffset + 4);
if (rxAvailable() < 12 + nameLen)
break;
const focused = compositorRxBuffer.readUInt32LE(8) !== 0;
const windowName = compositorRxBuffer.slice(12, 12 + nameLen).toString("utf8");
compositorRxBuffer = compositorRxBuffer.slice(12 + nameLen);
const focused = compositorRxBuffer.readUInt32LE(rxReadOffset + 8) !== 0;
const windowName = compositorRxBuffer.slice(rxReadOffset + 12, rxReadOffset + 12 + nameLen).toString("utf8");
rxConsume(12 + nameLen);
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("wo:focus-change", { window: windowName, focused });
}
continue;
}
if (magic === MAGIC.WINDOW_META) {
if (compositorRxBuffer.length < 8)
if (rxAvailable() < 8)
break;
const payloadLen = compositorRxBuffer.readUInt32LE(4);
if (compositorRxBuffer.length < 8 + payloadLen)
const payloadLen = compositorRxBuffer.readUInt32LE(rxReadOffset + 4);
if (rxAvailable() < 8 + payloadLen)
break;
const metadata = compositorRxBuffer.slice(8, 8 + payloadLen).toString("utf8");
compositorRxBuffer = compositorRxBuffer.slice(8 + payloadLen);
const metadata = compositorRxBuffer.slice(rxReadOffset + 8, rxReadOffset + 8 + payloadLen).toString("utf8");
rxConsume(8 + payloadLen);
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("wo:window-metadata", { type: "windowMetadata", metadata });
try {
@ -1142,13 +1181,13 @@ function connectToCompositor() {
continue;
}
if (magic === MAGIC.SHM_BUFFER) {
if (compositorRxBuffer.length < 8)
if (rxAvailable() < 8)
break;
const nameLen = compositorRxBuffer.readUInt32LE(4);
const nameLen = compositorRxBuffer.readUInt32LE(rxReadOffset + 4);
const baseHeader = 8 + nameLen + 20 + 4;
if (compositorRxBuffer.length < baseHeader)
if (rxAvailable() < baseHeader)
break;
let off = 8;
let off = rxReadOffset + 8;
const windowName = compositorRxBuffer.slice(off, off + nameLen).toString("utf8");
off += nameLen;
const sbWidth = compositorRxBuffer.readUInt32LE(off);
@ -1164,7 +1203,7 @@ function connectToCompositor() {
const numRects = compositorRxBuffer.readUInt32LE(off);
off += 4;
const fullHeader = baseHeader + numRects * 16;
if (compositorRxBuffer.length < fullHeader)
if (rxAvailable() < fullHeader)
break;
const damageRects = [];
for (let i = 0;i < numRects; i++) {
@ -1176,7 +1215,7 @@ function connectToCompositor() {
});
off += 16;
}
compositorRxBuffer = compositorRxBuffer.slice(fullHeader);
rxConsume(fullHeader);
try {
const extFd = getOrOpenShmFd(windowName, pid, fd);
const fullSize = sbStride * sbHeight;
@ -1220,8 +1259,9 @@ function connectToCompositor() {
pixels: safeBuffer,
damageRects: hasUsableRects ? damageRects : undefined
});
if (!surfaceBufferTimer) {
surfaceBufferTimer = setTimeout(flushSurfaceBuffers, 10);
if (!surfaceBufferFlushScheduled) {
surfaceBufferFlushScheduled = true;
setImmediate(flushSurfaceBuffers);
}
} catch (err) {
const cached = shmFdCache.get(windowName);
@ -1236,13 +1276,13 @@ function connectToCompositor() {
continue;
}
if (magic === MAGIC.DMABUF_FRAME) {
if (compositorRxBuffer.length < 8)
if (rxAvailable() < 8)
break;
const nameLen = compositorRxBuffer.readUInt32LE(4);
const nameLen = compositorRxBuffer.readUInt32LE(rxReadOffset + 4);
const numPlanesOffset = 8 + nameLen + 12;
if (compositorRxBuffer.length < numPlanesOffset + 4)
if (rxAvailable() < numPlanesOffset + 4)
break;
let off = 8;
let off = rxReadOffset + 8;
const dmabufName = compositorRxBuffer.slice(off, off + nameLen).toString("utf8");
off += nameLen;
const dmabufW = compositorRxBuffer.readUInt32LE(off);
@ -1253,8 +1293,8 @@ function connectToCompositor() {
off += 4;
const numPlanes = compositorRxBuffer.readUInt32LE(off);
off += 4;
const totalSize = off + numPlanes * 24;
if (compositorRxBuffer.length < totalSize)
const totalSize = off - rxReadOffset + numPlanes * 24;
if (rxAvailable() < totalSize)
break;
try {
if (nativeDmabuf && ipcSocket) {
@ -1281,31 +1321,31 @@ function connectToCompositor() {
} catch (err) {
debugLog(`[Wo] DMABUF import failed for ${dmabufName}:`, err);
}
compositorRxBuffer = compositorRxBuffer.slice(totalSize);
rxConsume(totalSize);
continue;
}
if (magic === MAGIC.POINTER_LOCK_REQUEST) {
if (compositorRxBuffer.length < 12)
if (rxAvailable() < 12)
break;
const nameLen = compositorRxBuffer.readUInt32LE(4);
if (compositorRxBuffer.length < 12 + nameLen)
const nameLen = compositorRxBuffer.readUInt32LE(rxReadOffset + 4);
if (rxAvailable() < 12 + nameLen)
break;
const lock = compositorRxBuffer.readUInt32LE(8) !== 0;
const windowName = compositorRxBuffer.slice(12, 12 + nameLen).toString("utf8");
compositorRxBuffer = compositorRxBuffer.slice(12 + nameLen);
const lock = compositorRxBuffer.readUInt32LE(rxReadOffset + 8) !== 0;
const windowName = compositorRxBuffer.slice(rxReadOffset + 12, rxReadOffset + 12 + nameLen).toString("utf8");
rxConsume(12 + nameLen);
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("wo:pointer-lock-request", { window: windowName, lock });
}
continue;
}
if (magic === MAGIC.ENV_UPDATE) {
if (compositorRxBuffer.length < 8)
if (rxAvailable() < 8)
break;
const jsonLen = compositorRxBuffer.readUInt32LE(4);
if (compositorRxBuffer.length < 8 + jsonLen)
const jsonLen = compositorRxBuffer.readUInt32LE(rxReadOffset + 4);
if (rxAvailable() < 8 + jsonLen)
break;
const jsonStr = compositorRxBuffer.slice(8, 8 + jsonLen).toString("utf8");
compositorRxBuffer = compositorRxBuffer.slice(8 + jsonLen);
const jsonStr = compositorRxBuffer.slice(rxReadOffset + 8, rxReadOffset + 8 + jsonLen).toString("utf8");
rxConsume(8 + jsonLen);
try {
const vars = JSON.parse(jsonStr);
for (const [key, value] of Object.entries(vars)) {
@ -1321,18 +1361,18 @@ function connectToCompositor() {
continue;
}
if (magic === MAGIC.SCREENCOPY_EVENT) {
if (compositorRxBuffer.length < 12)
if (rxAvailable() < 12)
break;
const active = compositorRxBuffer.readUInt32LE(4) !== 0;
const clientCount = compositorRxBuffer.readUInt32LE(8);
compositorRxBuffer = compositorRxBuffer.slice(12);
const active = compositorRxBuffer.readUInt32LE(rxReadOffset + 4) !== 0;
const clientCount = compositorRxBuffer.readUInt32LE(rxReadOffset + 8);
rxConsume(12);
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("wo:screencopy-event", { active, clientCount });
}
continue;
}
debugLog("[Wo] UNKNOWN magic 0x" + magic.toString(16).padStart(8, "0") + " bufLen=" + compositorRxBuffer.length + " — stream may be corrupted");
compositorRxBuffer = compositorRxBuffer.slice(1);
debugLog("[Wo] UNKNOWN magic 0x" + magic.toString(16).padStart(8, "0") + " bufLen=" + rxAvailable() + " — stream may be corrupted");
rxConsume(1);
}
const dataElapsed = Date.now() - dataT0;
if (dataElapsed > 50) {
@ -1716,7 +1756,9 @@ async function createWindow() {
contextIsolation: true,
preload: preloadPath,
nodeIntegration: false,
offscreen: !CLIENT_MODE
offscreen: !CLIENT_MODE,
webgl: true,
backgroundThrottling: false
},
show: CLIENT_MODE
});
@ -1757,7 +1799,7 @@ async function createWindow() {
}
}, frameIntervalMs);
let skippedCount = 0;
mainWindow.webContents.on("paint", (_event, _dirty, image) => {
mainWindow.webContents.on("paint", (_event, dirty, image) => {
if (!dmabufSender || !ipcConnected) {
skippedCount++;
debugLog("[Wo] SKIPPED paint (dmabufSender=" + !!dmabufSender + " ipcConnected=" + ipcConnected + ")");
@ -1778,11 +1820,33 @@ async function createWindow() {
damageBuffer = new DamageBuffer(size.width, size.height);
}
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, {
width: size.width,
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 {
const seq = dmabufSender.send(damageBuffer);
inFlightFrameSeqs.add(seq.toString());

View file

@ -32,6 +32,18 @@ function debugLog(...args: any[]) {
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 = {
id: string;
title: string;
@ -1717,7 +1729,9 @@ async function createWindow() {
contextIsolation: true,
preload: preloadPath,
nodeIntegration: false,
offscreen: !CLIENT_MODE
offscreen: !CLIENT_MODE,
webgl: true,
backgroundThrottling: false,
},
show: CLIENT_MODE,
});