From f59d7604bfb5896a594a2c69586fc6c5142dd00a Mon Sep 17 00:00:00 2001 From: meleki Date: Fri, 2 Oct 2026 08:42:34 +0200 Subject: [PATCH] Architecture, Development: text and boxes on device pixels - Architecture: how the toolkit puts text and boxes on device pixels (crispText, crisp, hairline, the nudge of the stage), and what it does not give (subpixel smoothing). - Development: the run is at 100 %, step 62 alone runs at 150 %. The hooks that read the frame as the screen gets it, and the deviceScaleFactor option of the harness. The sum is 1656. Belongs to 32a0af6 in the main repository. --- Architecture.md | 15 +++++++++++++++ Development.md | 11 ++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Architecture.md b/Architecture.md index 0ab37d7..76ffb05 100644 --- a/Architecture.md +++ b/Architecture.md @@ -25,6 +25,21 @@ own edges. The toolkit only folds; which groups are folded is kept by the editor (`editor/sectionFolds.ts`, one entry in `localStorage`). Every title of the inspector goes through `addTitle` or `sectionHeader` in `inspector/host.ts`, so a new section gets the band by itself. + **Text and boxes sit on device pixels** (`ui/pixelGrid.ts`). The layout counts in whole CSS + pixels, and on a screen scaled to 125 % or 150 % most of those fall between two device pixels: + the GPU averages both, text goes soft and a 1 px line becomes a pixel and a half of mixed colour. + So every text of the interface is made with `crispText()` instead of `new Text()` — it sets + Pixi's `roundPixels`, which rounds the corners of what is drawn to device pixels — and a + `Graphics` that holds nothing but upright boxes is passed through `crisp()`, which does the same + where the scale is not a whole number and nothing at 100 % or 200 %. A curve, an icon or anything + of the game must *not* go through it: rounding its points bends it. A line is drawn `hairline()` + wide, one device pixel as a browser draws `1px`, and flush with the edge it belongs to + (`h - hairline()`, not `h - 1`); `drawControlBox`, `drawHeaderBar` and `drawSectionBand` do both + already. At 150 % every odd pixel is an exact half, and which way the GPU rounds a tie is + decided by the last bit of its arithmetic — one band came out a row shorter than its neighbours — + so the stage is nudged by 1/32 px (`tieBreakFor`), which sends every tie the same way. + What this does not give is the subpixel smoothing a browser applies to its own text: Pixi draws + a text into a transparent picture first, and there only greyscale smoothing is possible. Double-clicks are counted in `clicks.ts`, not read off the event: Pixi only puts a click count on the `click` it synthesises and leaves `detail` at 0 on `pointerdown`, so every press handler that trusted `e.detail` never saw a second click. Presses are counted per key rather than per diff --git a/Development.md b/Development.md index a90e879..31fd74b 100644 --- a/Development.md +++ b/Development.md @@ -18,7 +18,7 @@ php tests/php/accounts.test.php # the API's accoun # has and the web server's container has not (SKIP there) php tests/php/snapshots.test.php # the snapshot store (#231), the same way php tests/php/releases.test.php # the release store (#266), the same way -node tools/seed-e2e.mjs && node tools/e2e.mjs # 1648 editor checks, screenshots in /tmp/gm-e2e-*.png +node tools/seed-e2e.mjs && node tools/e2e.mjs # 1656 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 @@ -44,6 +44,15 @@ gets no WebGL context with them at all, so the app never renders.) Every run als in the log (`renderer: …`) and stops if that is a software one such as llvmpipe or SwiftShader: under WSL2 a missing GPU driver does not fail by itself, llvmpipe still offers WebGL 2 there. +**The run is at 100 %, one step is not.** The harness sets `deviceScaleFactor: 1`, where a layout +pixel is a device pixel and nothing can sit between two. What a screen scaled to 150 % shows is +therefore invisible to every step but 62 (`62-device-pixels.mjs`), which switches the scale over +CDP, reloads the page and reads the frame as the screen gets it: `__gm.deviceColumn()` gives the +device pixels down a column, `__gm.edgeContrast()` how hard the edges in a rectangle are, and +`__gm.pixelGrid()` the scale and the width of a hairline. It switches back and reloads again, so it +can stand anywhere in the order. A probe of your own takes the scale as an option: +`runChecks({ projectId, baseUrl, deviceScaleFactor: 1.5 }, body)`. + **Leave the full run to CI.** Every push runs the whole proof on the runner (see *Continuous integration*), so locally take only what the change needs: the gate, plus the step that covers it — `GM_STEPS` keeps that to seconds. The whole suite works here too and costs about four minutes and