Skip to content

feat: configurable viewport, fused ARGB raster, and Vue Vapor host hardening for ESP32-P4#140

Merged
doodlewind merged 6 commits into
pocket-stack:mainfrom
odex21:codex/esp32-p4-vue-vapor-host
Jul 22, 2026
Merged

feat: configurable viewport, fused ARGB raster, and Vue Vapor host hardening for ESP32-P4#140
doodlewind merged 6 commits into
pocket-stack:mainfrom
odex21:codex/esp32-p4-vue-vapor-host

Conversation

@odex21

@odex21 odex21 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Motivation

ESP32 devices ship with wildly different panels — 480×272, 640×226, 800×480, etc. — there's no "standard" size like a PSP screen. PocketJS currently hardcodes 480×272 in the rasterizer, the wasm binding, and both hosts. This PR removes that assumption, then adds the last few pieces needed to run on real ESP32 hardware and iterate in the browser preview.

Each commit is self-contained and reviewable independently.

Commits

  1. feat(core,wasm,host): configurable viewport for layout and rasterUi carries a viewport; render_scaled derives framebuffer dimensions from it instead of the fixed SCREEN_W/H; wasm exposes ui_set_viewport; web/sim hosts accept viewport and raster-density options. Any target can size its logical surface at boot without touching the core. Stock hosts keep the 480×272 golden contract.

  2. feat(raster): fused ARGB8888 output for ESP32-P4 PPA — new render_scaled_argb() emits A,R,G,B bytes — the in-memory order the ESP32-P4 PPA SRM consumes as ARGB8888. Byte placement is fused into blend_px via a const generic shared with the RGBA path, so it costs nothing; the firmware drops its per-frame RGBA→ARGB CPU reorder copy (measured: present 25.5 → 13.0 ms, 14 → 17.1 FPS).

  3. fix(vue-vapor): isolate the guest document and tolerate fragment-wrapper mirrors — covers two issues encountered while hosting a Vue Vapor player page:

    • Document isolation: when PocketJS is embedded in a real browser page, globalThis.document already exists. The old if (!g.document) guard never fires, so the PocketJS DOM facade is not installed, and the Vue Vapor guest's document.createElement() falls through to the real browser DOM. This PR introduces globalThis.__pocketDocument and aliases document to it via esbuild define (only for --framework=vue-vapor builds), so the real document stays untouched.
    • Fragment-wrapper tolerance: Vue Vapor injects internal fragment wrappers ({ nodes: […] }) into node.children during dynamic branch swaps (e.g. {playing ? <PauseIcon/> : null}). Upstream PR chore: upgrade Vue to 3.6.0-rc.1 #137 and fix(vue-vapor): guard node.children access #142 add if (node.children) guards that prevent crashes when children is undefined — but a fragment wrapper passes the truthy guard and is silently serialized as {"t":"undefined"}, losing all inner content. forEachTreeChild / forEachTreeMirror recursively unpacks these wrappers so tree snapshots stay correct.

Why forEachTreeChild instead of if (node.children)?

PR #137 (merged) and #142 (open) add null guards to serializeNode / countNodes / subtreeHasRetained. These prevent the crash but don't handle the fragment-wrapper case. A test added in this PR demonstrates the difference:

// Inject a Vue Vapor fragment wrapper into the tree
(playbackStatus.children as unknown[]).push({ nodes: [[pausedTextNode]] });
// Trigger tree serialization
push({ t: "getTree" });
frame();
expect(treeJson).toContain("PAUSED"); // ← fails without forEachTreeMirror

With the simple guard: wrapper passes through → serialized as {"t":"undefined"}test fails.
With forEachTreeChild: wrapper is recursively unpacked → "PAUSED" found → test passes.

This PR keeps the comment-anchor skip from #137forEachTreeChild subsumes both.

Compatibility

  • render / render_scaled output is untouched and byte-exact: 49/49 committed pixel goldens pass.
  • cargo test (core): 78/78, including a new test pinning ARGB↔RGBA channel mapping through the translucent destination-read blend path.
  • bun test: renderer / clock / tiles / cursor / devtools / vue-vapor-dom 88 pass; sim 7 pass; launcher-sim 10 pass.
  • No new dependencies, no feature flags, no breaking API changes.

