mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
hostx: the compositor gets its own handle per pool sent, not the X server's descriptor number
Bug 3 (the xterm not presenting until clicked off and back on). The page's X
server closes a wl_shm pool's descriptor as soon as it has sent it, and its
next pool gets the same number. The compositor was handed the number and
located each buffer at commit time, so a buffer of an older pool resolved to
whichever pool held the number now. Typing into the xterm reuses its two
buffers; with both pools on one number the window showed the frame before
the one drawn, until something made a fresh pool. A cursor pool on the reused
number was too small for the frame, and the commit was dropped. hostx now
mints a handle per descriptor sent, as SCM_RIGHTS gives the receiver its own
descriptor. Guest Wayland clients (foot, Wine, Unity) pass real kernel
descriptors and never took this path.
The leg (a default leg of tests/demo/desktop.mjs, also step {"truth"}) clicks
into the xterm, types at a person's pace, moves the pointer across the window
edge, waits, then compares the X server's copy of the window (xwd through
tests/demo/guest/xsig.py: per-cell dark-pixel counts and a hash of the whole
dark bitmap) with the compositor's output. On edger, without the fix: 0 of 5
rounds equal at dpr 1 (47 dark pixels off, the last character and the
cursor), 0 of 3 at dpr 2 (228). With it: 5 of 5 and 3 of 3 exact.
The layers were measured with ?hostxcount=1 (kept): once a second, the bytes
the X server read from and wrote to each client, its bytes to the
compositor, and its scenes published. With ?wltrace=1 that showed two cursor
pools at one address and xterm buffers located to nothing. Damage and the
present path were ruled out first: forcing full uploads changed nothing, and
every commit changed the composed picture.
Known gap, in docs/gexcc.md §9.2: pool memory is never freed (as before).
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
c69c4e7162
commit
2dbb607943
5 files changed
+266
-15
No files matched your search
+24
-7
@@ -1750,12 +1750,29 @@ the apt overlay registers the whole pool (56 MB) as lazy files under
|
||||
`epoll_wait`s do not share wakeups. Real sharing needs the set in the global
|
||||
kernel, keyed by open file description; it comes with W's kernel-locking
|
||||
step 2 (per-object wait queues in the global kernel, docs/kernel-locking.md).
|
||||
- **xterm not presenting until refocused: open.** With no input, the
|
||||
compositor draws one frame per line the xterm prints, and the page's canvas
|
||||
holds each of them; `tests/demo/desktop.mjs`'s presentation leg checks
|
||||
that by drawing the canvas into a 2D canvas. Headless Chromium's
|
||||
screenshots never contain a worker's WebGPU canvas, so what a person's
|
||||
browser displays is not covered by any test here.
|
||||
- **xterm showing the frame before the one it drew.** The X server in the
|
||||
page closes a pool's descriptor as soon as it has sent it to the
|
||||
compositor, and its next pool gets the same number. The compositor was
|
||||
handed the number, so a buffer of an older pool was located, at its next
|
||||
commit, in whichever pool held the number now. Typing into the xterm
|
||||
reuses its two buffers, which then shared one pool's memory: the window
|
||||
showed the frame before the last until something made a fresh pool
|
||||
(clicking off and back on did). A cursor pool on the reused number made the
|
||||
commit fail its size check and be dropped. `web/src/hostx.js` now gives the
|
||||
compositor its own handle per descriptor sent, as `SCM_RIGHTS` would.
|
||||
The leg types into the xterm, waits, and compares the X server's copy of
|
||||
the window (`xwd`, through `tests/demo/guest/xsig.py`) with the
|
||||
compositor's output. Before the fix every round differed by the last
|
||||
character and the cursor: 47 dark pixels at dpr 1, 228 at dpr 2. After
|
||||
the fix every round matches exactly. `?hostxcount=1` logs, once a second,
|
||||
the bytes the X server took from and sent to each client, its bytes to the
|
||||
compositor and its scenes published.
|
||||
- **Known gap: pool memory.** A pool's pixels are never freed: the
|
||||
compositor may still read them after the X server closes the descriptor.
|
||||
Xwayland drops a buffer after a second unused and makes a new one, so an
|
||||
xterm printing a line every 1.3 s allocates about 0.9 MB per line.
|
||||
Freeing needs the mapping's lifetime (`gex_shm_unmap` in
|
||||
`web/xwayland/gexos.c` is a no-op) and a release from the compositor.
|
||||
|
||||
Legs: `node tests/demo/desktop.mjs web/dist-demo` (the desktop, then the
|
||||
cursor and presentation legs; `DEMO_DPR=2` for the device-pixel output).
|
||||
cursor, presentation and xterm-contents legs; `DEMO_DPR=2` for the device-pixel output).
|
||||
+133
-2
@@ -20,6 +20,9 @@
|
||||
// {"move": [X, Y]} the pointer to CSS pixel X, Y
|
||||
// {"click": [X, Y]} a left click there
|
||||
// {"keys": ["Escape", ...]} named keys (KeyboardEvent.code)
|
||||
// {"truth": true, "rounds": N} type into the xterm, then compare the X server's
|
||||
// copy of it (xwd) with the compositor's output
|
||||
// (needs the python and xeyes overlays)
|
||||
// A run step with "show": true prints the command's output tail even on success.
|
||||
// Env: CHROME, DEMO_WALL (s, default 300), DEMO_WEBGPU=sw (SwiftShader),
|
||||
// DEMO_QUERY (appended to the URL), DEMO_PROFILE (a profile dir to keep),
|
||||
@@ -105,7 +108,7 @@ const consoleLines = [];
|
||||
b.on('Runtime.consoleAPICalled', (p) => {
|
||||
const line = p.args.map((a) => a.value ?? a.description ?? '').join(' ');
|
||||
consoleLines.push(line);
|
||||
if (/\[desktop\]|error|Error/.test(line) && p.type !== 'debug') log(`console: ${line.slice(0, 300)}`);
|
||||
if (/\[desktop\]|\[hostx-count\]|error|Error/.test(line) && p.type !== 'debug') log(`console: ${line.slice(0, 300)}`);
|
||||
});
|
||||
b.on('Runtime.exceptionThrown', (p) => log(`exception: ${p.exceptionDetails?.exception?.description?.slice(0, 400)}`));
|
||||
await b.send('Runtime.enable', {}, s);
|
||||
@@ -196,6 +199,126 @@ async function canvasHash() {
|
||||
return h + ':' + lit;
|
||||
})()`);
|
||||
}
|
||||
/** One key into the focused window, as the page gets it from a person. */
|
||||
async function typeKey(ch) {
|
||||
if (ch === '\n') {
|
||||
const k = { key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13 };
|
||||
await b.send('Input.dispatchKeyEvent', { type: 'keyDown', ...k, text: '\r' }, s);
|
||||
await b.send('Input.dispatchKeyEvent', { type: 'keyUp', ...k }, s);
|
||||
return;
|
||||
}
|
||||
const [code, shift] = keyCode(ch);
|
||||
const m = shift ? 8 : 0;
|
||||
await b.send('Input.dispatchKeyEvent', { type: 'keyDown', key: ch, text: ch, code, modifiers: m }, s);
|
||||
await b.send('Input.dispatchKeyEvent', { type: 'keyUp', key: ch, code, modifiers: m }, s);
|
||||
}
|
||||
async function click(x, y) {
|
||||
await b.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x, y }, s);
|
||||
await b.send('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 }, s);
|
||||
await b.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 }, s);
|
||||
}
|
||||
/** A command in pid 1's session (not the xterm's); its output's lines. */
|
||||
async function guestRun(cmd, wall = 120000) {
|
||||
const tag = `g${Math.floor(Math.random() * 1e9)}`;
|
||||
await ev(`globalThis.gexDesktop.run(${JSON.stringify(`{ ${cmd} ; } >/tmp/${tag}.out 2>&1; echo "${tag} rc=$?"; cat /tmp/${tag}.out; echo "${tag} end"`)})`);
|
||||
await until(`globalThis.gexTerminalText().includes(${JSON.stringify(`${tag} end`)})`, cmd.slice(0, 40), wall);
|
||||
const t = (await ev('globalThis.gexTerminalText()')) || '';
|
||||
const body = t.slice(t.search(new RegExp(`${tag} rc=`)));
|
||||
const rc = Number(/rc=(\d+)/.exec(body)[1]);
|
||||
return { rc, lines: body.split('\n').slice(1).filter((l) => !l.includes(`${tag} end`)) };
|
||||
}
|
||||
const SIG_COLS = 16, SIG_ROWS = 8, SIG_EDGE = 2;
|
||||
/** The same signature as tests/demo/guest/xsig.py, of what the compositor
|
||||
* composed where the window is. */
|
||||
async function composedSig(x, y, w, h) {
|
||||
const r = await ev('globalThis.__gexCompositor.readoutBase64()');
|
||||
const px = Buffer.from(r.b64, 'base64');
|
||||
const cells = new Array(SIG_COLS * SIG_ROWS).fill(0);
|
||||
let exact = 2166136261;
|
||||
for (let j = 0; j < h; j++) {
|
||||
const cy = Math.floor(j * SIG_ROWS / h) * SIG_COLS;
|
||||
for (let i = 0; i < w; i++) {
|
||||
const o = ((y + j) * r.w + (x + i)) * 4;
|
||||
const dark = Math.max(px[o], px[o + 1], px[o + 2]) < 128 ? 1 : 0;
|
||||
if (dark) cells[cy + Math.floor(i * SIG_COLS / w)]++;
|
||||
exact = Math.imul(exact ^ dark, 16777619) >>> 0;
|
||||
}
|
||||
}
|
||||
cells.exact = exact;
|
||||
return cells;
|
||||
}
|
||||
/**
|
||||
* Bug 3 as a leg: what the xterm drew is what the page shows. Type into the
|
||||
* focused xterm at a person's pace -- each key a frame, so Xwayland draws into
|
||||
* the buffers it already has rather than making new ones -- then leave it
|
||||
* alone, and compare the X server's own copy of the window (xwd, in the guest)
|
||||
* with the compositor's output where the window is.
|
||||
*/
|
||||
async function truth(step) {
|
||||
const script = (await import('node:fs')).readFileSync(new URL('./guest/xsig.py', import.meta.url)).toString('base64');
|
||||
const put = await guestRun(`echo ${script} | base64 -d > /tmp/xsig.py`);
|
||||
if (put.rc) throw new Error('could not write /tmp/xsig.py');
|
||||
const p = await places();
|
||||
await click(...p.win);
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
let bad = 0;
|
||||
const rounds = step.rounds ?? 4;
|
||||
for (let n = 0; n < rounds; n++) {
|
||||
for (const ch of `echo round ${n} the quick brown fox ${'x'.repeat(n * 3)}\n`) {
|
||||
await typeKey(ch);
|
||||
await new Promise((r) => setTimeout(r, step.pace ?? 60));
|
||||
}
|
||||
// The pointer across the window's edge and back: the X server makes a
|
||||
// cursor pool for each shape it shows, between the xterm's frames.
|
||||
for (let i = 0; i < (step.wiggle ?? 3); i++) {
|
||||
await b.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: p.desk[0], y: p.desk[1] }, s);
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
await b.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: p.win[0], y: p.win[1] }, s);
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
await typeKey(String.fromCharCode(97 + i));
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, step.settle ?? 2000));
|
||||
const d = await ev('globalThis.__gexCompositor.describe()');
|
||||
const line = d.list.find((l) => / role=1 /.test(l));
|
||||
const [, w, h, x, y] = /(\d+)x(\d+)@(-?\d+),(-?\d+)/.exec(line).map(Number);
|
||||
const title = d.titles[0];
|
||||
const got = await guestRun(`python3.13 /tmp/xsig.py ${JSON.stringify(title)}`);
|
||||
const size = got.lines.find((l) => l.startsWith('xsize'));
|
||||
if (got.rc || !size) throw new Error(`xsig: rc=${got.rc} ${got.lines.slice(0, 4).join(' | ')}`);
|
||||
const drawn = [];
|
||||
for (let r = 0; r < SIG_ROWS; r++) {
|
||||
const l = got.lines.find((v) => v.startsWith(`xs${r} `));
|
||||
drawn.push(...l.slice(l.indexOf(' ') + 1).split(',').map(Number));
|
||||
}
|
||||
// The X window inside the surface: Xwayland's buffer carries the
|
||||
// window's border, which xwd leaves out -- so the best of a few offsets.
|
||||
const [xw, xh, xexact] = size.split(' ').slice(1).map(Number);
|
||||
let best = null;
|
||||
for (const [dx, dy] of [[0, 0], [1, 1], [-1, -1], [1, 0], [0, 1], [2, 2]]) {
|
||||
const sig = await composedSig(x + dx + SIG_EDGE, y + dy + SIG_EDGE, xw - 2 * SIG_EDGE, xh - 2 * SIG_EDGE);
|
||||
const d = drawn.reduce((a, v, i) => a + Math.abs(v - sig[i]), 0) + (sig.exact === xexact ? 0 : 0.5);
|
||||
if (!best || d < best.d) best = { d, sig, at: `${dx},${dy}` };
|
||||
if (!d) break;
|
||||
}
|
||||
const shown = best.sig;
|
||||
const diff = best.d;
|
||||
const cells = drawn.filter((v, i) => v !== shown[i]).length;
|
||||
// Equal counts and an equal bitmap: a cursor block one character along
|
||||
// stays in its cell, and only the bitmap tells.
|
||||
const ok = cells === 0 && shown.exact === xexact;
|
||||
if (!ok) bad++;
|
||||
if (!ok && process.env.DEMO_SIG) {
|
||||
for (let r = 0; r < SIG_ROWS; r++) {
|
||||
log(` X ${drawn.slice(r * SIG_COLS, (r + 1) * SIG_COLS).join(',')}`);
|
||||
log(` C ${shown.slice(r * SIG_COLS, (r + 1) * SIG_COLS).join(',')}`);
|
||||
}
|
||||
}
|
||||
log(` round ${n}: X window ${size.split(' ').slice(1).join('x')} vs composed ${w}x${h}@${x},${y}: `
|
||||
+ `${ok ? 'same' : `${cells} of ${drawn.length} cells differ (${Math.floor(diff)} dark pixels), bitmaps ${shown.exact === xexact ? 'equal' : 'differ'}`} at +${best.at} (serial ${/serial=(\d+)/.exec(line)[1]})`);
|
||||
}
|
||||
if (bad) failed = true;
|
||||
log(`${bad ? 'FAIL' : 'ok'} xterm shows what it drew: ${rounds - bad} of ${rounds} rounds`);
|
||||
}
|
||||
async function legs() {
|
||||
const p = await places();
|
||||
if (!p) throw new Error('no window to point at');
|
||||
@@ -257,6 +380,11 @@ async function legs() {
|
||||
if (!okShown) failed = true;
|
||||
log(`${okShown ? 'ok' : 'FAIL'} presentation leg: the page's canvas changed ${changes} times in 4 s of the xterm printing with no input`
|
||||
+ ` (compositor: ${d1.frames - d0.frames} frames drawn in ${d1.turns - d0.turns} turns; screenshots changed ${shots} times)`);
|
||||
// What the xterm drew is what the page shows (bug 3): xwd and python come
|
||||
// as overlays, the way a person adds them from the launcher.
|
||||
await new Promise((r) => setTimeout(r, 3000));
|
||||
for (const id of ['python', 'xeyes']) await ev(`globalThis.gexDesktop.install(${JSON.stringify(id)})`);
|
||||
await truth({ rounds: 3 });
|
||||
}
|
||||
function dump() {
|
||||
ev('globalThis.gexTerminalText?.()').then((t) => {
|
||||
@@ -281,7 +409,8 @@ try {
|
||||
if (process.env.DEMO_SHOT) await shot(process.env.DEMO_SHOT, 1500);
|
||||
// The legs every run has (DEMO_LEGS=0 skips them): the xterm's cursor is
|
||||
// the page's CSS cursor and none is composited; what the xterm prints with
|
||||
// no input reaches the screen (the page's own screenshot, not a readback).
|
||||
// no input reaches the page's canvas; what it draws as it is typed into is
|
||||
// what the compositor shows (the X server's copy against the output).
|
||||
if (process.env.DEMO_LEGS !== '0') await legs();
|
||||
let n = 0;
|
||||
let lastHash = null;
|
||||
@@ -337,6 +466,8 @@ try {
|
||||
const ok = kind === step.cursor;
|
||||
if (!ok) failed = true;
|
||||
log(`${ok ? 'ok' : 'FAIL'} cursor ${JSON.stringify(css.slice(0, 80))} (want ${step.cursor})`);
|
||||
} else if (step.truth) {
|
||||
await truth(step);
|
||||
} else if (step.keys) {
|
||||
await ev("document.getElementById('out').focus()");
|
||||
for (const code of step.keys) {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# What an X window holds, from the X server: `xwd` of the window named argv[1],
|
||||
# reduced to a signature tests/demo/desktop.mjs computes the same way from what
|
||||
# the compositor composed -- per cell of a COLSxROWS grid, how many pixels are
|
||||
# dark. Prints "xsize W H HASH" (the hash of the whole dark bitmap), then each row of cells as "xsR n,n,...".
|
||||
import struct, subprocess, sys
|
||||
|
||||
COLS, ROWS = 16, 8
|
||||
title = sys.argv[1]
|
||||
d = subprocess.run(['xwd', '-name', title, '-silent', '-nobdrs'], capture_output=True, check=True).stdout
|
||||
h = struct.unpack('>25I', d[:100])
|
||||
(size, _ver, _fmt, _depth, w, ht, _xoff, order, _unit, _bo, _pad, bpp, bpl,
|
||||
_cls, rm, gm, bm, _bits, _ents, ncolors) = h[:20]
|
||||
if bpp != 32:
|
||||
sys.exit(f'xsig: {bpp} bits per pixel')
|
||||
px = d[size + ncolors * 12:]
|
||||
end = '<' if order == 0 else '>'
|
||||
shift = lambda m: (m & -m).bit_length() - 1
|
||||
rs, gs, bs = shift(rm), shift(gm), shift(bm)
|
||||
# The outermost EDGE pixels are left out on both sides: where the window's
|
||||
# border meets its contents is the compositor's business, not the client's.
|
||||
EDGE = 2
|
||||
iw, ih = w - 2 * EDGE, ht - 2 * EDGE
|
||||
cells = [0] * (COLS * ROWS)
|
||||
exact = 2166136261 # FNV-1a over the dark bitmap, row by row
|
||||
for y in range(ih):
|
||||
row = struct.unpack_from(f'{end}{w}I', px, (y + EDGE) * bpl)
|
||||
cy = y * ROWS // ih * COLS
|
||||
for x in range(iw):
|
||||
v = row[x + EDGE]
|
||||
dark = max((v & rm) >> rs, (v & gm) >> gs, (v & bm) >> bs) < 128
|
||||
if dark:
|
||||
cells[cy + x * COLS // iw] += 1
|
||||
exact = ((exact ^ dark) * 16777619) & 0xffffffff
|
||||
# A row of cells a line: the terminal that carries this back wraps long ones.
|
||||
print('xsize', w, ht, exact)
|
||||
for r in range(ROWS):
|
||||
print(f'xs{r}', ','.join(map(str, cells[r * COLS:(r + 1) * COLS])))
|
||||
+65
-4
@@ -95,6 +95,24 @@ export class HostX {
|
||||
#fds = new Map();
|
||||
/** Pools of pixels, by descriptor: where they are in the server's memory. */
|
||||
#pools = new Map();
|
||||
/**
|
||||
* The pools the compositor holds, by its own handle for each: what a
|
||||
* descriptor sent on the Wayland connection became when it arrived.
|
||||
*
|
||||
* A descriptor is the server's name for a pool only until the server closes
|
||||
* it, and Xwayland closes a pool's descriptor as soon as it has sent it --
|
||||
* then the next pool it makes gets the same number. Handing the compositor
|
||||
* the number made every buffer of the old pool resolve, at its next commit,
|
||||
* to whichever pool had the number now. Typing into the xterm reuses its
|
||||
* two buffers, and with both pools on one number both showed the second's
|
||||
* memory: the window was a frame behind what it drew until something made
|
||||
* a fresh pool (clicking off it and back on did). A cursor pool on the
|
||||
* number was too small to hold the frame, and the commit was dropped. Over
|
||||
* a socket `SCM_RIGHTS` gives the receiver its own descriptor for the same
|
||||
* memory, and this is that: a handle per descriptor sent, never reused.
|
||||
*/
|
||||
#held = new Map();
|
||||
#nextHeld = 1;
|
||||
/**
|
||||
* Descriptors owed to the server on its Wayland connection.
|
||||
*
|
||||
@@ -117,10 +135,17 @@ export class HostX {
|
||||
#trace = 0;
|
||||
/** Publishes so far, for the note in `#tick`. */
|
||||
#published = 0;
|
||||
/**
|
||||
* `?hostxcount=1`: once a second, what moved through the server -- bytes in
|
||||
* from and out to each X client, bytes to the compositor, scenes published.
|
||||
* Where a picture stops between a client drawing and the page composing it.
|
||||
*/
|
||||
#count = null;
|
||||
|
||||
async start(msg) {
|
||||
this.#channel = msg.wlChannel;
|
||||
this.#trace = msg.trace | 0;
|
||||
if (msg.count) this.#count = { at: Date.now(), n: new Map(), polls: 0 };
|
||||
this.#token = msg.token;
|
||||
const size = outputSize(msg.wlChannel);
|
||||
this.#out = { w: size.w, h: size.h };
|
||||
@@ -333,7 +358,7 @@ export class HostX {
|
||||
|
||||
/** Where the pool this descriptor names lives, for the compositor. */
|
||||
#locate(fd, out) {
|
||||
const pool = this.#pools.get(fd);
|
||||
const pool = this.#held.get(fd);
|
||||
this.#hwView();
|
||||
const w = this.#hwWords;
|
||||
if (!pool) {
|
||||
@@ -342,7 +367,7 @@ export class HostX {
|
||||
// healthy -- the surface is there, the commits arrive, the scene is
|
||||
// published with an address of zero.
|
||||
this.#once(`pool:${fd}`, `[hostx] descriptor ${fd} is not a pool`
|
||||
+ ` (pools: ${[...this.#pools.keys()].join(',') || 'none'})`);
|
||||
+ ` (held: ${this.#held.size})`);
|
||||
w[out >> 2] = 0;
|
||||
w[(out >> 2) + 1] = 0;
|
||||
return 0;
|
||||
@@ -399,6 +424,9 @@ export class HostX {
|
||||
// reading them. So the allocation stays -- freeing it here is the one
|
||||
// way to make an X window's contents vanish mid-frame.
|
||||
self.#fds.delete(fd);
|
||||
// The server's name for a pool goes with the descriptor; the compositor
|
||||
// has its own (`#held`), and the memory stays (see above).
|
||||
self.#pools.delete(fd);
|
||||
try {
|
||||
self.#x.FS.close(self.#x.FS.getStream(fd));
|
||||
} catch {
|
||||
@@ -442,7 +470,12 @@ export class HostX {
|
||||
send(fd, buf, len, fds, nfds) {
|
||||
const ep = self.#fds.get(fd);
|
||||
if (!ep) return -EBADF;
|
||||
if (ep.kind === 'kernel') return self.#k.k_sock_write(ep.id, buf, len);
|
||||
if (ep.kind === 'kernel') {
|
||||
const r = self.#k.k_sock_write(ep.id, buf, len);
|
||||
if (self.#count && r > 0) self.#bump(`out${fd}`, r);
|
||||
return r;
|
||||
}
|
||||
if (self.#count) self.#bump(ep.kind, len);
|
||||
if (ep.kind === 'wl' || ep.kind === 'wm') {
|
||||
// Into the compositor module's memory, which is a different address
|
||||
// space: a protocol message, never a frame.
|
||||
@@ -450,7 +483,17 @@ export class HostX {
|
||||
const p = self.#hw.hw_scratch(pad + nfds * 4);
|
||||
self.#hwView().set(self.#x.HEAPU8.subarray(buf, buf + len), p);
|
||||
for (let i = 0; i < nfds; i++) {
|
||||
self.#hwWords[((p + pad) >> 2) + i] = self.#x.HEAP32[(fds >> 2) + i];
|
||||
// The compositor's own reference to the pool (see `#held`), not
|
||||
// the server's descriptor number, which the server is about to
|
||||
// close and reuse.
|
||||
const sent = self.#x.HEAP32[(fds >> 2) + i];
|
||||
const pool = ep.kind === 'wl' ? self.#pools.get(sent) : null;
|
||||
let word = sent;
|
||||
if (pool) {
|
||||
word = self.#nextHeld++;
|
||||
self.#held.set(word, pool);
|
||||
}
|
||||
self.#hwWords[((p + pad) >> 2) + i] = word;
|
||||
}
|
||||
if (nfds && self.#trace) {
|
||||
const got = [];
|
||||
@@ -472,6 +515,7 @@ export class HostX {
|
||||
if (nfds) self.#x.HEAP32[nfds >> 2] = 0;
|
||||
if (ep.kind === 'kernel') {
|
||||
const n = self.#k.k_sock_read(ep.id, buf, len);
|
||||
if (self.#count && n > 0) self.#bump(`in${fd}`, n);
|
||||
return n;
|
||||
}
|
||||
if (ep.kind === 'wl' || ep.kind === 'wm') {
|
||||
@@ -583,11 +627,28 @@ export class HostX {
|
||||
this.log(`[hostx] keymap event: ${n} bytes as fd ${fd}`);
|
||||
}
|
||||
|
||||
/** `?hostxcount=1`: `n` more bytes under `key` this second. */
|
||||
#bump(key, n) {
|
||||
const m = this.#count.n;
|
||||
m.set(key, (m.get(key) ?? 0) + n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Between two turns of the server's loop: run the window manager, take the
|
||||
* page's input, publish the scene.
|
||||
*/
|
||||
#tick() {
|
||||
if (this.#count) {
|
||||
const c = this.#count;
|
||||
c.polls++;
|
||||
if (Date.now() - c.at >= 1000) {
|
||||
const parts = [...c.n].map(([k, v]) => `${k}=${v}`).join(' ');
|
||||
this.log(`[hostx-count] turns=${c.polls} pub=${this.#published} ${parts}`);
|
||||
c.n.clear();
|
||||
c.polls = 0;
|
||||
c.at = Date.now();
|
||||
}
|
||||
}
|
||||
// A hook for a harness: this is the only turn anything else gets once the
|
||||
// server's `main` has the thread. See web/test/hostx.mjs.
|
||||
globalThis.GEX_HOSTX_TICK?.();
|
||||
|
||||
+7
-2
@@ -796,10 +796,14 @@ async function runProgramHere(
|
||||
// global kernel that only exists once the server has bound it.
|
||||
let listening = null;
|
||||
const up = new Promise((resolve) => (listening = resolve));
|
||||
const countX = !!flag('hostxcount');
|
||||
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);
|
||||
if (m.kind === 'log') {
|
||||
term.writeln(m.text);
|
||||
// `?hostxcount=1`: to the console too, timed, for a harness.
|
||||
if (countX) console.log(`${(performance.now() / 1000).toFixed(2)} ${m.text}`);
|
||||
} else if (m.kind === 'wl-memory') compositor?.addMemory(m.token, m.memory);
|
||||
else if (m.kind === 'ready') listening(true);
|
||||
else if (m.kind === 'failed') {
|
||||
term.writeln(`gex: the X server failed: ${m.why}`);
|
||||
@@ -824,6 +828,7 @@ async function runProgramHere(
|
||||
verbose: globalThis.GEX_HOSTX_VERBOSE || Number(new URLSearchParams(location.search).get('xverbose')) || undefined,
|
||||
trace: globalThis.GEX_HOSTX_TRACE || undefined,
|
||||
wlTrace: flag('wltrace') ? 1 : undefined,
|
||||
count: flag('hostxcount') ? 1 : undefined,
|
||||
});
|
||||
// Two modules and a kernel to instantiate; a slow machine takes a few
|
||||
// seconds. A run that goes ahead without the display is worse than one that
|
||||
|
||||
Reference in new issue
Block a user