Further optimization passes
This commit is contained in:
parent
8b640d9f91
commit
ff54140d62
19 changed files with 1099 additions and 781 deletions
211
electron/dist/main.js
vendored
211
electron/dist/main.js
vendored
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
36
electron/types/types.d.ts
vendored
36
electron/types/types.d.ts
vendored
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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}")),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
1020
whatever.txt
1020
whatever.txt
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue