-
Notifications
You must be signed in to change notification settings - Fork 252
tools/qemu-wasm: PebbleOS emulator in the browser (QEMU→wasm, app install, PebbleKit JS) #1910
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
ericmigi
wants to merge
27
commits into
main
Choose a base branch
from
claude/pebbleos-agent-swarm-iviybe
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 15 commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
18a21c6
tools/qemu-wasm: add browser emulator for the wasm QEMU build
5e9f89d
tools/qemu-wasm: single-file emulator page and minimal QEMU build
8cee325
tools/qemu-wasm: add prebuilt site bundle for Pages deployment
a172d0f
tools/qemu-wasm: TCI performance patches
5ae2732
tools/qemu-wasm: TCG-to-wasm JIT overlay, 17 fps
827f5bd
tools/qemu-wasm: coerce pointer exports to Number for MEMORY64 builds
16e8de1
tools/qemu-wasm: import the runtime as qemu-system-arm.js
a76db26
tools/qemu-wasm: refresh site bundle with the JIT build
0a3fc0c
qemu-wasm: bridge UART1 to the browser via shared-memory rings
1a4c026
qemu-wasm: install apps from the browser (store link or .pbw)
8b1172a
qemu-wasm: refresh site bundle with app-install build
6f08ee5
qemu-wasm: add an Upload .pbw button to the install panel
a5aaba6
qemu-wasm: run PebbleKit JS apps in the browser
d5b5595
qemu-wasm: add the PebbleKit JS CORS proxy worker
18dcb62
qemu-wasm: ship PebbleKit JS in the site bundle
dc0677a
qemu-wasm: carry the pkjs-proxy worker into site deploys
fe8f51a
qemu-wasm: default to the deployed pkjs-proxy worker
1731f39
qemu-wasm: render Clay data: config pages in an in-page modal
c7391d3
qemu-wasm: surface pkjs logs in the on-page console
58add8d
qemu-wasm: log geolocation and fetch outcomes from app JS
ad14a94
qemu-wasm: robust geolocation for JS apps
13c978a
qemu-wasm: wire up the touchscreen and audio output
cde681f
qemu-wasm: persistence, autoboot, console input, sensors, gabbro
3835de5
qemu-wasm: readable console, notifications, voice dictation
3a2438c
qemu-wasm: keep the board selector active while running
9242b68
qemu-wasm: stop form fields from driving the watch buttons
9d4caa6
qemu-wasm: fix deploy bundle missing the new page modules
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| web/qemu-system-arm.js | ||
| web/qemu-system-arm.wasm | ||
| web/qemu-system-arm.worker.js | ||
| web/firmware/ | ||
| web/qemu-system-arm.mjs |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| # Emscripten link-flag notes | ||
|
|
||
| The reference build (coredevices/qemu `build-wasm/`, emsdk 3.1.50) links | ||
| `qemu-system-arm.js` with, among others: | ||
|
|
||
| ``` | ||
| -pthread -sASYNCIFY=1 -sPROXY_TO_PTHREAD=1 -sFORCE_FILESYSTEM | ||
| -sALLOW_TABLE_GROWTH -sTOTAL_MEMORY=2GB -sWASM_BIGINT -sEXPORT_ES6=1 | ||
| -sASYNCIFY_IMPORTS=ffi_call_js | ||
| -sEXPORTED_RUNTIME_METHODS=addFunction,removeFunction,TTY,FS,HEAPU8,HEAPU32,callMain | ||
| -sPTHREAD_POOL_SIZE=4 -sUSE_SDL=2 | ||
| ``` | ||
|
|
||
| (from `configs/meson/emscripten.txt` in the qemu tree plus the wasm-deps | ||
| LDFLAGS; `-sEXPORT_ES6=1` implies MODULARIZE, hence the dynamic `import()` | ||
| in `web/index.html` and `smoke-test.mjs`, and `"type": "module"` in | ||
| `web/package.json`.) | ||
|
|
||
| ## What the shell needs | ||
|
|
||
| Covered by the current flags: `FS` (serial log polling, firmware upload in | ||
| `preRun`), `HEAPU8`/`HEAPU32` (display blit, button mask writes), and the | ||
| `EMSCRIPTEN_KEEPALIVE` browser glue in `hw/display/pebble_display.c` | ||
| (`pebble_wasm_display_{width,height,stride,data,frame_count}`) and | ||
| `hw/gpio/pebble_gpio.c` (`pebble_wasm_button_state_addr`), which need no | ||
| `-sEXPORTED_FUNCTIONS` entry. No extra relink flags are required for the | ||
| page as written. | ||
|
|
||
| ## Possible future flags / glue | ||
|
|
||
| - `ENV` in `-sEXPORTED_RUNTIME_METHODS` — set guest environment variables | ||
| from JS before `main()` runs (`PEBBLE_QEMU_FIRST_BOOT_LOGIC_ENABLE`, | ||
| `PEBBLE_QEMU_START_CONNECTED`, `PEBBLE_QEMU_START_PLUGGED_IN` are read | ||
| by `pebble_generic.c`). Without it the defaults (start connected) | ||
| apply. | ||
| - Touch injection (emery/gabbro): natively `./pbl touch` drives | ||
| `hw/misc/pebble_touch.c` through QMP `input-send-event`, which the | ||
| browser build doesn't run. Needs a QMP-less glue export in the touch | ||
| device (e.g. a shared-memory x/y/pressed record polled on a | ||
| virtual-clock timer, like the button mask). Do not fake it from JS. | ||
|
|
||
| ## SDL display experiment (not wired into the page) | ||
|
|
||
| The build also contains QEMU's SDL2 UI compiled against emscripten's SDL2 | ||
| port (`-sUSE_SDL=2`, `CONFIG_SDL`). In principle `-display sdl` with a | ||
| `Module.canvas` renders without any of the `pebble_wasm_*` glue, and would | ||
| also route keyboard/pointer events through QEMU's input layer (including | ||
| `pebble_touch`). Unverified under `-sPROXY_TO_PTHREAD` (SDL calls are | ||
| proxied to the main thread); if it is ever made to work, | ||
| `-sOFFSCREENCANVAS_SUPPORT=1` is worth trying to render from the QEMU | ||
| pthread directly. The shared-memory path above is the proven, primary | ||
| mechanism. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| # PebbleOS in the browser (QEMU + WebAssembly) | ||
|
|
||
| QEMU compiled to WebAssembly boots real PebbleOS firmware and renders the | ||
| watch display to an HTML canvas — no install, no server-side emulation. | ||
|
|
||
| ## History | ||
|
|
||
| Pebble's original emulator was a QEMU 2.5 fork with STM32 peripheral models. | ||
| [ericmigi/pebble-qemu-wasm](https://github.com/ericmigi/pebble-qemu-wasm) | ||
| ported those device models to QEMU 10.1 and solved the browser-integration | ||
| problems (emscripten pthreads, main-thread proxying, canvas rendering, | ||
| input injection); this shell is derived from it. What changed since: the | ||
| [coredevices/qemu](https://github.com/coredevices/qemu) fork replaced the | ||
| STM32 models with generic virtual Pebble machines (`pebble-emery`, | ||
| `pebble-flint`, `pebble-gabbro` in `hw/arm/pebble_generic.c`) that expose | ||
| simple MMIO peripherals — a plain framebuffer display, 4-button GPIO, | ||
| touch, UARTs — instead of emulating a specific MCU. That is the same QEMU | ||
| `./pbl qemu` uses natively (see `docs/development/qemu.md`), so the browser | ||
| build now tracks mainline PebbleOS instead of a firmware fork. | ||
|
|
||
| `web/coi-serviceworker.min.js` is | ||
| [coi-serviceworker](https://github.com/gzuidhof/coi-serviceworker) v0.1.7 | ||
| (MIT, Guido Zuidhof and contributors), copied via ericmigi/pebble-qemu-wasm. | ||
|
|
||
| ## Quick start | ||
|
|
||
| ```sh | ||
| # 1. Build QEMU for wasm32 (one-off, ~30 min) | ||
| ./build-qemu-wasm.sh ~/coredevices/qemu | ||
|
|
||
| # already have a build? just copy the artifacts: | ||
| ./get-artifacts.sh ~/coredevices/qemu/build-wasm | ||
|
|
||
| # 2. Fetch release firmware | ||
| ./fetch-firmware.sh emery | ||
|
|
||
| # 3. Serve and open | ||
| ./serve.py 8080 | ||
| open http://localhost:8080 | ||
| ``` | ||
|
|
||
| Click Boot. The page fetches the firmware images (~34 MB) and the WASM | ||
| binary, then boots. The boot logo appears within seconds; a full boot to | ||
| the launcher takes about a minute (TCI interpreter, measured under node on | ||
| 4 cores). | ||
|
|
||
| The dev server sends `Cross-Origin-Opener-Policy` / | ||
| `Cross-Origin-Embedder-Policy` headers required for `SharedArrayBuffer` | ||
| (emscripten pthreads). On static hosts without those headers the bundled | ||
| coi-serviceworker provides them after one reload. | ||
|
|
||
| URL parameters: `?board=emery|flint|gabbro`, `?auto` (boot immediately), | ||
| `?audio` (enable the SDL audio backend). | ||
|
|
||
| ## Headless smoke test | ||
|
|
||
| ```sh | ||
| node smoke-test.mjs --board emery --seconds 300 | ||
| ``` | ||
|
|
||
| Boots the WASM build under node with `-display none`, tails the UART2 | ||
| debug console, and exits 0 once boot markers (ending with | ||
| `Ready for communication.`) appear and the exported display surface | ||
| reports the board's resolution with an advancing frame counter. | ||
|
|
||
| ## How it works | ||
|
|
||
| - **Machine**: `-machine pebble-emery -kernel qemu_micro_flash.bin -drive | ||
| if=mtd,format=raw,file=qemu_spi_flash.bin -display none`. Code flash is | ||
| mapped at 0x0, so the raw release image boots directly via `-kernel`; | ||
| the 32 MB SPI flash image backs the external-flash device. | ||
| - **Display**: firmware writes pixels to framebuffer MMIO at 0x50000000; | ||
| `hw/display/pebble_display.c` converts them (ARGB2222 or 1bpp) to a | ||
| QemuConsole surface. Under emscripten a virtual-clock timer re-renders | ||
| that surface every 33 ms even with `-display none`, and the | ||
| `pebble_wasm_display_*` exports hand the page a heap pointer to the | ||
| 32bpp surface plus a frame counter. The page polls the counter on a | ||
| 30 ms interval and blits changed frames (BGRX to RGBA) into a canvas | ||
| `ImageData` — the same shared-memory scheme the old shell proved out. | ||
| - **Input**: `hw/gpio/pebble_gpio.c` exports | ||
| `pebble_wasm_button_state_addr()`, the heap address of a button | ||
| bitmask (bit 0 Back, 1 Up, 2 Select, 3 Down). Key and pointer events | ||
| `Atomics.store` the mask; a 16 ms virtual-clock poll inside the device | ||
| applies press and release edges, so held buttons work. | ||
| - **Serial**: UART2 (debug console) is routed to a MEMFS file the page | ||
| polls. UART1 (pebble-tool control protocol) is bridged to the page | ||
| through two ring buffers in wasm memory (`pebble_wasm_serial_ctrl()` | ||
| in `pebble_control.c`, JIT overlay): the page writes QemuProtocol | ||
| frames into the rx ring with Atomics and drains watch-bound bytes from | ||
| the tx ring; a 2 ms virtual-clock timer feeds the existing chardev | ||
| receive path. | ||
| - **PebbleKit JS**: apps with a `pebble-js-app.js` get their JS run in a | ||
| hidden same-origin iframe with a `Pebble` shim (`web/pkjs-runtime.js`), | ||
| started/stopped by the watch's app_run_state notifications on endpoint | ||
| 52. `web/appmessage.js` speaks AppMessage (endpoint 0x30): PUSH with | ||
| the app uuid + little-endian tuple dictionary, 2-byte ACK/NACK with | ||
| echoed transaction id; sends are serialized and pushes that arrive | ||
| during JS startup are queued. The app JS gets real fetch/XHR (with a | ||
| CORS-proxy fallback, `pkjs-proxy/`), geolocation, per-app-scoped | ||
| localStorage, and config pages via `openURL` + the `return_to` | ||
| convention (`web/config-return.html`). Timeline APIs are stubbed. | ||
| - **App install**: `web/pebble-transport.js` implements the phone side of | ||
| the QEMU serial framing (0xFEED/0xBEEF), Pebble Protocol reassembly, | ||
| and the endpoint-17 phone-version handshake (a V3 response that | ||
| re-asserts capabilities 0xA3 — the firmware replaces, not ORs, session | ||
| capabilities). `web/app-install.js` drives the 4.x install flow: | ||
| BlobDB INSERT of a 126-byte AppDBEntry into the app db (retrying on | ||
| TRY_LATER), `app_run_state` RUN to trigger the watch's AppFetch | ||
| request, then one PutBytes session per object (app binary, resources, | ||
| worker) with the legacy STM32 CRC (`legacyDefectiveCrc`, verified | ||
| against `tests/fw/util/test_legacy_checksum.c` vectors). `web/pbw.js` | ||
| unzips the .pbw with the native DecompressionStream and picks the best | ||
| platform directory; `web/store.js` resolves apps.repebble.com / | ||
| apps.rebble.io links through the CORS-enabled appstore API. The page | ||
| offers a paste-a-link box and drag-drop of .pbw files. | ||
|
|
||
| ## Controls | ||
|
|
||
| | Input | Button | | ||
| |-----------------------------|--------| | ||
| | Arrow Left, Escape, Backspace | Back | | ||
| | Arrow Up / `w` | Up | | ||
| | Arrow Right, Enter, `s` | Select | | ||
| | Arrow Down / `x` | Down | | ||
|
|
||
| On-screen buttons track pointer down/up, so press-and-hold works. | ||
|
|
||
| ## Known limitations | ||
|
|
||
| - TCI interpreter: roughly 1-2 orders of magnitude slower than native | ||
| TCG. Expect multi-minute boots and single-digit FPS. (The `jit/` build | ||
| the site ships does not have this problem.) | ||
| - App install requires the `jit/` build — the serial bridge lives in the | ||
| JIT overlay's `pebble_control.c`; the TCI patch set does not carry it | ||
| yet. | ||
| - No touch yet on emery/gabbro: natively `./pbl touch` injects pointer | ||
| events over QMP, which the browser build doesn't run (see NOTES.md). | ||
| - flint and gabbro machines exist but are untested in the browser; the | ||
| board selector will boot them if firmware is fetched. | ||
| - Audio is off by default (`?audio` to try the SDL backend). | ||
|
|
||
| For the native QEMU workflow (`./pbl qemu`, buttons via monitor `sendkey`, | ||
| touch via QMP, gdb, screenshots) see `docs/development/qemu.md`. | ||
|
|
||
| ## Files | ||
|
|
||
| - `web/index.html` — emulator page | ||
| - `web/coi-serviceworker.min.js` — COOP/COEP fallback for static hosts | ||
| - `serve.py` — dev server with COOP/COEP headers | ||
| - `build-qemu-wasm.sh` — reproducible emsdk + deps + QEMU build | ||
| - `get-artifacts.sh` — copy `qemu-system-arm.{js,wasm}` into `web/` | ||
| - `fetch-firmware.sh` — download release firmware into `web/firmware/` | ||
| - `smoke-test.mjs` — headless boot test under node | ||
| - `NOTES.md` — link-flag requirements for the emscripten build | ||
|
|
||
| ## Performance patches | ||
|
|
||
| `patches/0001` also carries the wasm perf work (all `__EMSCRIPTEN__`-gated | ||
| or wasm-only): RAM-backed display framebuffer (no MMIO traps on pixel | ||
| writes), inline TLB fast path in the TCI interpreter, `cpu_io_recompile` | ||
| skip, `-sASYNCIFY_REMOVE` for the interpreter hot path, mimalloc, and | ||
| `-Doptimization=3`. Measured on 4 shared cores under node, pebble-emery | ||
| v4.35.0, continuous launcher scroll: | ||
|
|
||
| | build | boot to ready | boot-anim fps | scroll fps | | ||
| |---|---|---|---| | ||
| | unpatched TCI | 91 s | 1.3 | 2.1 | | ||
| | patched TCI | 38 s | 6.9 | 4.5 | | ||
| | wasm JIT (`jit/`) | 15 s | 7.1 | 16.9 | | ||
|
|
||
| The interpreter plateaus around 4.5 fps; the TCG-to-wasm JIT build in | ||
| `jit/` is the next multiplier and reaches ~17 fps. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # Single-file emulator page | ||
|
|
||
| Packs the whole emulator — QEMU wasm, runtime JS, pthread worker, and the | ||
| release firmware images — into one self-contained HTML file (gzip + base64, | ||
| unpacked in the browser with `DecompressionStream`). Useful anywhere only a | ||
| single static page can be hosted, e.g. Claude artifacts. | ||
|
|
||
| The page needs a host that serves COOP/COEP headers (SharedArrayBuffer); | ||
| it probes the environment first and explains what is missing instead of | ||
| hanging. | ||
|
|
||
| Use the size-reduced QEMU build so the page fits common limits: configure | ||
| with `--without-default-devices --with-devices-arm=pebble` (the Pebble | ||
| machines only — 25 MB wasm instead of 39 MB, ~13.8 MB page total). | ||
|
|
||
| ```sh | ||
| python3 assemble.py <qemu-build-dir> pebble-emulator.html | ||
| node page-test.mjs pebble-emulator.html coi 300 shot.png # boots it headless | ||
| node page-test.mjs pebble-emulator.html nocoi 30 shot.png # diagnostics path | ||
| ``` | ||
|
|
||
| `assemble.py` also patches one emscripten 3.1.50 line whose | ||
| `new URL(..., import.meta.url)` throws when the module is imported from a | ||
| blob: URL inside the pthread worker (see comment in the script). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| #!/usr/bin/env python3 | ||
| """Assemble the single-file emulator artifact from template + payloads.""" | ||
| import base64 | ||
| import gzip | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| HERE = Path(__file__).parent | ||
| FW = Path('/home/user/qemu-wasm-firmware') | ||
|
|
||
|
|
||
| # In pthread workers Module.locateFile is lost to structured clone, so the | ||
| # runtime computes new URL(..., import.meta.url) — which throws for blob: | ||
| # bases. The worker receives the compiled wasm module by message and never | ||
| # fetches it, so a plain relative name is safe. | ||
| MJS_PATCH = ( | ||
| "wasmBinaryFile = new URL('qemu-system-arm.wasm', import.meta.url).href;", | ||
| "wasmBinaryFile = 'qemu-system-arm.wasm';", | ||
| ) | ||
|
|
||
|
|
||
| def pack(path): | ||
| raw = Path(path).read_bytes() | ||
| if path.name.endswith('.js') and not path.name.endswith('.worker.js'): | ||
| text = raw.decode() | ||
| assert text.count(MJS_PATCH[0]) == 1, 'mjs patch anchor not found' | ||
| raw = text.replace(*MJS_PATCH).encode() | ||
| gz = gzip.compress(raw, 9) | ||
| return base64.b64encode(gz).decode(), len(raw), len(gz) | ||
|
|
||
|
|
||
| def main(build_dir, out_path): | ||
| build = Path(build_dir) | ||
| tpl = (HERE / 'template.html').read_text() | ||
| total_unpacked = 0 | ||
| sizes = {} | ||
| for key, path in [ | ||
| ('__MJS_B64__', build / 'qemu-system-arm.js'), | ||
| ('__WORKER_B64__', build / 'qemu-system-arm.worker.js'), | ||
| ('__WASM_B64__', build / 'qemu-system-arm.wasm'), | ||
| ('__MICRO_B64__', FW / 'qemu_micro_flash.bin'), | ||
| ('__SPI_B64__', FW / 'qemu_spi_flash.bin'), | ||
| ]: | ||
| b64, raw, gz = pack(path) | ||
| tpl = tpl.replace(key, b64) | ||
| total_unpacked += raw | ||
| sizes[key] = (raw, gz, len(b64)) | ||
| print(f'{path.name}: raw={raw:,} gz={gz:,} b64={len(b64):,}') | ||
| tpl = tpl.replace('__UNPACK_MB__', str(round(total_unpacked / 1e6))) | ||
| tpl = tpl.replace('__PAYLOAD_NOTE__', | ||
| f'{round(sum(s[2] for s in sizes.values())/1e6, 1)} MB embedded, ' | ||
| f'{round(total_unpacked/1e6)} MB unpacked in memory') | ||
| Path(out_path).write_text(tpl) | ||
| print(f'TOTAL page: {len(tpl):,} bytes ({len(tpl)/1e6:.2f} MB; limit 16MB)') | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| main(sys.argv[1], sys.argv[2]) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| // Test the assembled single-file emulator page under chromium. | ||
| // Usage: node page-test.mjs <html-file> <coi|nocoi> <wait-seconds> <shot.png> | ||
| import { chromium } from 'playwright'; | ||
| import { createServer } from 'node:http'; | ||
| import { readFileSync } from 'node:fs'; | ||
|
|
||
| const [file, mode, waitS, shot] = process.argv.slice(2); | ||
| const html = readFileSync(file); | ||
|
|
||
| const server = createServer((req, res) => { | ||
| const headers = { 'Content-Type': 'text/html', 'Cache-Control': 'no-store' }; | ||
| if (mode === 'coi') { | ||
| headers['Cross-Origin-Opener-Policy'] = 'same-origin'; | ||
| headers['Cross-Origin-Embedder-Policy'] = 'require-corp'; | ||
| } | ||
| res.writeHead(200, headers); | ||
| res.end(html); | ||
| }); | ||
| await new Promise((r) => server.listen(8099, r)); | ||
|
|
||
| const browser = await chromium.launch({ executablePath: '/opt/pw-browsers/chromium', args: ['--no-sandbox'] }); | ||
| const page = await browser.newPage({ viewport: { width: 760, height: 1100 } }); | ||
| page.on('pageerror', (e) => console.log('[pageerror]', String(e).slice(0, 300))); | ||
| page.on('console', (m) => { const t = m.text(); if (/err|fail|abort|BLOCKED/i.test(t)) console.log('[console]', t.slice(0, 200)); }); | ||
|
|
||
| await page.goto('http://localhost:8099/?auto', { waitUntil: 'load' }); | ||
| console.log('loaded, mode=' + mode); | ||
|
|
||
| const t0 = Date.now(); | ||
| let last = ''; | ||
| while ((Date.now() - t0) / 1000 < Number(waitS)) { | ||
| await new Promise((r) => setTimeout(r, 5000)); | ||
| const state = await page.evaluate(() => ({ | ||
| status: document.getElementById('status')?.textContent || '', | ||
| fps: document.getElementById('fps')?.textContent || '', | ||
| checks: [...document.querySelectorAll('.chk')].map((c) => c.textContent).join(' | '), | ||
| fail: document.getElementById('fail')?.style.display !== 'none', | ||
| nonWhite: (() => { | ||
| const c = document.getElementById('screen'); | ||
| const d = c.getContext('2d').getImageData(0, 0, c.width, c.height).data; | ||
| let n = 0; | ||
| for (let i = 0; i < d.length; i += 4) if (d[i] < 240 || d[i + 1] < 240 || d[i + 2] < 240) n++; | ||
| return n; | ||
| })(), | ||
| })); | ||
| const line = JSON.stringify(state); | ||
| if (line !== last) { console.log(`[${Math.round((Date.now() - t0) / 1000)}s]`, line); last = line; } | ||
| if (mode === 'nocoi' && state.fail) { console.log('DIAGNOSTIC SHOWN AS EXPECTED'); break; } | ||
| if (mode === 'coi' && state.status.includes('Display active') && state.nonWhite > 200 && state.nonWhite < 40000) { | ||
| console.log('PIXELS ON CANVAS — BOOT OK'); break; | ||
| } | ||
| } | ||
| await page.screenshot({ path: shot }); | ||
| console.log('screenshot: ' + shot); | ||
| await browser.close(); | ||
| server.close(); | ||
| process.exit(0); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the page never reaches the expected display state—for example because QEMU aborts, pixels remain blank, or the loop simply reaches
waitS—execution falls through and exits successfully anyway. This makes the documentedcoibrowser test report a green status for broken builds; track whether the success predicate was reached and return nonzero when it was not.Useful? React with 👍 / 👎.