From 0dcb3b44322f675c17ee91d79d254fd6174fb2f3 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 11 Sep 2026 11:54:19 +0200 Subject: [PATCH] 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 --- README.md | 6 +++++- src/editor/debugHook.ts | 3 +++ src/main.ts | 17 ++++++++++++++++- todo-notes/graph-node-collapse-width.md | 2 ++ tools/e2e/06-graph.mjs | 4 ++++ 5 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 todo-notes/graph-node-collapse-width.md diff --git a/README.md b/README.md index 18b413df..92330035 100644 --- a/README.md +++ b/README.md @@ -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 `` 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 diff --git a/src/editor/debugHook.ts b/src/editor/debugHook.ts index b2884749..5e252458 100644 --- a/src/editor/debugHook.ts +++ b/src/editor/debugHook.ts @@ -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), }; diff --git a/src/main.ts b/src/main.ts index c879a774..f730558f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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 { + const params = new URLSearchParams(location.search); const app = new Application(); await app.init({ resizeTo: window, @@ -15,6 +30,7 @@ async function main(): Promise { 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 { 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); diff --git a/todo-notes/graph-node-collapse-width.md b/todo-notes/graph-node-collapse-width.md new file mode 100644 index 00000000..19f83ab1 --- /dev/null +++ b/todo-notes/graph-node-collapse-width.md @@ -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? diff --git a/tools/e2e/06-graph.mjs b/tools/e2e/06-graph.mjs index 89c582f1..8a54bfab 100644 --- a/tools/e2e/06-graph.mjs +++ b/tools/e2e/06-graph.mjs @@ -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',