feat: configurable viewport, fused ARGB raster, and Vue Vapor host hardening for ESP32-P4#140
Merged
doodlewind merged 6 commits intoJul 22, 2026
Conversation
odex21
force-pushed
the
codex/esp32-p4-vue-vapor-host
branch
from
July 21, 2026 11:46
7a17f6e to
ff1c7c5
Compare
…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
force-pushed
the
codex/esp32-p4-vue-vapor-host
branch
from
July 21, 2026 16:44
ff1c7c5 to
fc7b285
Compare
Contributor
Author
Updated PR descriptionThe PR body was revised to address review feedback. Key changes: Removed:
Added:
Unchanged:
|
…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
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>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
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
feat(core,wasm,host): configurable viewport for layout and raster—Uicarries a viewport;render_scaledderives framebuffer dimensions from it instead of the fixedSCREEN_W/H; wasm exposesui_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.feat(raster): fused ARGB8888 output for ESP32-P4 PPA— newrender_scaled_argb()emits A,R,G,B bytes — the in-memory order the ESP32-P4 PPA SRM consumes as ARGB8888. Byte placement is fused intoblend_pxvia 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).fix(vue-vapor): isolate the guest document and tolerate fragment-wrapper mirrors— covers two issues encountered while hosting a Vue Vapor player page:globalThis.documentalready exists. The oldif (!g.document)guard never fires, so the PocketJS DOM facade is not installed, and the Vue Vapor guest'sdocument.createElement()falls through to the real browser DOM. This PR introducesglobalThis.__pocketDocumentand aliasesdocumentto it via esbuilddefine(only for--framework=vue-vaporbuilds), so the realdocumentstays untouched.{ nodes: […] }) intonode.childrenduring 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 addif (node.children)guards that prevent crashes whenchildrenis undefined — but a fragment wrapper passes the truthy guard and is silently serialized as{"t":"undefined"}, losing all inner content.forEachTreeChild/forEachTreeMirrorrecursively unpacks these wrappers so tree snapshots stay correct.Why
forEachTreeChildinstead ofif (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: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 #137 —
forEachTreeChildsubsumes both.Compatibility
render/render_scaledoutput 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.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).