Render through a desynchronized WebGL context by default

Measured click-to-paint was 72-77 ms, of which the editor itself spends
under 4 ms; the rest is the finished frame waiting in the compositor
queue. A desynchronized context bypasses it (16-18 ms in the same probe).
Pixi has no option for it, so main.ts builds the WebGL2 context itself.
The mode may tear on some drivers; ?vsync falls back to Pixi's own
context.

__gm.lowLatency() reports the attributes the browser actually granted,
and a graph-step check pins that the mode stays on.

Also adds the todo note graph-node-collapse-width.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-11 11:54:19 +02:00
1 parent 13852fe4b2
commit 0dcb3b4432
5 files changed
+30 -2

No files matched your search

+5 -1
View File
@@ -549,6 +549,10 @@ selected and switched back on.
## Stack
- **PixiJS v8** – rendering and pointer events. No game engine; runtime systems live in `src/runtime/`.
`main.ts` creates the WebGL context itself with `desynchronized`, which Pixi has no option for:
frames skip the compositor queue, so the UI keeps up with the pointer (click to picture measured
72–77 ms before, 16–18 ms after). The price is possible tearing while things move fast;
`?vsync` falls back to Pixi's own context. A browser without the mode simply ignores it.
- **Own canvas widget toolkit** (`src/ui/`) – panels, buttons, lists, text/number/color fields,
popups. The only lasting DOM element is a hidden `<input>` used as a keyboard bridge (IME,
mobile keyboard, clipboard) while a canvas text field is focused; *Upload…* adds a short-lived
@@ -640,7 +644,7 @@ npm run check # typecheck + lint + format:check + tests, what CI would ru
# code is lost (tail's is reported), so a red run reads as green
npm run build # writes dist/ and regenerates index.html
export GM_REMOTE=user@host # optional: run the browser elsewhere; see below
node tools/seed-e2e.mjs && node tools/e2e.mjs # 534 editor checks, screenshots in /tmp/gm-e2e-*.png
node tools/seed-e2e.mjs && node tools/e2e.mjs # 535 editor checks, screenshots in /tmp/gm-e2e-*.png
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 – a subset while working on one step
./e2e.sh start [GM_STEPS] # the same run detached, see below
./e2e.sh status # live progress, or the last run's result
+3
View File
@@ -281,6 +281,9 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
};
},
fps: () => editor.fps,
lowLatency: () =>
(app.renderer as unknown as { gl?: WebGL2RenderingContext }).gl?.getContextAttributes()
?.desynchronized === true,
tab: () => editor.tab,
showTab: (tab: CentreTab) => editor.showTab(tab),
};
+16 -1
View File
@@ -6,7 +6,22 @@ import { startEditor } from './editor/App';
import { mountPlayground } from './playground';
import { initUI, Theme, VStack } from './ui';
// `desynchronized` bypasses the compositor queue (headless click-to-paint 72 → 16 ms) but may
// tear; `?vsync` opts out. Pixi has no option for it, hence our own context.
function lowLatencyContext(): { canvas?: HTMLCanvasElement; context?: WebGL2RenderingContext } {
const canvas = document.createElement('canvas');
const context = canvas.getContext('webgl2', {
alpha: false,
antialias: true,
premultipliedAlpha: true,
stencil: true,
desynchronized: true,
});
return context ? { canvas, context } : {};
}
async function main(): Promise<void> {
const params = new URLSearchParams(location.search);
const app = new Application();
await app.init({
resizeTo: window,
@@ -15,6 +30,7 @@ async function main(): Promise<void> {
resolution: Math.min(2, window.devicePixelRatio || 1),
autoDensity: true,
preference: 'webgl',
...(params.has('vsync') ? {} : lowLatencyContext()),
});
document.body.appendChild(app.canvas);
@@ -25,7 +41,6 @@ async function main(): Promise<void> {
app.stage.hitArea = app.screen;
initUI(app, root, overlay);
const params = new URLSearchParams(location.search);
if (params.has('playground')) mountPlayground(app, root);
else await startEditor(app, root);
+2
View File
@@ -0,0 +1,2 @@
man kann ja nun graph nodes zusammenklappen.
hier soll auch die breite veringert werden auf die titeltext breite wenn möglich?
+4
View File
@@ -253,6 +253,10 @@ export async function graph(t) {
// Graph while playing shows the live highlight: hold ArrowRight → the exec edge is hot.
const fpsEdit = await evaluate('__gm.fps()');
check('fps readout is measured in edit mode', typeof fpsEdit === 'number' && fpsEdit > 0, `fps=${fpsEdit}`);
check(
'the canvas bypasses the compositor queue by default',
(await evaluate('__gm.lowLatency()')) === true,
);
// Single evaluate: the readout refreshes twice a second, so read value and label together.
check(
'fps label is visible in the toolbar',