mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
page: input the compositor puts in a process's ring wakes its parked Workers
With every thread of a process parked on the global kernel's wake word,
nothing drained the wl input ring until a park's 20 ms bound ran out: every
click and key on mspaint could wait that long before the compositor pump
(on a scheduler turn) delivered it. The process's WlProxy now writes the
wake word's address into its arena block, the kernel memory travels with
the wl-memory message, and sendEvent bumps and notifies the word. The Worker
that wakes takes a scheduler turn, whose pump delivers the event and (since
b638c279) wakes the client's reader thread.
Leg: shims/browser/js/check-wl-input-wake.mjs, a Worker parked on the wake
word for 5 s returns 1.9-2.9 ms after the event; without the wake it sleeps
out its bound.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
This commit is contained in:
1 parent
b638c27940
commit
1c658a5483
10 files changed
+140
-10
No files matched your search
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Does page input reach a process whose every thread is parked, well before
|
||||
* the park's bound?
|
||||
*
|
||||
* A Wayland client's input goes page -> compositor -> the process's ring in
|
||||
* the wl arena (`sendEvent`) -> the shim's compositor pump, which runs on a
|
||||
* scheduler turn of one of the process's Workers. With every Worker parked on
|
||||
* the global kernel's wake word (`gex_park`, 20 ms bound), nothing drained the
|
||||
* ring until a bound ran out: up to 20 ms on every click and key, and the
|
||||
* park-late diag found Wine's Wayland reader thread waiting out its bound.
|
||||
* `sendEvent` now bumps the wake word the process's `WlProxy` names.
|
||||
*
|
||||
* This instantiates the kernel module in a second thread on shared memory and
|
||||
* parks it on the wake word for 5 s (`k_park`, as `gex_park` does); the first
|
||||
* thread claims a block as a guest would, then sends a pointer event 100 ms
|
||||
* later. The park must end, woken (0), within a few milliseconds of the event.
|
||||
*
|
||||
* Usage: node check-wl-input-wake.mjs [gex_kernel.wasm]
|
||||
*/
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Worker, isMainThread, workerData, parentPort } from 'node:worker_threads';
|
||||
import { makeWlChannel, WlProxy, sendEvent, setKernelMemory } from './wl-bridge.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..', '..');
|
||||
|
||||
async function instantiate(module, memory, process_) {
|
||||
const inst = await WebAssembly.instantiate(module, {
|
||||
env: { memory, process: process_, lazy_fetch: () => 0 },
|
||||
});
|
||||
return inst.exports;
|
||||
}
|
||||
|
||||
if (isMainThread) {
|
||||
const path = process.argv[2] || join(ROOT, 'web', 'public', 'gex', 'gex_kernel.wasm');
|
||||
const module = await WebAssembly.compile(await readFile(path));
|
||||
const memory = new WebAssembly.Memory({ initial: 2048, maximum: 65536, shared: true });
|
||||
const process_ = new WebAssembly.Memory({ initial: 1, maximum: 65535, shared: true });
|
||||
const k = await instantiate(module, memory, process_);
|
||||
|
||||
// The guest's half: a process claims its block (on its first publish).
|
||||
const channel = makeWlChannel();
|
||||
const proxy = new WlProxy(channel, () => {}, () => 7);
|
||||
proxy.wakeAddr = () => k.k_wake_addr();
|
||||
const guestMem = new WebAssembly.Memory({ initial: 1, maximum: 16, shared: true });
|
||||
proxy.publish(guestMem, 0, 0);
|
||||
if (!proxy.ok) {
|
||||
console.error('wl input wake check FAILED: the proxy claimed no block');
|
||||
process.exit(1);
|
||||
}
|
||||
// The compositor's half, in its own realm in the page: its own view of the
|
||||
// arena, and the kernel memory a process's `wl-memory` message carried.
|
||||
const compositorChannel = { arena: channel.arena };
|
||||
setKernelMemory(compositorChannel, memory);
|
||||
|
||||
const seq = k.k_wake_seq();
|
||||
const parked = new Promise((resolve, reject) => {
|
||||
const w = new Worker(fileURLToPath(import.meta.url), {
|
||||
workerData: { module, memory, process_, seq, ns: 5000 * 1e6 },
|
||||
});
|
||||
w.once('message', resolve);
|
||||
w.once('error', reject);
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
const sent = performance.timeOrigin + performance.now();
|
||||
// Block 0: the first block a lone process claims.
|
||||
sendEvent(compositorChannel, 0, 0 /* motion */, 10, 20, 1, 0, 1, 1);
|
||||
const r = await parked;
|
||||
const late = r.end - sent;
|
||||
if (r.r !== 0 || late > 10) {
|
||||
console.error(` the parked Worker returned ${r.r}, ${late.toFixed(1)} ms after the event (want 0, within 10 ms)`);
|
||||
console.error(`wl input wake check FAILED: ${path}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(` page input wakes a process whose threads are all parked (${late.toFixed(1)} ms after the event; the park's bound was 5 s)`);
|
||||
process.exit(0);
|
||||
} else {
|
||||
const { module, memory, process_, seq, ns } = workerData;
|
||||
const k = await instantiate(module, memory, process_);
|
||||
const r = k.k_park(seq, BigInt(ns));
|
||||
// `performance.now()` counts from each thread's own origin.
|
||||
parentPort.postMessage({ r, end: performance.timeOrigin + performance.now() });
|
||||
}
|
||||
@@ -1013,6 +1013,14 @@ export class Gex {
|
||||
|
||||
get memory() { return this.#memory; }
|
||||
|
||||
/** The global kernel's memory and its wake word's address, or null: what
|
||||
* a compositor bumps to wake this process's parked Workers. */
|
||||
kernelWake() {
|
||||
const k = this.#kernel;
|
||||
if (!k?.k_wake_addr || !this.#kernelSpec?.memory) return null;
|
||||
return { memory: this.#kernelSpec.memory, addr: k.k_wake_addr() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Every guest block PC translated in this process so far, as a BigUint64Array
|
||||
* (a fresh copy, safe to keep). The profile a sandbox run dumps to seed a
|
||||
|
||||
@@ -99,6 +99,9 @@ const B_HEARTBEAT = 1;
|
||||
const B_COUNT = 2;
|
||||
const B_GEN = 3;
|
||||
const B_ACK = 4;
|
||||
/** Where the global kernel's wake word is (`k_wake_addr`), or zero: what the
|
||||
* compositor bumps after putting an event in this block's ring. */
|
||||
const B_WAKE = 5;
|
||||
const B_WORDS = 8;
|
||||
const BLOCK_WORDS = B_WORDS + SLOTS * REC;
|
||||
|
||||
@@ -218,6 +221,11 @@ export function ackBlock(channel, block, gen) {
|
||||
Atomics.store(a, blockBase(block) + B_ACK, gen);
|
||||
}
|
||||
|
||||
/** The global kernel's memory, for `sendEvent`'s wake. One per page. */
|
||||
export function setKernelMemory(channel, memory) {
|
||||
if (memory && !channel.kernelMemory) channel.kernelMemory = memory;
|
||||
}
|
||||
|
||||
/** Write one event into a process's ring, for its guest to pick up. */
|
||||
export function sendEvent(channel, block, kind, a1 = 0, b1 = 0, c1 = 0, d1 = 0, client = 0, surface = 0) {
|
||||
const a = new Int32Array(channel.arena);
|
||||
@@ -233,6 +241,17 @@ export function sendEvent(channel, block, kind, a1 = 0, b1 = 0, c1 = 0, d1 = 0,
|
||||
a[slot + 6] = surface | 0;
|
||||
a[slot + 7] = 0;
|
||||
Atomics.store(a, base + K_HEAD, head + 1); // publish last
|
||||
// And a wake, as another guest's syscall would give: with every thread of
|
||||
// the process parked, nothing drains the ring until a park's bound (20 ms)
|
||||
// runs out. The Worker that wakes takes a scheduler turn, whose compositor
|
||||
// pump delivers the event and wakes the client's reader in turn.
|
||||
const wake = Atomics.load(a, blockBase(block) + B_WAKE);
|
||||
const k = channel.kernelMemory;
|
||||
if (wake && k && typeof SharedArrayBuffer === 'function' && k.buffer instanceof SharedArrayBuffer) {
|
||||
const w = new Int32Array(k.buffer);
|
||||
Atomics.add(w, wake >>> 2, 1);
|
||||
Atomics.notify(w, wake >>> 2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,6 +284,9 @@ export class WlProxy {
|
||||
#tokenFn;
|
||||
/** Called with the token once it is known, so the host can send the memory. */
|
||||
onToken = null;
|
||||
/** The global kernel's wake word address (`k_wake_addr`), or 0; asked when
|
||||
* the block is claimed. See `B_WAKE`. */
|
||||
wakeAddr = null;
|
||||
|
||||
/** Claim a block if this process does not have one yet. Returns whether it has. */
|
||||
#ready() {
|
||||
@@ -294,6 +316,7 @@ export class WlProxy {
|
||||
this.#block = b;
|
||||
this.#base = base;
|
||||
this.#ring = ringBase(b);
|
||||
this.#noteWake();
|
||||
return;
|
||||
}
|
||||
for (let pass = 0; pass < 2; pass++) {
|
||||
@@ -310,11 +333,17 @@ export class WlProxy {
|
||||
Atomics.store(this.#a, base + B_GEN, 0);
|
||||
Atomics.store(this.#a, base + B_ACK, 0);
|
||||
Atomics.store(this.#a, base + B_HEARTBEAT, now);
|
||||
this.#noteWake();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#noteWake() {
|
||||
const at = this.wakeAddr?.() | 0;
|
||||
if (at) Atomics.store(this.#a, this.#base + B_WAKE, at);
|
||||
}
|
||||
|
||||
get token() {
|
||||
return this.#token;
|
||||
}
|
||||
|
||||
@@ -488,9 +488,10 @@ async function handle(msg) {
|
||||
// The memory object, not its buffer: a shared memory that grows gives out a
|
||||
// new buffer, and the compositor has to be able to see the new length.
|
||||
gex.onWlToken = (token) => {
|
||||
port.post({ kind: 'wl-memory', token, memory: gex.memory });
|
||||
port.post({ kind: 'wl-memory', token, memory: gex.memory, kernel: gex.kernelWake()?.memory });
|
||||
};
|
||||
proxy.onToken = gex.onWlToken;
|
||||
proxy.wakeAddr = () => gex.kernelWake()?.addr ?? 0;
|
||||
} else if (msg.wlStub) {
|
||||
// A headless compositor for node: the shim's gex-wayland runs and binds
|
||||
// `wayland-0`, so a guest connects and inits exactly as in the browser, but
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
readScene,
|
||||
ackBlock,
|
||||
sendEvent,
|
||||
setKernelMemory,
|
||||
setOutputSize,
|
||||
ROLE_TOPLEVEL,
|
||||
ROLE_POPUP,
|
||||
@@ -472,8 +473,9 @@ class Compositor {
|
||||
* first window of a session appeared and the second never did, because its
|
||||
* buffer was "past the end of memory".
|
||||
*/
|
||||
addMemory(token, memory) {
|
||||
addMemory(token, memory, kernel) {
|
||||
this.#memories.set(token, { memory, view: new Uint8Array(memory.buffer) });
|
||||
setKernelMemory(this.#channel, kernel);
|
||||
}
|
||||
|
||||
/** The current view of a process's memory, re-derived if it has grown. */
|
||||
@@ -1498,7 +1500,7 @@ const onMessage = ({ data: m }) => {
|
||||
startGl(m, canvas).catch((e) =>
|
||||
post({ kind: 'gl-failed', error: e?.message || String(e) }));
|
||||
} else if (m.kind === 'memory') {
|
||||
comp?.addMemory(m.token, m.memory);
|
||||
comp?.addMemory(m.token, m.memory, m.kernel);
|
||||
} else if (m.kind === 'drop-memory') {
|
||||
comp?.dropMemory(m.token);
|
||||
// That process is gone: its GL context, its windows and their frames go
|
||||
|
||||
@@ -283,8 +283,8 @@ export class Compositor {
|
||||
}
|
||||
|
||||
/** A guest process's memory: where its surfaces' pixels are. */
|
||||
addMemory(token, memory) {
|
||||
this.#worker.postMessage({ kind: 'memory', token, memory });
|
||||
addMemory(token, memory, kernel) {
|
||||
this.#worker.postMessage({ kind: 'memory', token, memory, kernel });
|
||||
}
|
||||
|
||||
resize(width, height) {
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
readScene,
|
||||
ackBlock,
|
||||
sendEvent,
|
||||
setKernelMemory,
|
||||
setOutputSize,
|
||||
EV_MOTION,
|
||||
EV_BUTTON,
|
||||
@@ -390,8 +391,9 @@ export class DomDesktop {
|
||||
* megabytes of a process that has grown to hundreds. Every surface past
|
||||
* that point reads as zero -- a window that is simply blank, with nothing
|
||||
* to say for itself. */
|
||||
addMemory(token, memory) {
|
||||
addMemory(token, memory, kernel) {
|
||||
this.#memories.set(token, { memory, view: new Uint8Array(memory.buffer) });
|
||||
setKernelMemory(this.#channel, kernel);
|
||||
}
|
||||
|
||||
/** The current view of a process's memory, re-derived if it has grown. */
|
||||
|
||||
+4
-1
@@ -199,10 +199,13 @@ export class HostX {
|
||||
|
||||
// -- the arena ---------------------------------------------------------
|
||||
this.#proxy = new WlProxy(msg.wlChannel, () => this.emit({ kind: 'wl' }), () => this.#token);
|
||||
// The server waits on the global kernel's wake word (`wait` below), which
|
||||
// the compositor bumps when it puts input in this block's ring.
|
||||
this.#proxy.wakeAddr = () => this.#k?.k_wake_addr?.() ?? 0;
|
||||
// The compositor Worker reads an X window's pixels out of this memory, so
|
||||
// it needs the memory itself -- the object, not a buffer, since a shared
|
||||
// memory that grows hands out a new one.
|
||||
this.emit({ kind: 'wl-memory', token: this.#token, memory: this.#x.wasmMemory });
|
||||
this.emit({ kind: 'wl-memory', token: this.#token, memory: this.#x.wasmMemory, kernel: msg.kernel.memory });
|
||||
|
||||
// -- the transport the server will use ---------------------------------
|
||||
this.#x.gexos = this.#gexos();
|
||||
|
||||
+2
-2
@@ -759,7 +759,7 @@ async function runProgramHere(
|
||||
xServer.onmessage = (e) => {
|
||||
const m = e.data;
|
||||
if (m.kind === 'log') term.writeln(m.text);
|
||||
else if (m.kind === 'wl-memory') compositor?.addMemory(m.token, m.memory);
|
||||
else if (m.kind === 'wl-memory') compositor?.addMemory(m.token, m.memory, m.kernel);
|
||||
else if (m.kind === 'ready') listening(true);
|
||||
else if (m.kind === 'failed') {
|
||||
term.writeln(`gex: the X server failed: ${m.why}`);
|
||||
@@ -989,7 +989,7 @@ async function runProgramHere(
|
||||
if (m.kind === 'gl') glServe?.();
|
||||
// A process that has a compositor block hands the compositor its memory:
|
||||
// that is where the pixels of every one of its surfaces are.
|
||||
else if (m.kind === 'wl-memory') compositor?.addMemory(m.token, m.memory);
|
||||
else if (m.kind === 'wl-memory') compositor?.addMemory(m.token, m.memory, m.kernel);
|
||||
else if (m.kind === 'wl') {
|
||||
/* a publish happened; the compositor draws on its own clock */
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ async function openCompositor(width, height) {
|
||||
send,
|
||||
// What `runProgram` calls on a compositor.
|
||||
compositor: {
|
||||
addMemory: (token, memory) => send({ kind: 'memory', token, memory }),
|
||||
addMemory: (token, memory, kernel) => send({ kind: 'memory', token, memory, kernel }),
|
||||
stop: () => {
|
||||
clearInterval(timer);
|
||||
send({ kind: 'stop' });
|
||||
|
||||
Reference in new issue
Block a user