Hardware validation (commit 2)

ESP32-P4 @ 640×226 → PPA 2× scale + 90° rotation → 452×1280 panel: present time 25.5 → 13.0 ms, whole-pipeline 14 → 17.1 FPS, pixel stream bit-identical to the previous software reorder (colors verified on device).

@odex21
odex21 force-pushed the codex/esp32-p4-vue-vapor-host branch from 7a17f6e to ff1c7c5 Compare July 21, 2026 11:46
odex21 added 3 commits July 22, 2026 00:17
…per mirrors

Vue Vapor guests compiled with `document` aliased to
globalThis.__pocketDocument (esbuild define) so they never touch the real
DOM of an embedding page, and DevTools tree serialization/counting walks
children via forEachTreeChild to tolerate the fragment-wrapper mirrors
(comment-anchor-wrapped sibling ranges) Vue Vapor produces while swapping
a dynamic branch — the shape behind the playing/paused status freeze.
Hosts are no longer pinned to the stock 480x272 logical surface. The
rasterizer derives framebuffer dimensions from Ui::viewport() instead of
the fixed SCREEN_W/H, the wasm binding exposes ui_set_viewport, and the
web/sim hosts accept viewport and raster-density options (createWasmUi
options, bootWorld SimViewportOptions, ?width=/?height=/?scale=/?density=).
ui_render and ui_render_scaled(1) share one code path, so hosts call
renderScaled(scale) unconditionally. The vue-vapor entry sizes its root
layers from hostViewport. Stock hosts never set a viewport and keep the
byte-exact 480x272 golden contract.
Add render_scaled_argb(): same DrawList interpretation as render_scaled
but emitting A,R,G,B bytes per pixel — the in-memory order the ESP32-P4
PPA SRM consumes as ARGB8888 input. Byte placement is fused into blend_px
via a const generic shared with the RGBA path, so the layout change costs
nothing and the firmware drops its per-frame RGBA->ARGB reorder copy
(measured on ESP32-P4 at 640x226: present 25.5 -> 13.0 ms, 14 -> 17.1 FPS).
Existing render/render_scaled output is untouched and byte-exact; a new
unit test pins the ARGB<->RGBA channel mapping including the translucent
destination-read blend path.
@odex21
odex21 force-pushed the codex/esp32-p4-vue-vapor-host branch from ff1c7c5 to fc7b285 Compare July 21, 2026 16:44
@odex21 odex21 changed the title feat: configurable Vue Vapor hosts + fused ARGB8888 raster output (ESP32-P4) feat: configurable viewport, fused ARGB raster, and Vue Vapor host hardening for ESP32-P4 Jul 21, 2026
@odex21

odex21 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Updated PR description

The PR body was revised to address review feedback. Key changes:

Removed:

  • Duplicate rebase notes (the "package.json test chain is a union" and "src/devtools.ts" paragraphs appeared twice)
  • The individual "feat(host-web): viewport URL params, pointer touch input, perf HUD" commit was folded into the viewport commit description — its content is still in the diff, just not called out separately since it's integral to the viewport feature

Added:

Unchanged:

  • All three commits, their code, and their commit messages are untouched
  • Hardware benchmarks and compatibility numbers are the same

doodlewind and others added 3 commits July 22, 2026 08:13
…ocument patching

- engine.js: only inline-size the canvas for custom viewports; the stock
  480x272 host keeps the stylesheet's 2x preview.
- wasm-ops.js: tolerate wasm binaries that predate ui_set_viewport at the
  stock size (drawHash convention), fail loud for custom viewports.
- components-vue-vapor.ts: withNativeTextDocument patches the guest's
  __pocketDocument, never the embedding page's real document.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@doodlewind
doodlewind merged commit 2df9ce2 into pocket-stack:main Jul 22, 2026
doodlewind added a commit that referenced this pull request Jul 22, 2026
…port work)

Conflicts were squash-merge echoes of the launcher PR plus the ESP32
viewport refactor: kept the .pocket package side everywhere it owned the
hunk, took main's join()-style paths and SimViewportOptions, and merged
the test chain as the union of both additions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants