diff --git a/electron/dist/main.js b/electron/dist/main.js index 9e94253..b3bd6c5 100644 --- a/electron/dist/main.js +++ b/electron/dist/main.js @@ -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(); - applyDamagePayload(damageBuffer, { - width: size.width, - height: size.height, - fullFrame: new Uint8Array(pixels.buffer, pixels.byteOffset, pixels.byteLength) - }); + 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: 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()); diff --git a/electron/src/main.ts b/electron/src/main.ts index a792708..2414b23 100644 --- a/electron/src/main.ts +++ b/electron/src/main.ts @@ -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, });