Further optimization passes

This commit is contained in:
Allie H 2026-03-11 09:19:47 -04:00
parent 8b640d9f91
commit ff54140d62
19 changed files with 1099 additions and 781 deletions

211
electron/dist/main.js vendored
View file

@ -88,14 +88,14 @@ class DamageBuffer {
} }
applyFullFrame(frame, frameStride = this.width * 4) { applyFullFrame(frame, frameStride = this.width * 4) {
if (frameStride === this.stride) { if (frameStride === this.stride) {
Buffer.from(frame).copy(this.frame, 0, 0, this.frame.length); this.frame.set(new Uint8Array(frame.buffer, frame.byteOffset, this.frame.length));
return; return;
} }
const rowBytes = Math.min(frameStride, this.stride); const rowBytes = Math.min(frameStride, this.stride);
for (let y = 0;y < this.height; y += 1) { for (let y = 0;y < this.height; y += 1) {
const srcStart = y * frameStride; const srcStart = y * frameStride;
const dstStart = y * this.stride; const dstStart = y * this.stride;
Buffer.from(frame).copy(this.frame, dstStart, srcStart, srcStart + rowBytes); this.frame.set(new Uint8Array(frame.buffer, frame.byteOffset + srcStart, rowBytes), dstStart);
} }
} }
applyPatch(patch) { applyPatch(patch) {
@ -109,7 +109,7 @@ class DamageBuffer {
for (let row = 0;row < rect.height; row += 1) { for (let row = 0;row < rect.height; row += 1) {
const srcStart = row * patchStride; const srcStart = row * patchStride;
const dstStart = (rect.y + row) * this.stride + dstStartX; const dstStart = (rect.y + row) * this.stride + dstStartX;
Buffer.from(patch.rgba).copy(this.frame, dstStart, srcStart, srcStart + rowBytes); this.frame.set(new Uint8Array(patch.rgba.buffer, patch.rgba.byteOffset + srcStart, rowBytes), dstStart);
} }
} }
applyPatches(patches) { applyPatches(patches) {
@ -124,40 +124,49 @@ class WoDmabufSender {
windowName; windowName;
native; native;
seq = 1n; seq = 1n;
wireBuf;
nameLen;
seqOffset;
widthOffset;
heightOffset;
constructor(socket, windowName, native) { constructor(socket, windowName, native) {
this.socket = socket; this.socket = socket;
this.windowName = windowName; this.windowName = windowName;
this.native = native; this.native = native;
const nameBuf = Buffer.from(windowName, "utf8");
this.nameLen = nameBuf.length;
const totalLen = 32 + this.nameLen + 16;
this.wireBuf = Buffer.alloc(totalLen);
let off = 0;
this.wireBuf.writeUInt32LE(MAGIC.FRAME, off);
off += 4;
this.wireBuf.writeUInt32LE(this.nameLen, off);
off += 4;
nameBuf.copy(this.wireBuf, off);
off += this.nameLen;
this.seqOffset = off;
off += 8;
this.widthOffset = off;
off += 4;
this.heightOffset = off;
off += 4;
this.wireBuf.writeUInt32LE(ARGB8888, off);
off += 4;
this.wireBuf.writeUInt32LE(1, off);
} }
send(buffer) { send(buffer) {
const imported = this.native.importRgba(buffer.getFrameBuffer(), buffer.getWidth(), buffer.getHeight(), buffer.getStride()); const imported = this.native.importRgba(buffer.getFrameBuffer(), buffer.getWidth(), buffer.getHeight(), buffer.getStride());
try { try {
const socketFd = getSocketFd(this.socket); const socketFd = getSocketFd(this.socket);
const nameBuf = Buffer.from(this.windowName, "utf8"); this.wireBuf.writeBigUInt64LE(this.seq, this.seqOffset);
const header = Buffer.alloc(4 + 4 + nameBuf.length + 8 + 4 + 4 + 4 + 4); this.wireBuf.writeUInt32LE(buffer.getWidth(), this.widthOffset);
let offset = 0; this.wireBuf.writeUInt32LE(buffer.getHeight(), this.heightOffset);
header.writeUInt32LE(MAGIC.FRAME, offset); const planeOff = this.wireBuf.length - 16;
offset += 4; this.wireBuf.writeUInt32LE(imported.offset, planeOff);
header.writeUInt32LE(nameBuf.length, offset); this.wireBuf.writeUInt32LE(imported.stride, planeOff + 4);
offset += 4; this.wireBuf.writeUInt32LE(imported.modifier_hi, planeOff + 8);
nameBuf.copy(header, offset); this.wireBuf.writeUInt32LE(imported.modifier_lo, planeOff + 12);
offset += nameBuf.length; writeSync(socketFd, this.wireBuf, 0, this.wireBuf.length);
header.writeBigUInt64LE(this.seq, offset);
offset += 8;
header.writeUInt32LE(buffer.getWidth(), offset);
offset += 4;
header.writeUInt32LE(buffer.getHeight(), offset);
offset += 4;
header.writeUInt32LE(ARGB8888, offset);
offset += 4;
header.writeUInt32LE(1, offset);
const plane = Buffer.alloc(16);
plane.writeUInt32LE(imported.offset, 0);
plane.writeUInt32LE(imported.stride, 4);
plane.writeUInt32LE(imported.modifier_hi, 8);
plane.writeUInt32LE(imported.modifier_lo, 12);
const combined = Buffer.concat([header, plane]);
writeSync(socketFd, combined, 0, combined.length);
this.native.sendFd(socketFd, imported.fd); this.native.sendFd(socketFd, imported.fd);
const sentSeq = this.seq; const sentSeq = this.seq;
this.seq += 1n; this.seq += 1n;
@ -205,12 +214,24 @@ var __filename2 = fileURLToPath(import.meta.url);
var __dirname2 = dirname(__filename2); var __dirname2 = dirname(__filename2);
var require2 = createRequire2(import.meta.url); var require2 = createRequire2(import.meta.url);
var DEBUG_LOG = "/tmp/wo-electron-debug.log"; var DEBUG_LOG = "/tmp/wo-electron-debug.log";
var _logBuffer = [];
var _logTimer = null;
function _flushLog() {
if (_logBuffer.length === 0)
return;
try {
fs.appendFileSync(DEBUG_LOG, _logBuffer.join(""));
} catch (e) {}
_logBuffer = [];
_logTimer = null;
}
function debugLog(...args) { function debugLog(...args) {
const msg = `[${new Date().toISOString()}] ` + args.join(" ") + ` const msg = `[${new Date().toISOString()}] ` + args.join(" ") + `
`; `;
try { _logBuffer.push(msg);
fs.appendFileSync(DEBUG_LOG, msg); if (!_logTimer) {
} catch (e) {} _logTimer = setTimeout(_flushLog, 100);
}
console.log(...args); console.log(...args);
} }
debugLog("[Wo] Electron starting, NODE_VERSION=", process.version); debugLog("[Wo] Electron starting, NODE_VERSION=", process.version);
@ -219,9 +240,10 @@ app.commandLine.appendSwitch("enable-gpu-rasterization");
app.commandLine.appendSwitch("enable-zero-copy"); app.commandLine.appendSwitch("enable-zero-copy");
app.commandLine.appendSwitch("enable-native-gpu-memory-buffers"); app.commandLine.appendSwitch("enable-native-gpu-memory-buffers");
app.commandLine.appendSwitch("disable-software-rasterizer"); app.commandLine.appendSwitch("disable-software-rasterizer");
app.commandLine.appendSwitch("enable-features", "CanvasOopRasterization,Vulkan,VaapiVideoDecoder,VaapiVideoEncoder"); app.commandLine.appendSwitch("enable-features", "CanvasOopRasterization,Vulkan,VaapiVideoDecoder,VaapiVideoEncoder,SharedArrayBuffer,RawDraw");
app.commandLine.appendSwitch("use-gl", "egl");
app.commandLine.appendSwitch("enable-accelerated-video-decode"); app.commandLine.appendSwitch("enable-accelerated-video-decode");
app.commandLine.appendSwitch("disable-frame-rate-limit");
app.commandLine.appendSwitch("disable-gpu-vsync");
debugLog("[Wo] GPU acceleration flags applied"); 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);
@ -249,9 +271,10 @@ var nativeDmabuf = null;
var compositorRxBuffer = Buffer.alloc(64 * 1024); var compositorRxBuffer = Buffer.alloc(64 * 1024);
var rxWriteOffset = 0; var rxWriteOffset = 0;
var rxReadOffset = 0; var rxReadOffset = 0;
var surfaceBufferPending = new Map; var surfaceSabCache = new Map;
var surfaceBufferFlushScheduled = false; var surfaceUpdatePending = new Map;
var shmReadBufferCache = new Map; var surfaceUpdateGeneration = 0;
var surfaceUpdateFlushScheduled = false;
var shmFdCache = new Map; var shmFdCache = new Map;
function rxAppend(chunk) { function rxAppend(chunk) {
const needed = rxWriteOffset + chunk.length; const needed = rxWriteOffset + chunk.length;
@ -304,28 +327,39 @@ function getOrOpenShmFd(windowName, pid, fd) {
shmFdCache.set(windowName, { pid, fd, extFd }); shmFdCache.set(windowName, { pid, fd, extFd });
return extFd; return extFd;
} }
function getReusableShmBuffer(windowName, size) { function getOrCreateSab(windowName, width2, height2, stride) {
const existing = shmReadBufferCache.get(windowName); const existing = surfaceSabCache.get(windowName);
if (existing && existing.length >= size) { const neededSize = stride * height2;
if (existing && existing.sab.byteLength >= neededSize && existing.width === width2 && existing.height === height2 && existing.stride === stride) {
return existing; return existing;
} }
const next = Buffer.allocUnsafe(size); const sab = new SharedArrayBuffer(neededSize);
shmReadBufferCache.set(windowName, next); const entry = { sab, width: width2, height: height2, stride };
return next; surfaceSabCache.set(windowName, entry);
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.postMessage("wo:surface-sab", {
name: windowName,
sab,
width: width2,
height: height2,
stride
});
}
return entry;
} }
function flushSurfaceBuffers() { function flushSurfaceUpdates() {
surfaceBufferFlushScheduled = false; surfaceUpdateFlushScheduled = false;
if (!mainWindow || mainWindow.isDestroyed()) { if (!mainWindow || mainWindow.isDestroyed()) {
surfaceBufferPending.clear(); surfaceUpdatePending.clear();
return; return;
} }
for (const entry of surfaceBufferPending.values()) { for (const entry of surfaceUpdatePending.values()) {
mainWindow.webContents.send("wo:surface-buffer", entry); mainWindow.webContents.send("wo:surface-update", entry);
} }
surfaceBufferPending.clear(); surfaceUpdatePending.clear();
} }
var inFlightFrameSeqs = new Set; var inFlightFrameSeqs = new Set;
var MAX_IN_FLIGHT_FRAMES = 2; var MAX_IN_FLIGHT_FRAMES = 3;
var appUsedDamageHelper = false; var appUsedDamageHelper = false;
var pointerX = 0; var pointerX = 0;
var pointerY = 0; var pointerY = 0;
@ -1173,6 +1207,23 @@ function connectToCompositor() {
try { try {
const parsed = JSON.parse(metadata); const parsed = JSON.parse(metadata);
if (parsed && Array.isArray(parsed.windows)) { if (parsed && Array.isArray(parsed.windows)) {
const currentNames = new Set(parsed.windows.map((w) => w.name).filter(Boolean));
for (const cachedName of surfaceSabCache.keys()) {
if (!currentNames.has(cachedName)) {
surfaceSabCache.delete(cachedName);
}
}
for (const cachedName of shmFdCache.keys()) {
if (!currentNames.has(cachedName)) {
const entry = shmFdCache.get(cachedName);
if (entry) {
try {
fs.closeSync(entry.extFd);
} catch {}
}
shmFdCache.delete(cachedName);
}
}
compositorWindows = parsed.windows; compositorWindows = parsed.windows;
mainWindow.webContents.send("wo:windows", compositorWindows); mainWindow.webContents.send("wo:windows", compositorWindows);
} }
@ -1219,9 +1270,9 @@ function connectToCompositor() {
try { try {
const extFd = getOrOpenShmFd(windowName, pid, fd); const extFd = getOrOpenShmFd(windowName, pid, fd);
const fullSize = sbStride * sbHeight; const fullSize = sbStride * sbHeight;
const safeBuffer = getReusableShmBuffer(windowName, fullSize); const sabEntry = getOrCreateSab(windowName, sbWidth, sbHeight, sbStride);
const hasUsableRects = damageRects.length > 0 && damageRects.length < 64; const hasUsableRects = damageRects.length > 0 && damageRects.length < 64;
if (hasUsableRects) { if (hasUsableRects && nativeDmabuf?.copyMmapDamageToSab) {
let minY = sbHeight, maxY = 0; let minY = sbHeight, maxY = 0;
for (const r of damageRects) { for (const r of damageRects) {
const ry = Math.max(0, r.y); const ry = Math.max(0, r.y);
@ -1232,36 +1283,34 @@ function connectToCompositor() {
maxY = ryEnd; maxY = ryEnd;
} }
if (minY < maxY) { if (minY < maxY) {
const readOffset = minY * sbStride; nativeDmabuf.copyMmapDamageToSab(extFd, sabEntry.sab, sbStride, [
const readLen = (maxY - minY) * sbStride; { y: minY, h: maxY - minY }
let totalRead = 0; ]);
while (totalRead < readLen) {
const bytesRead = fs.readSync(extFd, safeBuffer, readOffset + totalRead, readLen - totalRead, readOffset + totalRead);
if (bytesRead <= 0)
break;
totalRead += bytesRead;
}
} }
} else if (nativeDmabuf?.copyMmapToSab) {
nativeDmabuf.copyMmapToSab(extFd, sabEntry.sab, fullSize);
} else { } else {
const tmpBuf = Buffer.alloc(fullSize);
let totalRead = 0; let totalRead = 0;
while (totalRead < fullSize) { while (totalRead < fullSize) {
const bytesRead = fs.readSync(extFd, safeBuffer, totalRead, fullSize - totalRead, totalRead); const bytesRead = fs.readSync(extFd, tmpBuf, totalRead, fullSize - totalRead, totalRead);
if (bytesRead <= 0) if (bytesRead <= 0)
break; break;
totalRead += bytesRead; totalRead += bytesRead;
} }
new Uint8Array(sabEntry.sab).set(new Uint8Array(tmpBuf.buffer, tmpBuf.byteOffset, tmpBuf.byteLength));
} }
surfaceBufferPending.set(windowName, { surfaceUpdatePending.set(windowName, {
name: windowName, name: windowName,
width: sbWidth, width: sbWidth,
height: sbHeight, height: sbHeight,
stride: sbStride, stride: sbStride,
pixels: safeBuffer, generation: ++surfaceUpdateGeneration,
damageRects: hasUsableRects ? damageRects : undefined damageRects: hasUsableRects ? damageRects : undefined
}); });
if (!surfaceBufferFlushScheduled) { if (!surfaceUpdateFlushScheduled) {
surfaceBufferFlushScheduled = true; surfaceUpdateFlushScheduled = true;
setImmediate(flushSurfaceBuffers); setImmediate(flushSurfaceUpdates);
} }
} catch (err) { } catch (err) {
const cached = shmFdCache.get(windowName); const cached = shmFdCache.get(windowName);
@ -1396,7 +1445,12 @@ function connectToCompositor() {
ipcSocket = null; ipcSocket = null;
dmabufSender = null; dmabufSender = null;
inFlightFrameSeqs.clear(); inFlightFrameSeqs.clear();
compositorRxBuffer = Buffer.alloc(0); compositorRxBuffer = Buffer.alloc(64 * 1024);
rxWriteOffset = 0;
rxReadOffset = 0;
surfaceSabCache.clear();
surfaceUpdatePending.clear();
closeShmFdCache();
scheduleIpcReconnect(); scheduleIpcReconnect();
}); });
setTimeout(() => { setTimeout(() => {
@ -1762,6 +1816,15 @@ async function createWindow() {
}, },
show: CLIENT_MODE show: CLIENT_MODE
}); });
mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
"Cross-Origin-Opener-Policy": ["same-origin"],
"Cross-Origin-Embedder-Policy": ["require-corp"]
}
});
});
debugLog("[Wo] BrowserWindow created, CLIENT_MODE=" + CLIENT_MODE + " offscreen=" + !CLIENT_MODE); debugLog("[Wo] BrowserWindow created, CLIENT_MODE=" + CLIENT_MODE + " offscreen=" + !CLIENT_MODE);
setupIpcHandlers(); setupIpcHandlers();
debugLog("[Wo] setupIpcHandlers completed"); debugLog("[Wo] setupIpcHandlers completed");
@ -1794,10 +1857,11 @@ async function createWindow() {
}); });
const frameIntervalMs = Math.round(1000 / (WINDOW_CONFIG.fps ?? 60)); const frameIntervalMs = Math.round(1000 / (WINDOW_CONFIG.fps ?? 60));
setInterval(() => { setInterval(() => {
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed() && inFlightFrameSeqs.size < MAX_IN_FLIGHT_FRAMES) {
mainWindow.webContents.invalidate(); mainWindow.webContents.invalidate();
} }
}, frameIntervalMs); }, frameIntervalMs);
let patchBuf = null;
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) {
@ -1819,7 +1883,7 @@ async function createWindow() {
if (!damageBuffer) { if (!damageBuffer) {
damageBuffer = new DamageBuffer(size.width, size.height); damageBuffer = new DamageBuffer(size.width, size.height);
} }
const pixels = image.getBitmap(); const pixels = image.toBitmap();
const pixelData = 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; const isFullFrame = dirty.x === 0 && dirty.y === 0 && dirty.width === size.width && dirty.height === size.height;
if (isFullFrame) { if (isFullFrame) {
@ -1831,7 +1895,11 @@ async function createWindow() {
} else { } else {
const stride = size.width * 4; const stride = size.width * 4;
const patchStride = dirty.width * 4; const patchStride = dirty.width * 4;
const patchData = new Uint8Array(dirty.width * dirty.height * 4); const patchBytes = patchStride * dirty.height;
if (!patchBuf || patchBuf.byteLength < patchBytes) {
patchBuf = new Uint8Array(patchBytes);
}
const patchData = patchBuf.byteLength === patchBytes ? patchBuf : patchBuf.subarray(0, patchBytes);
for (let row = 0;row < dirty.height; row++) { for (let row = 0;row < dirty.height; row++) {
const srcOff = (dirty.y + row) * stride + dirty.x * 4; const srcOff = (dirty.y + row) * stride + dirty.x * 4;
const dstOff = row * patchStride; const dstOff = row * patchStride;
@ -1885,6 +1953,9 @@ async function createWindow() {
} }
app.on("ready", async () => { app.on("ready", async () => {
debugLog("[Wo] App ready, CLIENT_MODE=", CLIENT_MODE, "WO_STANDALONE=", process.env.WO_STANDALONE); debugLog("[Wo] App ready, CLIENT_MODE=", CLIENT_MODE, "WO_STANDALONE=", process.env.WO_STANDALONE);
const gpuInfo = app.getGPUFeatureStatus();
debugLog("[Wo] GPU feature status:", JSON.stringify(gpuInfo));
debugLog("[Wo] GPU info:", JSON.stringify(app.getGPUInfo("basic").catch(() => ({}))));
try { try {
startPortalUiBridge(); startPortalUiBridge();
if (!CLIENT_MODE && process.env.WO_STANDALONE !== "1") { if (!CLIENT_MODE && process.env.WO_STANDALONE !== "1") {

View file

@ -144,6 +144,51 @@ static std::map<uint32_t, BufEntry> g_bufs;
static std::mutex g_bufs_mutex; static std::mutex g_bufs_mutex;
static uint32_t g_next_token = 1; static uint32_t g_next_token = 1;
// ── GBM BO pool ──────────────────────────────────────────────────────
// Reuse GBM BOs of the same (width, height) to avoid alloc/dealloc
// overhead on every frame. The pool keeps up to two idle BOs per
// dimension so the sender can fill the next frame while the previous
// one is still in flight (double-buffering).
struct PoolKey {
uint32_t w, h;
bool operator<(const PoolKey &o) const {
return w < o.w || (w == o.w && h < o.h);
}
};
struct PoolEntry {
gbm_bo *bo;
uint32_t dst_stride;
};
static constexpr size_t MAX_POOL_PER_DIM = 2;
static std::map<PoolKey, std::vector<PoolEntry>> g_bo_pool;
static std::mutex g_bo_pool_mutex;
static gbm_bo *pool_acquire(uint32_t w, uint32_t h, uint32_t *out_stride) {
std::lock_guard<std::mutex> lk(g_bo_pool_mutex);
auto it = g_bo_pool.find({w, h});
if (it != g_bo_pool.end() && !it->second.empty()) {
PoolEntry pe = it->second.back();
it->second.pop_back();
*out_stride = pe.dst_stride;
return pe.bo;
}
return nullptr;
}
static void pool_return(gbm_bo *bo, uint32_t w, uint32_t h, uint32_t stride) {
std::lock_guard<std::mutex> lk(g_bo_pool_mutex);
PoolKey key{w, h};
auto &vec = g_bo_pool[key];
if (vec.size() >= MAX_POOL_PER_DIM) {
// Pool full — destroy the returned BO
gbm_bo_destroy(bo);
} else {
vec.push_back({bo, stride});
}
}
static uint32_t store_buf(gbm_bo *bo, int fd) { static uint32_t store_buf(gbm_bo *bo, int fd) {
std::lock_guard<std::mutex> lk(g_bufs_mutex); std::lock_guard<std::mutex> lk(g_bufs_mutex);
uint32_t tok = g_next_token++; uint32_t tok = g_next_token++;
@ -157,8 +202,13 @@ static void release_buf(uint32_t tok) {
if (it == g_bufs.end()) if (it == g_bufs.end())
return; return;
::close(it->second.dmabuf_fd); ::close(it->second.dmabuf_fd);
gbm_bo_destroy(it->second.bo); gbm_bo *bo = it->second.bo;
uint32_t w = gbm_bo_get_width(bo);
uint32_t h = gbm_bo_get_height(bo);
uint32_t stride = gbm_bo_get_stride(bo);
g_bufs.erase(it); g_bufs.erase(it);
// Return to pool for reuse instead of destroying
pool_return(bo, w, h, stride);
} }
Napi::Value Init(const Napi::CallbackInfo &info) { Napi::Value Init(const Napi::CallbackInfo &info) {
@ -195,44 +245,51 @@ Napi::Value ImportRgba(const Napi::CallbackInfo &info) {
return env.Null(); return env.Null();
} }
gbm_bo *bo = nullptr; // Try to reuse a pooled BO of matching dimensions
uint32_t dst_stride = 0;
bo = gbm_bo_create_with_modifiers2(g_gbm, w, h, GBM_FORMAT_ARGB8888, gbm_bo *bo = pool_acquire(w, h, &dst_stride);
(const uint64_t[]){DRM_FORMAT_MOD_LINEAR},
1,
GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR);
if (!bo) { if (!bo) {
bo = gbm_bo_create(g_gbm, w, h, GBM_FORMAT_ARGB8888, bo = gbm_bo_create_with_modifiers2(g_gbm, w, h, GBM_FORMAT_ARGB8888,
GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR); (const uint64_t[]){DRM_FORMAT_MOD_LINEAR},
} 1,
GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR);
if (!bo) { if (!bo) {
bo = gbm_bo_create(g_gbm, w, h, GBM_FORMAT_ARGB8888, GBM_BO_USE_WRITE); bo = gbm_bo_create(g_gbm, w, h, GBM_FORMAT_ARGB8888,
} GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR);
}
if (!bo) { if (!bo) {
Napi::Error::New( bo = gbm_bo_create(g_gbm, w, h, GBM_FORMAT_ARGB8888, GBM_BO_USE_WRITE);
env, "gbm_bo_create failed: all buffer creation methods exhausted") }
.ThrowAsJavaScriptException(); if (!bo) {
return env.Null(); Napi::Error::New(
env, "gbm_bo_create failed: all buffer creation methods exhausted")
.ThrowAsJavaScriptException();
return env.Null();
}
} }
void *map_data = nullptr; void *map_data = nullptr;
uint32_t dst_stride = 0; uint32_t map_stride = 0;
void *ptr = void *ptr =
gbm_bo_map(bo, 0, 0, w, h, GBM_BO_TRANSFER_WRITE, &dst_stride, &map_data); gbm_bo_map(bo, 0, 0, w, h, GBM_BO_TRANSFER_WRITE, &map_stride, &map_data);
if (!ptr) { if (!ptr) {
gbm_bo_destroy(bo); gbm_bo_destroy(bo);
Napi::Error::New(env, "gbm_bo_map failed").ThrowAsJavaScriptException(); Napi::Error::New(env, "gbm_bo_map failed").ThrowAsJavaScriptException();
return env.Null(); return env.Null();
} }
dst_stride = map_stride;
const uint8_t *src = pixel_buf.Data(); const uint8_t *src = pixel_buf.Data();
uint8_t *dst = static_cast<uint8_t *>(ptr); uint8_t *dst = static_cast<uint8_t *>(ptr);
uint32_t row_bytes = std::min(src_stride, dst_stride); if (src_stride == dst_stride) {
for (uint32_t y = 0; y < h; ++y) // Fast path: strides match — single memcpy for entire image
std::memcpy(dst + y * dst_stride, src + y * src_stride, row_bytes); std::memcpy(dst, src, static_cast<size_t>(dst_stride) * h);
} else {
uint32_t row_bytes = std::min(src_stride, dst_stride);
for (uint32_t y = 0; y < h; ++y)
std::memcpy(dst + y * dst_stride, src + y * src_stride, row_bytes);
}
gbm_bo_unmap(bo, map_data); gbm_bo_unmap(bo, map_data);
@ -508,6 +565,118 @@ Napi::Value MmapFd(const Napi::CallbackInfo &info) {
hint); hint);
} }
// ── Persistent mmap cache ─────────────────────────────────────────────
// CopyMmapToSab and CopyMmapDamageToSab are called on every frame for
// the same fd+size. Avoid the mmap/munmap syscall pair per frame by
// caching the mapping. Evict when fd or size changes.
struct MmapCacheEntry {
int fd;
void *ptr;
size_t size;
};
static MmapCacheEntry g_mmap_cache{-1, MAP_FAILED, 0};
static std::mutex g_mmap_cache_mutex;
static void *mmap_cached(int fd, size_t size) {
std::lock_guard<std::mutex> lk(g_mmap_cache_mutex);
if (g_mmap_cache.fd == fd && g_mmap_cache.size == size &&
g_mmap_cache.ptr != MAP_FAILED) {
return g_mmap_cache.ptr;
}
// Evict old mapping
if (g_mmap_cache.ptr != MAP_FAILED) {
::munmap(g_mmap_cache.ptr, g_mmap_cache.size);
}
void *ptr = ::mmap(nullptr, size, PROT_READ, MAP_SHARED, fd, 0);
g_mmap_cache = {fd, ptr, size};
return ptr;
}
// mmap a file descriptor into a region and copy into the backing store of a
// pre-existing SharedArrayBuffer. This is the fast path for SHM surface
// buffers: the compositor writes to a memfd, we mmap+memcpy into a SAB
// that is already shared with the Electron renderer — no structured-clone
// serialization across the Electron IPC boundary.
//
// copyMmapToSab(fd, sab, size, srcOffset?, dstOffset?)
// If damage rects are provided, only the specified rows are copied.
Napi::Value CopyMmapToSab(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
if (info.Length() < 3) {
Napi::TypeError::New(env, "copyMmapToSab(fd, sab, size [, srcOff, dstOff])")
.ThrowAsJavaScriptException();
return env.Undefined();
}
int fd = info[0].As<Napi::Number>().Int32Value();
auto arrayBuf = info[1].As<Napi::ArrayBuffer>();
size_t size = static_cast<size_t>(info[2].As<Napi::Number>().Int64Value());
size_t src_off = (info.Length() > 3 && info[3].IsNumber())
? static_cast<size_t>(info[3].As<Napi::Number>().Int64Value())
: 0;
size_t dst_off = (info.Length() > 4 && info[4].IsNumber())
? static_cast<size_t>(info[4].As<Napi::Number>().Int64Value())
: 0;
if (dst_off + size > arrayBuf.ByteLength()) {
Napi::RangeError::New(env, "copyMmapToSab: destination overflow")
.ThrowAsJavaScriptException();
return env.Undefined();
}
void *ptr = mmap_cached(fd, src_off + size);
if (ptr == MAP_FAILED) {
Napi::Error::New(env, std::string("mmap failed: ") + ::strerror(errno))
.ThrowAsJavaScriptException();
return env.Undefined();
}
std::memcpy(static_cast<uint8_t *>(arrayBuf.Data()) + dst_off,
static_cast<const uint8_t *>(ptr) + src_off, size);
return env.Undefined();
}
// Partial-damage variant: copy only specified row ranges from mmap'd fd
// into a SharedArrayBuffer.
// copyMmapDamageToSab(fd, sab, stride, rects: [{y, h}])
Napi::Value CopyMmapDamageToSab(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
if (info.Length() < 4) {
Napi::TypeError::New(env, "copyMmapDamageToSab(fd, sab, stride, rects)")
.ThrowAsJavaScriptException();
return env.Undefined();
}
int fd = info[0].As<Napi::Number>().Int32Value();
auto arrayBuf = info[1].As<Napi::ArrayBuffer>();
uint32_t stride = info[2].As<Napi::Number>().Uint32Value();
auto rects = info[3].As<Napi::Array>();
size_t totalSize = arrayBuf.ByteLength();
void *ptr = mmap_cached(fd, totalSize);
if (ptr == MAP_FAILED) {
Napi::Error::New(env, std::string("mmap failed: ") + ::strerror(errno))
.ThrowAsJavaScriptException();
return env.Undefined();
}
uint8_t *src = static_cast<uint8_t *>(ptr);
uint8_t *dst = static_cast<uint8_t *>(arrayBuf.Data());
for (uint32_t i = 0; i < rects.Length(); i++) {
auto rect = rects.Get(i).As<Napi::Object>();
uint32_t ry = rect.Get("y").As<Napi::Number>().Uint32Value();
uint32_t rh = rect.Get("h").As<Napi::Number>().Uint32Value();
size_t off = static_cast<size_t>(ry) * stride;
size_t len = static_cast<size_t>(rh) * stride;
if (off + len > totalSize) len = totalSize - off;
std::memcpy(dst + off, src + off, len);
}
return env.Undefined();
}
Napi::Object ModuleInit(Napi::Env env, Napi::Object exports) { Napi::Object ModuleInit(Napi::Env env, Napi::Object exports) {
exports.Set("init", Napi::Function::New(env, Init)); exports.Set("init", Napi::Function::New(env, Init));
exports.Set("importRgba", Napi::Function::New(env, ImportRgba)); exports.Set("importRgba", Napi::Function::New(env, ImportRgba));
@ -515,6 +684,8 @@ Napi::Object ModuleInit(Napi::Env env, Napi::Object exports) {
exports.Set("sendFd", Napi::Function::New(env, SendFd)); exports.Set("sendFd", Napi::Function::New(env, SendFd));
exports.Set("recvFd", Napi::Function::New(env, RecvFd)); exports.Set("recvFd", Napi::Function::New(env, RecvFd));
exports.Set("mmapFd", Napi::Function::New(env, MmapFd)); exports.Set("mmapFd", Napi::Function::New(env, MmapFd));
exports.Set("copyMmapToSab", Napi::Function::New(env, CopyMmapToSab));
exports.Set("copyMmapDamageToSab", Napi::Function::New(env, CopyMmapDamageToSab));
exports.Set("importDmabufTexture", Napi::Function::New(env, ImportDmabufTexture)); exports.Set("importDmabufTexture", Napi::Function::New(env, ImportDmabufTexture));
exports.Set("getTexture", Napi::Function::New(env, GetTexture)); exports.Set("getTexture", Napi::Function::New(env, GetTexture));
exports.Set("releaseTexture", Napi::Function::New(env, ReleaseTexture)); exports.Set("releaseTexture", Napi::Function::New(env, ReleaseTexture));

View file

@ -60,6 +60,8 @@ type NativeDmabufModule = {
sendFd: (socketFd: number, dmabufFd: number) => void; sendFd: (socketFd: number, dmabufFd: number) => void;
recvFd: (socketFd: number) => number; recvFd: (socketFd: number) => number;
mmapFd: (fd: number, size: number) => Buffer; mmapFd: (fd: number, size: number) => Buffer;
copyMmapToSab: (fd: number, sab: SharedArrayBuffer, size: number, srcOff?: number, dstOff?: number) => void;
copyMmapDamageToSab: (fd: number, sab: SharedArrayBuffer, stride: number, rects: Array<{y: number; h: number}>) => void;
importDmabufTexture: (windowName: string, fd: number, width: number, height: number, format: number) => { texture: number; width: number; height: number }; importDmabufTexture: (windowName: string, fd: number, width: number, height: number, format: number) => { texture: number; width: number; height: number };
getTexture: (windowName: string) => { texture: number; width: number; height: number } | null; getTexture: (windowName: string) => { texture: number; width: number; height: number } | null;
releaseTexture: (windowName: string) => void; releaseTexture: (windowName: string) => void;
@ -105,7 +107,8 @@ export class DamageBuffer {
applyFullFrame(frame: Uint8Array, frameStride: number = this.width * 4): void { applyFullFrame(frame: Uint8Array, frameStride: number = this.width * 4): void {
if (frameStride === this.stride) { if (frameStride === this.stride) {
Buffer.from(frame).copy(this.frame, 0, 0, this.frame.length); // Fast path: direct typed-array copy, no intermediate Buffer wrapper
this.frame.set(new Uint8Array(frame.buffer, frame.byteOffset, this.frame.length));
return; return;
} }
@ -113,7 +116,10 @@ export class DamageBuffer {
for (let y = 0; y < this.height; y += 1) { for (let y = 0; y < this.height; y += 1) {
const srcStart = y * frameStride; const srcStart = y * frameStride;
const dstStart = y * this.stride; const dstStart = y * this.stride;
Buffer.from(frame).copy(this.frame, dstStart, srcStart, srcStart + rowBytes); this.frame.set(
new Uint8Array(frame.buffer, frame.byteOffset + srcStart, rowBytes),
dstStart,
);
} }
} }
@ -130,7 +136,10 @@ export class DamageBuffer {
for (let row = 0; row < rect.height; row += 1) { for (let row = 0; row < rect.height; row += 1) {
const srcStart = row * patchStride; const srcStart = row * patchStride;
const dstStart = (rect.y + row) * this.stride + dstStartX; const dstStart = (rect.y + row) * this.stride + dstStartX;
Buffer.from(patch.rgba).copy(this.frame, dstStart, srcStart, srcStart + rowBytes); this.frame.set(
new Uint8Array(patch.rgba.buffer, patch.rgba.byteOffset + srcStart, rowBytes),
dstStart,
);
} }
} }
@ -146,11 +155,35 @@ export class WoDmabufSender {
private readonly windowName: string; private readonly windowName: string;
private readonly native: NativeDmabufModule; private readonly native: NativeDmabufModule;
private seq = 1n; private seq = 1n;
// Pre-allocated wire buffer: header (variable) + plane (16 bytes).
// Layout: MAGIC(4) + nameLen(4) + name(N) + seq(8) + w(4) + h(4) + fmt(4) + planes(4) + plane(16)
private readonly wireBuf: Buffer;
private readonly nameLen: number;
private readonly seqOffset: number;
private readonly widthOffset: number;
private readonly heightOffset: number;
constructor(socket: Socket, windowName: string, native: NativeDmabufModule) { constructor(socket: Socket, windowName: string, native: NativeDmabufModule) {
this.socket = socket; this.socket = socket;
this.windowName = windowName; this.windowName = windowName;
this.native = native; this.native = native;
const nameBuf = Buffer.from(windowName, 'utf8');
this.nameLen = nameBuf.length;
// header = 4+4+nameLen+8+4+4+4+4 = 32+nameLen, plus 16 bytes for plane
const totalLen = 32 + this.nameLen + 16;
this.wireBuf = Buffer.alloc(totalLen);
// Static fields that never change
let off = 0;
this.wireBuf.writeUInt32LE(MAGIC.FRAME, off); off += 4;
this.wireBuf.writeUInt32LE(this.nameLen, off); off += 4;
nameBuf.copy(this.wireBuf, off); off += this.nameLen;
this.seqOffset = off; off += 8;
this.widthOffset = off; off += 4;
this.heightOffset = off; off += 4;
this.wireBuf.writeUInt32LE(ARGB8888, off); off += 4;
this.wireBuf.writeUInt32LE(1, off); // planes count = 1
} }
send(buffer: DamageBuffer): bigint { send(buffer: DamageBuffer): bigint {
@ -164,32 +197,19 @@ export class WoDmabufSender {
try { try {
const socketFd = getSocketFd(this.socket); const socketFd = getSocketFd(this.socket);
const nameBuf = Buffer.from(this.windowName, 'utf8'); // Write per-frame fields into the pre-allocated buffer
const header = Buffer.alloc(4 + 4 + nameBuf.length + 8 + 4 + 4 + 4 + 4); this.wireBuf.writeBigUInt64LE(this.seq, this.seqOffset);
let offset = 0; this.wireBuf.writeUInt32LE(buffer.getWidth(), this.widthOffset);
header.writeUInt32LE(MAGIC.FRAME, offset); offset += 4; this.wireBuf.writeUInt32LE(buffer.getHeight(), this.heightOffset);
header.writeUInt32LE(nameBuf.length, offset); offset += 4;
nameBuf.copy(header, offset); offset += nameBuf.length;
header.writeBigUInt64LE(this.seq, offset); offset += 8;
header.writeUInt32LE(buffer.getWidth(), offset); offset += 4;
header.writeUInt32LE(buffer.getHeight(), offset); offset += 4;
header.writeUInt32LE(ARGB8888, offset); offset += 4;
header.writeUInt32LE(1, offset);
const plane = Buffer.alloc(16); // Plane data (last 16 bytes of wireBuf)
plane.writeUInt32LE(imported.offset, 0); const planeOff = this.wireBuf.length - 16;
plane.writeUInt32LE(imported.stride, 4); this.wireBuf.writeUInt32LE(imported.offset, planeOff);
plane.writeUInt32LE(imported.modifier_hi, 8); this.wireBuf.writeUInt32LE(imported.stride, planeOff + 4);
plane.writeUInt32LE(imported.modifier_lo, 12); this.wireBuf.writeUInt32LE(imported.modifier_hi, planeOff + 8);
this.wireBuf.writeUInt32LE(imported.modifier_lo, planeOff + 12);
// CRITICAL: We must use synchronous, unbuffered writes to the raw writeSync(socketFd, this.wireBuf, 0, this.wireBuf.length);
// socket fd so that the header+plane bytes are in the kernel buffer
// BEFORE sendFd's sendmsg() call. Node's socket.write() is async
// and buffers in userspace — sendFd (a native sendmsg) would race
// ahead, delivering the SCM_RIGHTS ancillary data before the header
// bytes, corrupting the protocol framing on the compositor side.
const combined = Buffer.concat([header, plane]);
writeSync(socketFd, combined, 0, combined.length);
this.native.sendFd(socketFd, imported.fd); this.native.sendFd(socketFd, imported.fd);
const sentSeq = this.seq; const sentSeq = this.seq;

View file

@ -18,30 +18,44 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
// Debug logging to file // Debug logging to file — buffered to avoid synchronous I/O in hot paths
const DEBUG_LOG = '/tmp/wo-electron-debug.log'; const DEBUG_LOG = '/tmp/wo-electron-debug.log';
function debugLog(...args: any[]) { let _logBuffer: string[] = [];
const msg = `[${new Date().toISOString()}] ` + args.join(' ') + '\n'; let _logTimer: ReturnType<typeof setTimeout> | null = null;
function _flushLog() {
if (_logBuffer.length === 0) return;
try { try {
fs.appendFileSync(DEBUG_LOG, msg); fs.appendFileSync(DEBUG_LOG, _logBuffer.join(''));
} catch (e) { } catch (e) {
// ignore // ignore
} }
_logBuffer = [];
_logTimer = null;
}
function debugLog(...args: any[]) {
const msg = `[${new Date().toISOString()}] ` + args.join(' ') + '\n';
_logBuffer.push(msg);
if (!_logTimer) {
_logTimer = setTimeout(_flushLog, 100);
}
console.log(...args); console.log(...args);
} }
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') // GPU acceleration & rendering performance flags (must be set before app 'ready')
// Note: core flags (--in-process-gpu, --use-gl=egl, --ozone-platform=wayland)
// are set on the command line in the Rust spawn(). Flags below augment those.
app.commandLine.appendSwitch('ignore-gpu-blocklist'); app.commandLine.appendSwitch('ignore-gpu-blocklist');
app.commandLine.appendSwitch('enable-gpu-rasterization'); app.commandLine.appendSwitch('enable-gpu-rasterization');
app.commandLine.appendSwitch('enable-zero-copy'); app.commandLine.appendSwitch('enable-zero-copy');
app.commandLine.appendSwitch('enable-native-gpu-memory-buffers'); app.commandLine.appendSwitch('enable-native-gpu-memory-buffers');
app.commandLine.appendSwitch('disable-software-rasterizer'); app.commandLine.appendSwitch('disable-software-rasterizer');
app.commandLine.appendSwitch('enable-features', app.commandLine.appendSwitch('enable-features',
'CanvasOopRasterization,Vulkan,VaapiVideoDecoder,VaapiVideoEncoder'); 'CanvasOopRasterization,Vulkan,VaapiVideoDecoder,VaapiVideoEncoder,SharedArrayBuffer,RawDraw');
app.commandLine.appendSwitch('use-gl', 'egl');
app.commandLine.appendSwitch('enable-accelerated-video-decode'); app.commandLine.appendSwitch('enable-accelerated-video-decode');
app.commandLine.appendSwitch('disable-frame-rate-limit');
app.commandLine.appendSwitch('disable-gpu-vsync');
debugLog('[Wo] GPU acceleration flags applied'); debugLog('[Wo] GPU acceleration flags applied');
type ClientWindowInfo = { type ClientWindowInfo = {
@ -85,9 +99,28 @@ let nativeDmabuf: any = null;
let compositorRxBuffer = Buffer.alloc(64 * 1024); let compositorRxBuffer = Buffer.alloc(64 * 1024);
let rxWriteOffset = 0; // next write position let rxWriteOffset = 0; // next write position
let rxReadOffset = 0; // next read position let rxReadOffset = 0; // next read position
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}> }>(); // ── Zero-copy surface buffer transfer via SharedArrayBuffer ──
let surfaceBufferFlushScheduled = false; // One SAB per window. The main process mmap's compositor memfd data into
const shmReadBufferCache = new Map<string, Buffer>(); // the SAB; the renderer reads directly from the same memory — no
// structured-clone serialization across the Electron IPC boundary.
type SabEntry = {
sab: SharedArrayBuffer;
width: number;
height: number;
stride: number;
};
const surfaceSabCache = new Map<string, SabEntry>();
// Lightweight generation counter + damage rects sent via IPC instead of pixels
const surfaceUpdatePending = new Map<string, {
name: string;
width: number;
height: number;
stride: number;
generation: number;
damageRects?: Array<{x: number; y: number; width: number; height: number}>;
}>();
let surfaceUpdateGeneration = 0;
let surfaceUpdateFlushScheduled = false;
const shmFdCache = new Map<string, { pid: number; fd: number; extFd: number }>(); const shmFdCache = new Map<string, { pid: number; fd: number; extFd: number }>();
/** Append incoming chunk to the rx ring buffer, growing if needed. */ /** Append incoming chunk to the rx ring buffer, growing if needed. */
@ -158,26 +191,39 @@ function getOrOpenShmFd(windowName: string, pid: number, fd: number): number {
return extFd; return extFd;
} }
function getReusableShmBuffer(windowName: string, size: number): Buffer { function getOrCreateSab(windowName: string, width: number, height: number, stride: number): SabEntry {
const existing = shmReadBufferCache.get(windowName); const existing = surfaceSabCache.get(windowName);
if (existing && existing.length >= size) { const neededSize = stride * height;
if (existing && existing.sab.byteLength >= neededSize && existing.width === width && existing.height === height && existing.stride === stride) {
return existing; return existing;
} }
const next = Buffer.allocUnsafe(size); const sab = new SharedArrayBuffer(neededSize);
shmReadBufferCache.set(windowName, next); const entry: SabEntry = { sab, width, height, stride };
return next; surfaceSabCache.set(windowName, entry);
// Tell renderer about the new SAB so it can reference it
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.postMessage('wo:surface-sab', {
name: windowName,
sab,
width,
height,
stride,
});
}
return entry;
} }
function flushSurfaceBuffers() { function flushSurfaceUpdates() {
surfaceBufferFlushScheduled = false; surfaceUpdateFlushScheduled = false;
if (!mainWindow || mainWindow.isDestroyed()) { surfaceBufferPending.clear(); return; } if (!mainWindow || mainWindow.isDestroyed()) { surfaceUpdatePending.clear(); return; }
for (const entry of surfaceBufferPending.values()) { for (const entry of surfaceUpdatePending.values()) {
mainWindow.webContents.send('wo:surface-buffer', entry); // Send only metadata (no pixels) — the renderer reads from SAB directly
mainWindow.webContents.send('wo:surface-update', entry);
} }
surfaceBufferPending.clear(); surfaceUpdatePending.clear();
} }
const inFlightFrameSeqs = new Set<string>(); const inFlightFrameSeqs = new Set<string>();
const MAX_IN_FLIGHT_FRAMES = 2; const MAX_IN_FLIGHT_FRAMES = 3;
// When the app uses compositor.submitDamageFrame() directly, skip OSR paint path to avoid double submission // When the app uses compositor.submitDamageFrame() directly, skip OSR paint path to avoid double submission
let appUsedDamageHelper = false; let appUsedDamageHelper = false;
// Track pointer position for mouse button/scroll events (window-local coords) // Track pointer position for mouse button/scroll events (window-local coords)
@ -1084,6 +1130,23 @@ function connectToCompositor(): Promise<void> {
try { try {
const parsed = JSON.parse(metadata); const parsed = JSON.parse(metadata);
if (parsed && Array.isArray(parsed.windows)) { if (parsed && Array.isArray(parsed.windows)) {
// Clean up caches for windows that disappeared
const currentNames = new Set((parsed.windows as Array<{name?: string}>).map(w => w.name).filter(Boolean));
for (const cachedName of surfaceSabCache.keys()) {
if (!currentNames.has(cachedName)) {
surfaceSabCache.delete(cachedName);
}
}
for (const cachedName of shmFdCache.keys()) {
if (!currentNames.has(cachedName)) {
const entry = shmFdCache.get(cachedName);
if (entry) {
try { fs.closeSync(entry.extFd); } catch { /* ignore */ }
}
shmFdCache.delete(cachedName);
}
}
compositorWindows = parsed.windows; compositorWindows = parsed.windows;
mainWindow.webContents.send('wo:windows', compositorWindows); mainWindow.webContents.send('wo:windows', compositorWindows);
} }
@ -1125,13 +1188,13 @@ function connectToCompositor(): Promise<void> {
try { try {
const extFd = getOrOpenShmFd(windowName, pid, fd); const extFd = getOrOpenShmFd(windowName, pid, fd);
const fullSize = sbStride * sbHeight; const fullSize = sbStride * sbHeight;
const safeBuffer = getReusableShmBuffer(windowName, fullSize);
// Partial read: only read damaged rows from memfd. // Ensure a SAB exists for this window at the right dimensions
// Empty damageRects = full frame update (always do full read). const sabEntry = getOrCreateSab(windowName, sbWidth, sbHeight, sbStride);
// Also do full read on first frame for this window (buffer is uninitialized).
const hasUsableRects = damageRects.length > 0 && damageRects.length < 64; const hasUsableRects = damageRects.length > 0 && damageRects.length < 64;
if (hasUsableRects) { if (hasUsableRects && nativeDmabuf?.copyMmapDamageToSab) {
// Partial damage: mmap and copy only damaged row bands into SAB
let minY = sbHeight, maxY = 0; let minY = sbHeight, maxY = 0;
for (const r of damageRects) { for (const r of damageRects) {
const ry = Math.max(0, r.y); const ry = Math.max(0, r.y);
@ -1140,36 +1203,36 @@ function connectToCompositor(): Promise<void> {
if (ryEnd > maxY) maxY = ryEnd; if (ryEnd > maxY) maxY = ryEnd;
} }
if (minY < maxY) { if (minY < maxY) {
const readOffset = minY * sbStride; nativeDmabuf.copyMmapDamageToSab(extFd, sabEntry.sab, sbStride, [
const readLen = (maxY - minY) * sbStride; { y: minY, h: maxY - minY },
let totalRead = 0; ]);
while (totalRead < readLen) {
const bytesRead = fs.readSync(extFd, safeBuffer, readOffset + totalRead, readLen - totalRead, readOffset + totalRead);
if (bytesRead <= 0) break;
totalRead += bytesRead;
}
} }
} else if (nativeDmabuf?.copyMmapToSab) {
// Full frame: mmap entire buffer into SAB
nativeDmabuf.copyMmapToSab(extFd, sabEntry.sab, fullSize);
} else { } else {
// Full read (no rects, too many rects, or first frame) // Fallback: fs.readSync into a temporary buffer, then copy to SAB
const tmpBuf = Buffer.alloc(fullSize);
let totalRead = 0; let totalRead = 0;
while (totalRead < fullSize) { while (totalRead < fullSize) {
const bytesRead = fs.readSync(extFd, safeBuffer, totalRead, fullSize - totalRead, totalRead); const bytesRead = fs.readSync(extFd, tmpBuf, totalRead, fullSize - totalRead, totalRead);
if (bytesRead <= 0) break; if (bytesRead <= 0) break;
totalRead += bytesRead; totalRead += bytesRead;
} }
new Uint8Array(sabEntry.sab).set(new Uint8Array(tmpBuf.buffer, tmpBuf.byteOffset, tmpBuf.byteLength));
} }
surfaceBufferPending.set(windowName, { surfaceUpdatePending.set(windowName, {
name: windowName, name: windowName,
width: sbWidth, width: sbWidth,
height: sbHeight, height: sbHeight,
stride: sbStride, stride: sbStride,
pixels: safeBuffer, generation: ++surfaceUpdateGeneration,
damageRects: hasUsableRects ? damageRects : undefined, damageRects: hasUsableRects ? damageRects : undefined,
}); });
if (!surfaceBufferFlushScheduled) { if (!surfaceUpdateFlushScheduled) {
surfaceBufferFlushScheduled = true; surfaceUpdateFlushScheduled = true;
setImmediate(flushSurfaceBuffers); setImmediate(flushSurfaceUpdates);
} }
} catch (err) { } catch (err) {
const cached = shmFdCache.get(windowName); const cached = shmFdCache.get(windowName);
@ -1311,7 +1374,13 @@ function connectToCompositor(): Promise<void> {
ipcSocket = null; ipcSocket = null;
dmabufSender = null; dmabufSender = null;
inFlightFrameSeqs.clear(); inFlightFrameSeqs.clear();
compositorRxBuffer = Buffer.alloc(0); compositorRxBuffer = Buffer.alloc(64 * 1024);
rxWriteOffset = 0;
rxReadOffset = 0;
// Clean up surface caches
surfaceSabCache.clear();
surfaceUpdatePending.clear();
closeShmFdCache();
scheduleIpcReconnect(); scheduleIpcReconnect();
}); });
@ -1736,6 +1805,18 @@ async function createWindow() {
show: CLIENT_MODE, show: CLIENT_MODE,
}); });
// Enable SharedArrayBuffer in the renderer by injecting
// cross-origin isolation headers on all responses.
mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Cross-Origin-Opener-Policy': ['same-origin'],
'Cross-Origin-Embedder-Policy': ['require-corp'],
},
});
});
debugLog('[Wo] BrowserWindow created, CLIENT_MODE=' + CLIENT_MODE + ' offscreen=' + !CLIENT_MODE); debugLog('[Wo] BrowserWindow created, CLIENT_MODE=' + CLIENT_MODE + ' offscreen=' + !CLIENT_MODE);
setupIpcHandlers(); setupIpcHandlers();
@ -1775,12 +1856,22 @@ async function createWindow() {
mainWindow.webContents.on('did-finish-load', () => { mainWindow.webContents.on('did-finish-load', () => {
mainWindow!.webContents.setFrameRate(WINDOW_CONFIG.fps ?? 60); mainWindow!.webContents.setFrameRate(WINDOW_CONFIG.fps ?? 60);
}); });
// Invalidate periodically to ensure continuous repaints for dynamic
// content. We only invalidate when below the in-flight limit so we
// don't pile up redundant paints while the compositor is still
// processing previous frames.
const frameIntervalMs = Math.round(1000 / (WINDOW_CONFIG.fps ?? 60)); const frameIntervalMs = Math.round(1000 / (WINDOW_CONFIG.fps ?? 60));
setInterval(() => { setInterval(() => {
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed() && inFlightFrameSeqs.size < MAX_IN_FLIGHT_FRAMES) {
mainWindow.webContents.invalidate(); mainWindow.webContents.invalidate();
} }
}, frameIntervalMs); }, frameIntervalMs);
// Reusable patch buffer to avoid allocating a new typed array on
// every partial-update paint event.
let patchBuf: Uint8Array | null = null;
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) {
@ -1802,7 +1893,7 @@ async function createWindow() {
if (!damageBuffer) { if (!damageBuffer) {
damageBuffer = new DamageBuffer(size.width, size.height); damageBuffer = new DamageBuffer(size.width, size.height);
} }
const pixels = image.getBitmap(); const pixels = image.toBitmap();
const pixelData = new Uint8Array(pixels.buffer, pixels.byteOffset, pixels.byteLength); const pixelData = new Uint8Array(pixels.buffer, pixels.byteOffset, pixels.byteLength);
// Use dirty rect to avoid full-frame copy when only a region changed // Use dirty rect to avoid full-frame copy when only a region changed
@ -1816,10 +1907,18 @@ async function createWindow() {
fullFrame: pixelData, fullFrame: pixelData,
}); });
} else { } else {
// Partial update — copy only the dirty region into the damage buffer // Partial update — copy only the dirty region into the damage
// buffer. Reuse a single scratch buffer to avoid per-frame GC
// pressure from allocating a new Uint8Array every paint.
const stride = size.width * 4; const stride = size.width * 4;
const patchStride = dirty.width * 4; const patchStride = dirty.width * 4;
const patchData = new Uint8Array(dirty.width * dirty.height * 4); const patchBytes = patchStride * dirty.height;
if (!patchBuf || patchBuf.byteLength < patchBytes) {
patchBuf = new Uint8Array(patchBytes);
}
const patchData = patchBuf.byteLength === patchBytes
? patchBuf
: patchBuf.subarray(0, patchBytes);
for (let row = 0; row < dirty.height; row++) { for (let row = 0; row < dirty.height; row++) {
const srcOff = (dirty.y + row) * stride + dirty.x * 4; const srcOff = (dirty.y + row) * stride + dirty.x * 4;
const dstOff = row * patchStride; const dstOff = row * patchStride;
@ -1886,6 +1985,12 @@ async function createWindow() {
app.on('ready', async () => { app.on('ready', async () => {
debugLog('[Wo] App ready, CLIENT_MODE=', CLIENT_MODE, 'WO_STANDALONE=', process.env.WO_STANDALONE); debugLog('[Wo] App ready, CLIENT_MODE=', CLIENT_MODE, 'WO_STANDALONE=', process.env.WO_STANDALONE);
// Log GPU status so we can verify hardware acceleration is active.
const gpuInfo = app.getGPUFeatureStatus();
debugLog('[Wo] GPU feature status:', JSON.stringify(gpuInfo));
debugLog('[Wo] GPU info:', JSON.stringify(app.getGPUInfo('basic').catch(() => ({}))));
try { try {
startPortalUiBridge(); startPortalUiBridge();

View file

@ -51,6 +51,7 @@ const compositorAPI: CompositorAPI = {
}, },
// Surface buffer: Wayland window pixel content from compositor // Surface buffer: Wayland window pixel content from compositor
// Legacy path for backward compatibility (full pixel copy via IPC)
onSurfaceBuffer: (callback: (data: { name: string; width: number; height: number; stride: number; pixels: Buffer; damageRects?: Array<{x: number; y: number; width: number; height: number}> }) => 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; damageRects?: Array<{x: number; y: number; width: number; height: number}> }) => 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);
@ -58,6 +59,23 @@ const compositorAPI: CompositorAPI = {
return () => ipcRenderer.removeListener('wo:surface-buffer', handler); return () => ipcRenderer.removeListener('wo:surface-buffer', handler);
}, },
// Zero-copy surface buffer via SharedArrayBuffer.
// 'wo:surface-sab' delivers the SAB once per window (or on resize).
// 'wo:surface-update' is a lightweight signal (no pixels) when new data is ready.
onSurfaceSab: (callback: (data: { name: string; sab: SharedArrayBuffer; width: number; height: number; stride: number }) => void) => {
const handler = (_event: Electron.IpcRendererEvent, data: { name: string; sab: SharedArrayBuffer; width: number; height: number; stride: number }) =>
callback(data);
ipcRenderer.on('wo:surface-sab', handler);
return () => ipcRenderer.removeListener('wo:surface-sab', handler);
},
onSurfaceUpdate: (callback: (data: { name: string; width: number; height: number; stride: number; generation: number; 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; generation: number; damageRects?: Array<{x: number; y: number; width: number; height: number}> }) =>
callback(data);
ipcRenderer.on('wo:surface-update', handler);
return () => ipcRenderer.removeListener('wo:surface-update', handler);
},
createDamageHelper: (options: RendererDamageHelperOptions): RendererDamageHelper => { createDamageHelper: (options: RendererDamageHelperOptions): RendererDamageHelper => {
return createRendererDamageHelper( return createRendererDamageHelper(
async (payload: DamageFramePayload) => ipcRenderer.invoke('wo:submit-damage-frame', payload), async (payload: DamageFramePayload) => ipcRenderer.invoke('wo:submit-damage-frame', payload),

View file

@ -2,7 +2,7 @@
"name": "@wl-wo/wo-types", "name": "@wl-wo/wo-types",
"module": "index.ts", "module": "index.ts",
"type": "module", "type": "module",
"version": "0.3.0", "version": "1.0.0",
"devDependencies": { "devDependencies": {
"@types/bun": "latest" "@types/bun": "latest"
}, },

View file

@ -299,6 +299,18 @@ export interface Compositor {
* Fires when Wayland clients (e.g. OBS, grim) capture the screen via wlr-screencopy. * Fires when Wayland clients (e.g. OBS, grim) capture the screen via wlr-screencopy.
*/ */
onScreencopyEvent(callback: (data: ScreencopyEvent) => void): Unsubscribe; onScreencopyEvent(callback: (data: ScreencopyEvent) => void): Unsubscribe;
/**
* Receive a SharedArrayBuffer for a given Wayland surface.
* Sent once when the SAB is first allocated or reallocated.
*/
onSurfaceSab(callback: (data: SurfaceSabData) => void): Unsubscribe;
/**
* Subscribe to lightweight surface update signals (no pixel payload).
* After receiving this, read pixels directly from the previously-sent SAB.
*/
onSurfaceUpdate(callback: (data: SurfaceUpdateData) => void): Unsubscribe;
} }
/** Alias for Compositor to match preload.ts imports */ /** Alias for Compositor to match preload.ts imports */
@ -316,6 +328,30 @@ export interface SurfaceBufferData {
damageRects?: DamageRect[]; damageRects?: DamageRect[];
} }
/**
* SharedArrayBuffer registration for a Wayland surface.
* Sent once per window when the SAB is first created or reallocated.
*/
export interface SurfaceSabData {
name: string;
width: number;
height: number;
stride: number;
sab: SharedArrayBuffer;
}
/**
* Lightweight surface update signal (no pixel data).
* The renderer should read from the previously-received SAB.
*/
export interface SurfaceUpdateData {
name: string;
width: number;
height: number;
stride: number;
damageRects?: DamageRect[];
}
/** /**
* Notification data from the compositor * Notification data from the compositor
*/ */

View file

@ -8,6 +8,7 @@ use smithay::utils::{Physical, Rectangle};
use crate::dmabuf::ElectronTexture; use crate::dmabuf::ElectronTexture;
/// Common interface for rendering frames across different backends /// Common interface for rendering frames across different backends
#[allow(dead_code)]
pub trait RenderBackend { pub trait RenderBackend {
/// Begin rendering a new frame with the given size and transform /// Begin rendering a new frame with the given size and transform
fn begin_frame(&mut self, width: i32, height: i32) -> Result<()>; fn begin_frame(&mut self, width: i32, height: i32) -> Result<()>;
@ -36,12 +37,14 @@ pub trait RenderBackend {
} }
/// Frame abstraction for rendering operations /// Frame abstraction for rendering operations
#[allow(dead_code)]
pub struct Frame { pub struct Frame {
pub transform: smithay::utils::Transform, pub transform: smithay::utils::Transform,
pub output_rect: Rectangle<i32, Physical>, pub output_rect: Rectangle<i32, Physical>,
} }
impl Frame { impl Frame {
#[allow(dead_code)]
pub fn new(output_rect: Rectangle<i32, Physical>) -> Self { pub fn new(output_rect: Rectangle<i32, Physical>) -> Self {
Frame { Frame {
transform: smithay::utils::Transform::Normal, transform: smithay::utils::Transform::Normal,

View file

@ -7,7 +7,7 @@ use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::gles::GlesRenderer; use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::gles::GlesTexture; use smithay::backend::renderer::gles::GlesTexture;
use smithay::backend::renderer::ImportMem; use smithay::backend::renderer::ImportMem;
use smithay::utils::{Buffer as BufferCoord, Size}; use smithay::utils::Size;
use std::collections::HashMap; use std::collections::HashMap;
use tracing::{info, warn}; use tracing::{info, warn};

View file

@ -48,7 +48,6 @@ impl WindowDmabufCache {
/// Returns Ok(true) if pixels were successfully updated. /// Returns Ok(true) if pixels were successfully updated.
#[allow(dead_code)] #[allow(dead_code)]
pub fn update_pixels(&mut self, pixels: &[u8]) -> Result<()> { pub fn update_pixels(&mut self, pixels: &[u8]) -> Result<()> {
use std::os::unix::io::AsRawFd;
use nix::unistd; use nix::unistd;
// Seek to start // Seek to start
@ -209,6 +208,7 @@ pub fn export_dmabuf_for_client(texture: &ElectronTexture) -> Dmabuf {
} }
/// Re-import a cached DMABUF frame into a renderer (used during rendering). /// Re-import a cached DMABUF frame into a renderer (used during rendering).
#[allow(dead_code)]
pub fn reimport_cached_frame( pub fn reimport_cached_frame(
renderer: &mut GlesRenderer, renderer: &mut GlesRenderer,
cached: &CachedDmabufFrame, cached: &CachedDmabufFrame,
@ -277,8 +277,6 @@ pub fn export_texture_as_dmabuf(
// For now, we'll read pixels and create a DMABUF from them // For now, we'll read pixels and create a DMABUF from them
// A more optimal approach would use EGLImage to back the DMABUF, but // A more optimal approach would use EGLImage to back the DMABUF, but
// this is simpler and still zero-copy at the IPC boundary // this is simpler and still zero-copy at the IPC boundary
use smithay::utils::Rectangle;
// This would need a mutable reference, which we don't have // This would need a mutable reference, which we don't have
// So instead, for each window we'll create DMABUF on-demand after pixelreads // So instead, for each window we'll create DMABUF on-demand after pixelreads
anyhow::bail!("texture export not directly supported; use create_temp_dmabuf_from_pixels after reading pixels") anyhow::bail!("texture export not directly supported; use create_temp_dmabuf_from_pixels after reading pixels")

View file

@ -54,6 +54,7 @@ pub const MAGIC_SCREENCOPY_EVENT: u32 = 0x574F5345; // "WOSE" — screencopy cap
#[repr(C, packed)] #[repr(C, packed)]
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
#[allow(dead_code)]
pub struct PlaneInfoWire { pub struct PlaneInfoWire {
pub offset: u32, pub offset: u32,
pub stride: u32, pub stride: u32,
@ -329,7 +330,6 @@ impl ElectronClientConnection {
// For each plane: fd(4) + offset(8) + stride(4) + modifier(8) // For each plane: fd(4) + offset(8) + stride(4) + modifier(8)
// Note: We'll collect actual plane data from dmabuf handles iterator // Note: We'll collect actual plane data from dmabuf handles iterator
let mut plane_idx = 0;
for _handle in dmabuf.handles() { for _handle in dmabuf.handles() {
// fd: will be sent via ancillary data, so we put 0 as placeholder // fd: will be sent via ancillary data, so we put 0 as placeholder
buf[off..off + 4].copy_from_slice(&0u32.to_le_bytes()); buf[off..off + 4].copy_from_slice(&0u32.to_le_bytes());
@ -345,7 +345,6 @@ impl ElectronClientConnection {
let modifier_val: u64 = format.modifier.into(); let modifier_val: u64 = format.modifier.into();
buf[off..off + 8].copy_from_slice(&modifier_val.to_le_bytes()); buf[off..off + 8].copy_from_slice(&modifier_val.to_le_bytes());
off += 8; off += 8;
plane_idx += 1;
} }
// Send metadata with file descriptors as ancillary data // Send metadata with file descriptors as ancillary data
@ -600,25 +599,39 @@ impl ElectronProcess {
.env("WO_IPC_SOCKET", ipc_socket) .env("WO_IPC_SOCKET", ipc_socket)
.env("WO_WINDOW_CONFIG", &serialized) .env("WO_WINDOW_CONFIG", &serialized)
.env("WO_DRM_RENDER_NODE", render_node) .env("WO_DRM_RENDER_NODE", render_node)
// Headless Ozone backend: Electron is used purely for offscreen // Wayland Ozone platform: Electron connects to our own Wayland
// rendering — it must not try to connect to a Wayland or X11 // compositor (WAYLAND_DISPLAY is already set in the env) for
// display server (there is none for it to connect to; *we* are // GPU/EGL initialisation. This gives Chromium a proper
// the compositor). The headless platform lets Chromium's OSR // wayland-egl display so hardware-accelerated rasterisation
// paint pipeline work while the native wo_dmabuf module handles // works. The BrowserWindow still uses `offscreen: true`, so
// GPU buffer allocation via its own GBM/EGL context on the // no Wayland surfaces are mapped — the connection is used
// render node. // solely for the EGL context.
.arg("--ozone-platform=headless") //
// The previous `--ozone-platform=headless` approach failed
// because headless Ozone has no EGL display, causing the GPU
// subprocess to crash with "gl=none,angle=none not found" and
// silently falling back to ~1 FPS software rasterisation.
.arg("--ozone-platform=wayland")
.arg("--no-sandbox") .arg("--no-sandbox")
.arg("--disable-gpu-sandbox") .arg("--disable-gpu-sandbox")
// Use the *software* GL implementation inside Chromium for its // Run GPU code in the main Electron process to avoid the
// own compositing (we do not need hardware-accelerated web // separate GPU subprocess which adds IPC overhead and can
// content — the paint callback gives us BGRA bitmaps that the // fail to initialise independently.
// native module imports into a GBM BO). This avoids Chromium .arg("--in-process-gpu")
// fighting with the compositor over the DRM render node. // Use native EGL (provided by wayland-egl through the Ozone
.arg("--use-gl=swiftshader") // Wayland platform) for Chromium's compositor.
// Make sure OSR (offscreen) rendering actually produces .arg("--use-gl=egl")
// paint callbacks at the requested frame rate. // Disable GPU vsync — we are rendering offscreen, there is
.arg("--enable-features=Vulkan") // no physical display to synchronise with.
.arg("--disable-gpu-vsync")
// Enable GPU-accelerated rasterization and zero-copy features
// so OSR paint callbacks deliver GPU-backed bitmaps instead
// of slow CPU readbacks.
.arg("--enable-features=CanvasOopRasterization,Vulkan,VaapiVideoDecoder,RawDraw,SharedArrayBuffer")
// Use multiple raster threads for parallelised tile painting.
.arg("--num-raster-threads=4")
// Don't throttle renderers that lose focus.
.arg("--disable-renderer-backgrounding")
.stdin(Stdio::null()) .stdin(Stdio::null())
.stdout(Stdio::inherit()) .stdout(Stdio::inherit())
.stderr(Stdio::inherit()) .stderr(Stdio::inherit())
@ -1253,7 +1266,7 @@ fn write_nonblocking(fd: RawFd, buf: &[u8]) -> Result<()> {
return match send(fd, buf, MsgFlags::MSG_DONTWAIT) { return match send(fd, buf, MsgFlags::MSG_DONTWAIT) {
Ok(n) if n == buf.len() => Ok(()), Ok(n) if n == buf.len() => Ok(()),
Ok(_) => Ok(()), // partial write — drop silently, stream may be corrupt Ok(_) => Ok(()), // partial write — drop silently, stream may be corrupt
Err(nix::errno::Errno::EAGAIN) | Err(nix::errno::Errno::EWOULDBLOCK) => Ok(()), Err(nix::errno::Errno::EAGAIN) => Ok(()),
Err(e) => Err(anyhow::anyhow!("IPC non-blocking write: {e}")), Err(e) => Err(anyhow::anyhow!("IPC non-blocking write: {e}")),
}; };
} }
@ -1280,7 +1293,7 @@ fn write_nonblocking(fd: RawFd, buf: &[u8]) -> Result<()> {
"IPC partial non-blocking write: {n}/{} bytes sent despite space check", "IPC partial non-blocking write: {n}/{} bytes sent despite space check",
buf.len() buf.len()
)), )),
Err(nix::errno::Errno::EAGAIN) | Err(nix::errno::Errno::EWOULDBLOCK) => Ok(()), Err(nix::errno::Errno::EAGAIN) => Ok(()),
Err(e) => Err(anyhow::anyhow!("IPC non-blocking write: {e}")), Err(e) => Err(anyhow::anyhow!("IPC non-blocking write: {e}")),
} }
} }

View file

@ -436,7 +436,7 @@ impl WoState {
let pos: Point<f64, Logical> = self.pointer_location; let pos: Point<f64, Logical> = self.pointer_location;
let under_surface = self.space.element_under(pos); let under_surface = self.space.element_under(pos);
if let Some((window, surface_loc)) = under_surface { if let Some((window, _surface_loc)) = under_surface {
let surface_opt = if let Some(x11) = window.x11_surface() { let surface_opt = if let Some(x11) = window.x11_surface() {
x11.wl_surface() x11.wl_surface()
} else { } else {

View file

@ -3,7 +3,6 @@
/// Implements `zwlr_screencopy_manager_v1` allowing clients like grim, OBS, etc. /// Implements `zwlr_screencopy_manager_v1` allowing clients like grim, OBS, etc.
/// to request frame copies of outputs or output regions. /// to request frame copies of outputs or output regions.
use std::collections::HashMap;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
use wayland_protocols_wlr::screencopy::v1::server::{ use wayland_protocols_wlr::screencopy::v1::server::{
@ -12,7 +11,7 @@ use wayland_protocols_wlr::screencopy::v1::server::{
}; };
use wayland_server::{ use wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
protocol::{wl_buffer::WlBuffer, wl_output::WlOutput, wl_shm}, protocol::{wl_buffer::WlBuffer, wl_shm},
}; };
use crate::state::WoState; use crate::state::WoState;

View file

@ -50,6 +50,7 @@ impl XdgShellHandler for WoState {
self.ssd_windows.remove(&oid); self.ssd_windows.remove(&oid);
#[allow(deprecated)]
let window = Window::new(surface); let window = Window::new(surface);
self.all_windows.push(window.clone()); self.all_windows.push(window.clone());
@ -246,7 +247,7 @@ impl XdgShellHandler for WoState {
.find(|w| w.toplevel().map(|t| t == &surface).unwrap_or(false)) .find(|w| w.toplevel().map(|t| t == &surface).unwrap_or(false))
.cloned(); .cloned();
if let Some(window) = window { if let Some(_window) = window {
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State as XdgState; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State as XdgState;
let (ow, oh) = (self.output_size.0 as i32, self.output_size.1 as i32); let (ow, oh) = (self.output_size.0 as i32, self.output_size.1 as i32);

View file

@ -255,6 +255,7 @@ impl XwmHandler for WoState {
let (ow, oh) = (self.output_size.0 as i32, self.output_size.1 as i32); let (ow, oh) = (self.output_size.0 as i32, self.output_size.1 as i32);
let content_h = (oh - y_offset).max(1); let content_h = (oh - y_offset).max(1);
#[allow(deprecated)]
if let Err(e) = window.configure(Some(Rectangle::from_loc_and_size((0, y_offset), (ow, content_h)))) { if let Err(e) = window.configure(Some(Rectangle::from_loc_and_size((0, y_offset), (ow, content_h)))) {
warn!("XWayland: fullscreen configure failed: {e}"); warn!("XWayland: fullscreen configure failed: {e}");
} }
@ -300,6 +301,7 @@ impl XwmHandler for WoState {
let suggest_w = (ow * 2 / 3).max(640).min(1600); let suggest_w = (ow * 2 / 3).max(640).min(1600);
let suggest_h = (oh * 2 / 3).max(480).min(1000); let suggest_h = (oh * 2 / 3).max(480).min(1000);
#[allow(deprecated)]
if let Err(e) = window.configure(Some(Rectangle::from_loc_and_size((0, 0), (suggest_w, suggest_h)))) { if let Err(e) = window.configure(Some(Rectangle::from_loc_and_size((0, 0), (suggest_w, suggest_h)))) {
warn!("XWayland: unfullscreen configure failed: {e}"); warn!("XWayland: unfullscreen configure failed: {e}");
} }

View file

@ -15,7 +15,7 @@ use gbm::Modifier;
use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus}; use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
use smithay::backend::allocator::{Fourcc, Slot}; use smithay::backend::allocator::{Fourcc, Slot};
use smithay::backend::renderer::element::{Element, RenderElement}; use smithay::backend::renderer::element::Element;
use smithay::wayland::xdg_activation::{XdgActivationToken, XdgActivationTokenData}; use smithay::wayland::xdg_activation::{XdgActivationToken, XdgActivationTokenData};
use smithay::{ use smithay::{
backend::{ backend::{
@ -770,8 +770,6 @@ fn render_frame(data: &mut EventLoopData) {
use smithay::reexports::wayland_server::Resource; use smithay::reexports::wayland_server::Resource;
use smithay::wayland::compositor::{with_states, Damage, SurfaceAttributes}; use smithay::wayland::compositor::{with_states, Damage, SurfaceAttributes};
use smithay::wayland::shm::with_buffer_contents; use smithay::wayland::shm::with_buffer_contents;
use std::os::unix::io::AsRawFd;
use nix::sys::memfd::MFdFlags;
use tracing::debug; use tracing::debug;
let pid = std::process::id(); let pid = std::process::id();

View file

@ -5,7 +5,6 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use drm_fourcc::DrmFourcc; use drm_fourcc::DrmFourcc;
use nix::sys::memfd::MFdFlags;
use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus}; use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
use smithay::{ use smithay::{
backend::{ backend::{
@ -501,6 +500,12 @@ pub fn run_nested(config: Config) -> Result<()> {
}; };
if last_render.elapsed() < adaptive_frame_time { if last_render.elapsed() < adaptive_frame_time {
// Sleep for the remaining frame budget instead of busy-looping.
// Cap the sleep to avoid missing events for too long.
let remaining = adaptive_frame_time.saturating_sub(last_render.elapsed());
if remaining > Duration::from_micros(500) {
std::thread::sleep(remaining.min(Duration::from_millis(2)));
}
continue; continue;
} }
last_render = std::time::Instant::now(); last_render = std::time::Instant::now();
@ -1144,7 +1149,17 @@ pub fn run_nested(config: Config) -> Result<()> {
warn!("render+submit+callbacks total took {}ms!", render_total_ms); warn!("render+submit+callbacks total took {}ms!", render_total_ms);
} }
std::thread::sleep(Duration::from_micros(100)); // Sleep for the remaining frame budget to avoid burning CPU.
// The per-frame work above (dispatch + render + submit + callbacks) eats
// some of the budget; sleep the rest so the next iteration starts close
// to the target frame boundary.
let elapsed = loop_t0.elapsed();
let target = if input_events_pending { Duration::from_millis(8) } else { frame_time };
if elapsed < target {
let remaining = target - elapsed;
// Don't sleep more than 4ms to stay responsive to new Wayland/IPC events
std::thread::sleep(remaining.min(Duration::from_millis(4)));
}
// Log if the entire loop iteration was slow. // Log if the entire loop iteration was slow.
let total_loop_ms = loop_t0.elapsed().as_millis(); let total_loop_ms = loop_t0.elapsed().as_millis();

View file

@ -612,7 +612,7 @@ impl WoState {
/// The actual forwarding is handled by process_input_event. /// The actual forwarding is handled by process_input_event.
pub fn forward_input_to_electron<B: smithay::backend::input::InputBackend>( pub fn forward_input_to_electron<B: smithay::backend::input::InputBackend>(
&mut self, &mut self,
event: &smithay::backend::input::InputEvent<B>, _event: &smithay::backend::input::InputEvent<B>,
) { ) {
// The actual forwarding is done by process_input_event which is called separately // The actual forwarding is done by process_input_event which is called separately
// This is a no-op wrapper that exists for API compatibility with nested.rs // This is a no-op wrapper that exists for API compatibility with nested.rs

File diff suppressed because it is too large Load diff