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) {
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;
}
const rowBytes = Math.min(frameStride, this.stride);
for (let y = 0;y < this.height; y += 1) {
const srcStart = y * frameStride;
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) {
@ -109,7 +109,7 @@ class DamageBuffer {
for (let row = 0;row < rect.height; row += 1) {
const srcStart = row * patchStride;
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) {
@ -124,40 +124,49 @@ class WoDmabufSender {
windowName;
native;
seq = 1n;
wireBuf;
nameLen;
seqOffset;
widthOffset;
heightOffset;
constructor(socket, windowName, native) {
this.socket = socket;
this.windowName = windowName;
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) {
const imported = this.native.importRgba(buffer.getFrameBuffer(), buffer.getWidth(), buffer.getHeight(), buffer.getStride());
try {
const socketFd = getSocketFd(this.socket);
const nameBuf = Buffer.from(this.windowName, "utf8");
const header = Buffer.alloc(4 + 4 + nameBuf.length + 8 + 4 + 4 + 4 + 4);
let offset = 0;
header.writeUInt32LE(MAGIC.FRAME, offset);
offset += 4;
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.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.wireBuf.writeBigUInt64LE(this.seq, this.seqOffset);
this.wireBuf.writeUInt32LE(buffer.getWidth(), this.widthOffset);
this.wireBuf.writeUInt32LE(buffer.getHeight(), this.heightOffset);
const planeOff = this.wireBuf.length - 16;
this.wireBuf.writeUInt32LE(imported.offset, planeOff);
this.wireBuf.writeUInt32LE(imported.stride, planeOff + 4);
this.wireBuf.writeUInt32LE(imported.modifier_hi, planeOff + 8);
this.wireBuf.writeUInt32LE(imported.modifier_lo, planeOff + 12);
writeSync(socketFd, this.wireBuf, 0, this.wireBuf.length);
this.native.sendFd(socketFd, imported.fd);
const sentSeq = this.seq;
this.seq += 1n;
@ -205,12 +214,24 @@ var __filename2 = fileURLToPath(import.meta.url);
var __dirname2 = dirname(__filename2);
var require2 = createRequire2(import.meta.url);
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) {
const msg = `[${new Date().toISOString()}] ` + args.join(" ") + `
`;
try {
fs.appendFileSync(DEBUG_LOG, msg);
} catch (e) {}
_logBuffer.push(msg);
if (!_logTimer) {
_logTimer = setTimeout(_flushLog, 100);
}
console.log(...args);
}
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-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-features", "CanvasOopRasterization,Vulkan,VaapiVideoDecoder,VaapiVideoEncoder,SharedArrayBuffer,RawDraw");
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");
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);
@ -249,9 +271,10 @@ var nativeDmabuf = null;
var compositorRxBuffer = Buffer.alloc(64 * 1024);
var rxWriteOffset = 0;
var rxReadOffset = 0;
var surfaceBufferPending = new Map;
var surfaceBufferFlushScheduled = false;
var shmReadBufferCache = new Map;
var surfaceSabCache = new Map;
var surfaceUpdatePending = new Map;
var surfaceUpdateGeneration = 0;
var surfaceUpdateFlushScheduled = false;
var shmFdCache = new Map;
function rxAppend(chunk) {
const needed = rxWriteOffset + chunk.length;
@ -304,28 +327,39 @@ function getOrOpenShmFd(windowName, pid, fd) {
shmFdCache.set(windowName, { pid, fd, extFd });
return extFd;
}
function getReusableShmBuffer(windowName, size) {
const existing = shmReadBufferCache.get(windowName);
if (existing && existing.length >= size) {
function getOrCreateSab(windowName, width2, height2, stride) {
const existing = surfaceSabCache.get(windowName);
const neededSize = stride * height2;
if (existing && existing.sab.byteLength >= neededSize && existing.width === width2 && existing.height === height2 && existing.stride === stride) {
return existing;
}
const next = Buffer.allocUnsafe(size);
shmReadBufferCache.set(windowName, next);
return next;
const sab = new SharedArrayBuffer(neededSize);
const entry = { sab, width: width2, height: height2, stride };
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() {
surfaceBufferFlushScheduled = false;
function flushSurfaceUpdates() {
surfaceUpdateFlushScheduled = false;
if (!mainWindow || mainWindow.isDestroyed()) {
surfaceBufferPending.clear();
surfaceUpdatePending.clear();
return;
}
for (const entry of surfaceBufferPending.values()) {
mainWindow.webContents.send("wo:surface-buffer", entry);
for (const entry of surfaceUpdatePending.values()) {
mainWindow.webContents.send("wo:surface-update", entry);
}
surfaceBufferPending.clear();
surfaceUpdatePending.clear();
}
var inFlightFrameSeqs = new Set;
var MAX_IN_FLIGHT_FRAMES = 2;
var MAX_IN_FLIGHT_FRAMES = 3;
var appUsedDamageHelper = false;
var pointerX = 0;
var pointerY = 0;
@ -1173,6 +1207,23 @@ function connectToCompositor() {
try {
const parsed = JSON.parse(metadata);
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;
mainWindow.webContents.send("wo:windows", compositorWindows);
}
@ -1219,9 +1270,9 @@ function connectToCompositor() {
try {
const extFd = getOrOpenShmFd(windowName, pid, fd);
const fullSize = sbStride * sbHeight;
const safeBuffer = getReusableShmBuffer(windowName, fullSize);
const sabEntry = getOrCreateSab(windowName, sbWidth, sbHeight, sbStride);
const hasUsableRects = damageRects.length > 0 && damageRects.length < 64;
if (hasUsableRects) {
if (hasUsableRects && nativeDmabuf?.copyMmapDamageToSab) {
let minY = sbHeight, maxY = 0;
for (const r of damageRects) {
const ry = Math.max(0, r.y);
@ -1232,36 +1283,34 @@ function connectToCompositor() {
maxY = ryEnd;
}
if (minY < maxY) {
const readOffset = minY * sbStride;
const readLen = (maxY - minY) * sbStride;
let totalRead = 0;
while (totalRead < readLen) {
const bytesRead = fs.readSync(extFd, safeBuffer, readOffset + totalRead, readLen - totalRead, readOffset + totalRead);
if (bytesRead <= 0)
break;
totalRead += bytesRead;
}
nativeDmabuf.copyMmapDamageToSab(extFd, sabEntry.sab, sbStride, [
{ y: minY, h: maxY - minY }
]);
}
} else if (nativeDmabuf?.copyMmapToSab) {
nativeDmabuf.copyMmapToSab(extFd, sabEntry.sab, fullSize);
} else {
const tmpBuf = Buffer.alloc(fullSize);
let totalRead = 0;
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;
totalRead += bytesRead;
}
new Uint8Array(sabEntry.sab).set(new Uint8Array(tmpBuf.buffer, tmpBuf.byteOffset, tmpBuf.byteLength));
}
surfaceBufferPending.set(windowName, {
surfaceUpdatePending.set(windowName, {
name: windowName,
width: sbWidth,
height: sbHeight,
stride: sbStride,
pixels: safeBuffer,
generation: ++surfaceUpdateGeneration,
damageRects: hasUsableRects ? damageRects : undefined
});
if (!surfaceBufferFlushScheduled) {
surfaceBufferFlushScheduled = true;
setImmediate(flushSurfaceBuffers);
if (!surfaceUpdateFlushScheduled) {
surfaceUpdateFlushScheduled = true;
setImmediate(flushSurfaceUpdates);
}
} catch (err) {
const cached = shmFdCache.get(windowName);
@ -1396,7 +1445,12 @@ function connectToCompositor() {
ipcSocket = null;
dmabufSender = null;
inFlightFrameSeqs.clear();
compositorRxBuffer = Buffer.alloc(0);
compositorRxBuffer = Buffer.alloc(64 * 1024);
rxWriteOffset = 0;
rxReadOffset = 0;
surfaceSabCache.clear();
surfaceUpdatePending.clear();
closeShmFdCache();
scheduleIpcReconnect();
});
setTimeout(() => {
@ -1762,6 +1816,15 @@ async function createWindow() {
},
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);
setupIpcHandlers();
debugLog("[Wo] setupIpcHandlers completed");
@ -1794,10 +1857,11 @@ async function createWindow() {
});
const frameIntervalMs = Math.round(1000 / (WINDOW_CONFIG.fps ?? 60));
setInterval(() => {
if (mainWindow && !mainWindow.isDestroyed()) {
if (mainWindow && !mainWindow.isDestroyed() && inFlightFrameSeqs.size < MAX_IN_FLIGHT_FRAMES) {
mainWindow.webContents.invalidate();
}
}, frameIntervalMs);
let patchBuf = null;
let skippedCount = 0;
mainWindow.webContents.on("paint", (_event, dirty, image) => {
if (!dmabufSender || !ipcConnected) {
@ -1819,7 +1883,7 @@ async function createWindow() {
if (!damageBuffer) {
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 isFullFrame = dirty.x === 0 && dirty.y === 0 && dirty.width === size.width && dirty.height === size.height;
if (isFullFrame) {
@ -1831,7 +1895,11 @@ async function createWindow() {
} else {
const stride = size.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++) {
const srcOff = (dirty.y + row) * stride + dirty.x * 4;
const dstOff = row * patchStride;
@ -1885,6 +1953,9 @@ async function createWindow() {
}
app.on("ready", async () => {
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 {
startPortalUiBridge();
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 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) {
std::lock_guard<std::mutex> lk(g_bufs_mutex);
uint32_t tok = g_next_token++;
@ -157,8 +202,13 @@ static void release_buf(uint32_t tok) {
if (it == g_bufs.end())
return;
::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);
// Return to pool for reuse instead of destroying
pool_return(bo, w, h, stride);
}
Napi::Value Init(const Napi::CallbackInfo &info) {
@ -195,44 +245,51 @@ Napi::Value ImportRgba(const Napi::CallbackInfo &info) {
return env.Null();
}
gbm_bo *bo = nullptr;
bo = gbm_bo_create_with_modifiers2(g_gbm, w, h, GBM_FORMAT_ARGB8888,
(const uint64_t[]){DRM_FORMAT_MOD_LINEAR},
1,
GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR);
// Try to reuse a pooled BO of matching dimensions
uint32_t dst_stride = 0;
gbm_bo *bo = pool_acquire(w, h, &dst_stride);
if (!bo) {
bo = gbm_bo_create(g_gbm, w, h, GBM_FORMAT_ARGB8888,
GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR);
}
if (!bo) {
bo = gbm_bo_create(g_gbm, w, h, GBM_FORMAT_ARGB8888, GBM_BO_USE_WRITE);
}
if (!bo) {
Napi::Error::New(
env, "gbm_bo_create failed: all buffer creation methods exhausted")
.ThrowAsJavaScriptException();
return env.Null();
bo = gbm_bo_create_with_modifiers2(g_gbm, w, h, GBM_FORMAT_ARGB8888,
(const uint64_t[]){DRM_FORMAT_MOD_LINEAR},
1,
GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR);
if (!bo) {
bo = gbm_bo_create(g_gbm, w, h, GBM_FORMAT_ARGB8888,
GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR);
}
if (!bo) {
bo = gbm_bo_create(g_gbm, w, h, GBM_FORMAT_ARGB8888, GBM_BO_USE_WRITE);
}
if (!bo) {
Napi::Error::New(
env, "gbm_bo_create failed: all buffer creation methods exhausted")
.ThrowAsJavaScriptException();
return env.Null();
}
}
void *map_data = nullptr;
uint32_t dst_stride = 0;
uint32_t map_stride = 0;
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) {
gbm_bo_destroy(bo);
Napi::Error::New(env, "gbm_bo_map failed").ThrowAsJavaScriptException();
return env.Null();
}
dst_stride = map_stride;
const uint8_t *src = pixel_buf.Data();
uint8_t *dst = static_cast<uint8_t *>(ptr);
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);
if (src_stride == dst_stride) {
// Fast path: strides match — single memcpy for entire image
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);
@ -508,6 +565,118 @@ Napi::Value MmapFd(const Napi::CallbackInfo &info) {
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) {
exports.Set("init", Napi::Function::New(env, Init));
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("recvFd", Napi::Function::New(env, RecvFd));
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("getTexture", Napi::Function::New(env, GetTexture));
exports.Set("releaseTexture", Napi::Function::New(env, ReleaseTexture));

View file

@ -60,6 +60,8 @@ type NativeDmabufModule = {
sendFd: (socketFd: number, dmabufFd: number) => void;
recvFd: (socketFd: number) => number;
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 };
getTexture: (windowName: string) => { texture: number; width: number; height: number } | null;
releaseTexture: (windowName: string) => void;
@ -105,7 +107,8 @@ export class DamageBuffer {
applyFullFrame(frame: Uint8Array, frameStride: number = this.width * 4): void {
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;
}
@ -113,7 +116,10 @@ export class DamageBuffer {
for (let y = 0; y < this.height; y += 1) {
const srcStart = y * frameStride;
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) {
const srcStart = row * patchStride;
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 native: NativeDmabufModule;
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) {
this.socket = socket;
this.windowName = windowName;
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 {
@ -164,32 +197,19 @@ export class WoDmabufSender {
try {
const socketFd = getSocketFd(this.socket);
const nameBuf = Buffer.from(this.windowName, 'utf8');
const header = Buffer.alloc(4 + 4 + nameBuf.length + 8 + 4 + 4 + 4 + 4);
let offset = 0;
header.writeUInt32LE(MAGIC.FRAME, offset); offset += 4;
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);
// Write per-frame fields into the pre-allocated buffer
this.wireBuf.writeBigUInt64LE(this.seq, this.seqOffset);
this.wireBuf.writeUInt32LE(buffer.getWidth(), this.widthOffset);
this.wireBuf.writeUInt32LE(buffer.getHeight(), this.heightOffset);
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);
// Plane data (last 16 bytes of wireBuf)
const planeOff = this.wireBuf.length - 16;
this.wireBuf.writeUInt32LE(imported.offset, planeOff);
this.wireBuf.writeUInt32LE(imported.stride, planeOff + 4);
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
// 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);
writeSync(socketFd, this.wireBuf, 0, this.wireBuf.length);
this.native.sendFd(socketFd, imported.fd);
const sentSeq = this.seq;

View file

@ -18,30 +18,44 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
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';
function debugLog(...args: any[]) {
const msg = `[${new Date().toISOString()}] ` + args.join(' ') + '\n';
let _logBuffer: string[] = [];
let _logTimer: ReturnType<typeof setTimeout> | null = null;
function _flushLog() {
if (_logBuffer.length === 0) return;
try {
fs.appendFileSync(DEBUG_LOG, msg);
fs.appendFileSync(DEBUG_LOG, _logBuffer.join(''));
} catch (e) {
// 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);
}
debugLog('[Wo] Electron starting, NODE_VERSION=', process.version);
// 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('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');
'CanvasOopRasterization,Vulkan,VaapiVideoDecoder,VaapiVideoEncoder,SharedArrayBuffer,RawDraw');
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');
type ClientWindowInfo = {
@ -85,9 +99,28 @@ let nativeDmabuf: any = null;
let compositorRxBuffer = Buffer.alloc(64 * 1024);
let rxWriteOffset = 0; // next write 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}> }>();
let surfaceBufferFlushScheduled = false;
const shmReadBufferCache = new Map<string, Buffer>();
// ── Zero-copy surface buffer transfer via SharedArrayBuffer ──
// One SAB per window. The main process mmap's compositor memfd data into
// 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 }>();
/** 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;
}
function getReusableShmBuffer(windowName: string, size: number): Buffer {
const existing = shmReadBufferCache.get(windowName);
if (existing && existing.length >= size) {
function getOrCreateSab(windowName: string, width: number, height: number, stride: number): SabEntry {
const existing = surfaceSabCache.get(windowName);
const neededSize = stride * height;
if (existing && existing.sab.byteLength >= neededSize && existing.width === width && existing.height === height && existing.stride === stride) {
return existing;
}
const next = Buffer.allocUnsafe(size);
shmReadBufferCache.set(windowName, next);
return next;
const sab = new SharedArrayBuffer(neededSize);
const entry: SabEntry = { sab, width, height, stride };
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() {
surfaceBufferFlushScheduled = false;
if (!mainWindow || mainWindow.isDestroyed()) { surfaceBufferPending.clear(); return; }
for (const entry of surfaceBufferPending.values()) {
mainWindow.webContents.send('wo:surface-buffer', entry);
function flushSurfaceUpdates() {
surfaceUpdateFlushScheduled = false;
if (!mainWindow || mainWindow.isDestroyed()) { surfaceUpdatePending.clear(); return; }
for (const entry of surfaceUpdatePending.values()) {
// 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 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
let appUsedDamageHelper = false;
// Track pointer position for mouse button/scroll events (window-local coords)
@ -1084,6 +1130,23 @@ function connectToCompositor(): Promise<void> {
try {
const parsed = JSON.parse(metadata);
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;
mainWindow.webContents.send('wo:windows', compositorWindows);
}
@ -1125,13 +1188,13 @@ function connectToCompositor(): Promise<void> {
try {
const extFd = getOrOpenShmFd(windowName, pid, fd);
const fullSize = sbStride * sbHeight;
const safeBuffer = getReusableShmBuffer(windowName, fullSize);
// Partial read: only read damaged rows from memfd.
// Empty damageRects = full frame update (always do full read).
// Also do full read on first frame for this window (buffer is uninitialized).
// Ensure a SAB exists for this window at the right dimensions
const sabEntry = getOrCreateSab(windowName, sbWidth, sbHeight, sbStride);
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;
for (const r of damageRects) {
const ry = Math.max(0, r.y);
@ -1140,36 +1203,36 @@ function connectToCompositor(): Promise<void> {
if (ryEnd > maxY) maxY = ryEnd;
}
if (minY < maxY) {
const readOffset = minY * sbStride;
const readLen = (maxY - minY) * sbStride;
let totalRead = 0;
while (totalRead < readLen) {
const bytesRead = fs.readSync(extFd, safeBuffer, readOffset + totalRead, readLen - totalRead, readOffset + totalRead);
if (bytesRead <= 0) break;
totalRead += bytesRead;
}
nativeDmabuf.copyMmapDamageToSab(extFd, sabEntry.sab, sbStride, [
{ y: minY, h: maxY - minY },
]);
}
} else if (nativeDmabuf?.copyMmapToSab) {
// Full frame: mmap entire buffer into SAB
nativeDmabuf.copyMmapToSab(extFd, sabEntry.sab, fullSize);
} 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;
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;
totalRead += bytesRead;
}
new Uint8Array(sabEntry.sab).set(new Uint8Array(tmpBuf.buffer, tmpBuf.byteOffset, tmpBuf.byteLength));
}
surfaceBufferPending.set(windowName, {
surfaceUpdatePending.set(windowName, {
name: windowName,
width: sbWidth,
height: sbHeight,
stride: sbStride,
pixels: safeBuffer,
generation: ++surfaceUpdateGeneration,
damageRects: hasUsableRects ? damageRects : undefined,
});
if (!surfaceBufferFlushScheduled) {
surfaceBufferFlushScheduled = true;
setImmediate(flushSurfaceBuffers);
if (!surfaceUpdateFlushScheduled) {
surfaceUpdateFlushScheduled = true;
setImmediate(flushSurfaceUpdates);
}
} catch (err) {
const cached = shmFdCache.get(windowName);
@ -1311,7 +1374,13 @@ function connectToCompositor(): Promise<void> {
ipcSocket = null;
dmabufSender = null;
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();
});
@ -1736,6 +1805,18 @@ async function createWindow() {
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);
setupIpcHandlers();
@ -1775,12 +1856,22 @@ async function createWindow() {
mainWindow.webContents.on('did-finish-load', () => {
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));
setInterval(() => {
if (mainWindow && !mainWindow.isDestroyed()) {
if (mainWindow && !mainWindow.isDestroyed() && inFlightFrameSeqs.size < MAX_IN_FLIGHT_FRAMES) {
mainWindow.webContents.invalidate();
}
}, 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;
mainWindow.webContents.on('paint', (_event, dirty, image) => {
if (!dmabufSender || !ipcConnected) {
@ -1802,7 +1893,7 @@ async function createWindow() {
if (!damageBuffer) {
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);
// Use dirty rect to avoid full-frame copy when only a region changed
@ -1816,10 +1907,18 @@ async function createWindow() {
fullFrame: pixelData,
});
} 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 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++) {
const srcOff = (dirty.y + row) * stride + dirty.x * 4;
const dstOff = row * patchStride;
@ -1886,6 +1985,12 @@ async function createWindow() {
app.on('ready', async () => {
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 {
startPortalUiBridge();

View file

@ -51,6 +51,7 @@ const compositorAPI: CompositorAPI = {
},
// 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) => {
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);
@ -58,6 +59,23 @@ const compositorAPI: CompositorAPI = {
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 => {
return createRendererDamageHelper(
async (payload: DamageFramePayload) => ipcRenderer.invoke('wo:submit-damage-frame', payload),

View file

@ -2,7 +2,7 @@
"name": "@wl-wo/wo-types",
"module": "index.ts",
"type": "module",
"version": "0.3.0",
"version": "1.0.0",
"devDependencies": {
"@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.
*/
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 */
@ -316,6 +328,30 @@ export interface SurfaceBufferData {
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
*/

View file

@ -8,6 +8,7 @@ use smithay::utils::{Physical, Rectangle};
use crate::dmabuf::ElectronTexture;
/// Common interface for rendering frames across different backends
#[allow(dead_code)]
pub trait RenderBackend {
/// Begin rendering a new frame with the given size and transform
fn begin_frame(&mut self, width: i32, height: i32) -> Result<()>;
@ -36,12 +37,14 @@ pub trait RenderBackend {
}
/// Frame abstraction for rendering operations
#[allow(dead_code)]
pub struct Frame {
pub transform: smithay::utils::Transform,
pub output_rect: Rectangle<i32, Physical>,
}
impl Frame {
#[allow(dead_code)]
pub fn new(output_rect: Rectangle<i32, Physical>) -> Self {
Frame {
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::GlesTexture;
use smithay::backend::renderer::ImportMem;
use smithay::utils::{Buffer as BufferCoord, Size};
use smithay::utils::Size;
use std::collections::HashMap;
use tracing::{info, warn};

View file

@ -48,7 +48,6 @@ impl WindowDmabufCache {
/// Returns Ok(true) if pixels were successfully updated.
#[allow(dead_code)]
pub fn update_pixels(&mut self, pixels: &[u8]) -> Result<()> {
use std::os::unix::io::AsRawFd;
use nix::unistd;
// 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).
#[allow(dead_code)]
pub fn reimport_cached_frame(
renderer: &mut GlesRenderer,
cached: &CachedDmabufFrame,
@ -277,8 +277,6 @@ pub fn export_texture_as_dmabuf(
// For now, we'll read pixels and create a DMABUF from them
// A more optimal approach would use EGLImage to back the DMABUF, but
// 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
// 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")

View file

@ -54,6 +54,7 @@ pub const MAGIC_SCREENCOPY_EVENT: u32 = 0x574F5345; // "WOSE" — screencopy cap
#[repr(C, packed)]
#[derive(Clone, Copy, Debug)]
#[allow(dead_code)]
pub struct PlaneInfoWire {
pub offset: u32,
pub stride: u32,
@ -329,7 +330,6 @@ impl ElectronClientConnection {
// For each plane: fd(4) + offset(8) + stride(4) + modifier(8)
// Note: We'll collect actual plane data from dmabuf handles iterator
let mut plane_idx = 0;
for _handle in dmabuf.handles() {
// fd: will be sent via ancillary data, so we put 0 as placeholder
buf[off..off + 4].copy_from_slice(&0u32.to_le_bytes());
@ -345,7 +345,6 @@ impl ElectronClientConnection {
let modifier_val: u64 = format.modifier.into();
buf[off..off + 8].copy_from_slice(&modifier_val.to_le_bytes());
off += 8;
plane_idx += 1;
}
// Send metadata with file descriptors as ancillary data
@ -600,25 +599,39 @@ impl ElectronProcess {
.env("WO_IPC_SOCKET", ipc_socket)
.env("WO_WINDOW_CONFIG", &serialized)
.env("WO_DRM_RENDER_NODE", render_node)
// Headless Ozone backend: Electron is used purely for offscreen
// rendering — it must not try to connect to a Wayland or X11
// display server (there is none for it to connect to; *we* are
// the compositor). The headless platform lets Chromium's OSR
// paint pipeline work while the native wo_dmabuf module handles
// GPU buffer allocation via its own GBM/EGL context on the
// render node.
.arg("--ozone-platform=headless")
// Wayland Ozone platform: Electron connects to our own Wayland
// compositor (WAYLAND_DISPLAY is already set in the env) for
// GPU/EGL initialisation. This gives Chromium a proper
// wayland-egl display so hardware-accelerated rasterisation
// works. The BrowserWindow still uses `offscreen: true`, so
// no Wayland surfaces are mapped — the connection is used
// solely for the EGL context.
//
// 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("--disable-gpu-sandbox")
// Use the *software* GL implementation inside Chromium for its
// own compositing (we do not need hardware-accelerated web
// content — the paint callback gives us BGRA bitmaps that the
// native module imports into a GBM BO). This avoids Chromium
// fighting with the compositor over the DRM render node.
.arg("--use-gl=swiftshader")
// Make sure OSR (offscreen) rendering actually produces
// paint callbacks at the requested frame rate.
.arg("--enable-features=Vulkan")
// Run GPU code in the main Electron process to avoid the
// separate GPU subprocess which adds IPC overhead and can
// fail to initialise independently.
.arg("--in-process-gpu")
// Use native EGL (provided by wayland-egl through the Ozone
// Wayland platform) for Chromium's compositor.
.arg("--use-gl=egl")
// Disable GPU vsync — we are rendering offscreen, there is
// 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())
.stdout(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) {
Ok(n) if n == buf.len() => Ok(()),
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}")),
};
}
@ -1280,7 +1293,7 @@ fn write_nonblocking(fd: RawFd, buf: &[u8]) -> Result<()> {
"IPC partial non-blocking write: {n}/{} bytes sent despite space check",
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}")),
}
}

View file

@ -436,7 +436,7 @@ impl WoState {
let pos: Point<f64, Logical> = self.pointer_location;
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() {
x11.wl_surface()
} else {

View file

@ -3,7 +3,6 @@
/// Implements `zwlr_screencopy_manager_v1` allowing clients like grim, OBS, etc.
/// to request frame copies of outputs or output regions.
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tracing::{debug, info, warn};
use wayland_protocols_wlr::screencopy::v1::server::{
@ -12,7 +11,7 @@ use wayland_protocols_wlr::screencopy::v1::server::{
};
use wayland_server::{
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;

View file

@ -50,6 +50,7 @@ impl XdgShellHandler for WoState {
self.ssd_windows.remove(&oid);
#[allow(deprecated)]
let window = Window::new(surface);
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))
.cloned();
if let Some(window) = window {
if let Some(_window) = window {
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);

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 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)))) {
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_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)))) {
warn!("XWayland: unfullscreen configure failed: {e}");
}

View file

@ -15,7 +15,7 @@ use gbm::Modifier;
use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
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::{
backend::{
@ -770,8 +770,6 @@ fn render_frame(data: &mut EventLoopData) {
use smithay::reexports::wayland_server::Resource;
use smithay::wayland::compositor::{with_states, Damage, SurfaceAttributes};
use smithay::wayland::shm::with_buffer_contents;
use std::os::unix::io::AsRawFd;
use nix::sys::memfd::MFdFlags;
use tracing::debug;
let pid = std::process::id();

View file

@ -5,7 +5,6 @@
use anyhow::{Context, Result};
use drm_fourcc::DrmFourcc;
use nix::sys::memfd::MFdFlags;
use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
use smithay::{
backend::{
@ -501,6 +500,12 @@ pub fn run_nested(config: Config) -> Result<()> {
};
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;
}
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);
}
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.
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.
pub fn forward_input_to_electron<B: smithay::backend::input::InputBackend>(
&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
// This is a no-op wrapper that exists for API compatibility with nested.rs

File diff suppressed because it is too large Load diff