feat(3ds): Nintendo 3DS host with a PICA200 backend - #252
Draft
doodlewind wants to merge 2 commits into
Draft
Conversation
A QuickJS guest over the PICA200 GPU on the 3DS top screen, admitted through the out-of-registry `3ds-dev` profile (hostAbi 7) until it passes on hardware, the same route symbian-e7-dev and iphone2g-dev take. The split follows hosts/iphone2g rather than hosts/psp: citro3d is mostly `static inline`, so C owns the GPU and Rust owns the core. `hosts/3ds/core` is a no_std staticlib built for the built-in `armv6k-nintendo-3ds` target with `-Z build-std` on macOS, exporting the `ui_*` C ABI plus the DrawList itself; `hosts/3ds/src/gfx.c` walks that list and issues citro3d calls. QuickJS builds for the 3DS from the revision hosts/psp already pins, with three portability flags: `JS_NO_NAN_BOXING` (the Vita treatment for 32-bit ARM), `__TM_GMTOFF=tm_gmtoff` (newlib declares the field only under that macro), and `-Wno-incompatible-pointer-types` (devkitARM ships GCC 16). Everything the device toolchain touches runs in the `devkitpro/devkitarm` container, driven by `tools/3ds.ts`. The top screen is 400x240 — smaller than 480x272 on both axes — so integer-fit is arithmetically impossible and the resolver has no scaling fallback by design. The profile declares 400x240 `native`, and apps/3ds-demo declares that viewport and doubles as a calibration surface: an orientation key whose notch moves quadrant if the texture flip or the 8x8 Morton tiling is wrong, corner brackets that only touch all four edges at this size, and the raw packed analog word printed so a wrong `(x<<8)|y` is readable off a capture. `input.touch` is deliberately not advertised: the touchscreen is the bottom screen while the UI is on the top, so reporting those contacts as top-screen logical coordinates would be false. tests/e2e/azahar.ts builds a capture .3dsx per spec, boots Azahar against a fixture $HOME (the emulator has no config or user-dir flag, and CITRA_USER_DIR is a no-op on macOS), waits for the guest's sentinel, SIGKILLs, and compares a GX display transfer of the render target — a real GPU readback, not a CPU oracle. Software and Vulkan do not agree (48.7% of pixels on real UI content), so the fixture pins graphics_api=0 and tests/goldens/3ds/AZAHAR-BUILD.txt records what the goldens came from. Two bugs found by measuring against the wasm oracle rather than by reading: the C objects did not depend on their `-D` values, so a changed capture window or input tape lingered in cached objects (a CFLAGS stamp now forces the rebuild); and `qjs.c` never published `ui.__viewport`, so the framework sized its layers at the 480x272 spec screen and everything measured from a row's right edge sat exactly 80px too far right. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The capture display transfer asked the PPF for a 32-bit linear output out of the 240x400 tiled colour buffer. Azahar's software rasterizer answers that correctly, which is why the committed goldens are right, but a hardware renderer returns rows that are individually correct and progressively misregistered: the same build came back shredded under Vulkan. Transfer with GX_TRANSFER_FMT_RGB8 instead, the format citro3d's own presentation transfer uses, and widen B,G,R into the A,B,G,R capture word on device so the driver's decode is unchanged. Re-recorded goldens are byte-identical to the old ones. A Vulkan capture now decodes to the correct screen; it still differs from the software goldens on 5.1% of pixels, 99.5% of them by 1 or 2 of 255, so the backend pin stays and E2E_AZAHAR_GRAPHICS_API re-measures the gap. --cia writes dist/3ds/<app>.cia from the same ELF and the same staged romfs directory. A .3dsx inherits the Homebrew Launcher's memory allocation; a CIA is its own title and asks for its own region through hosts/3ds/app.rsf's SystemMode: 64MB. makerom ships in neither devkitPro nor Homebrew, so it is cloned shallow, built in the container and cached the way libquickjs.a is. Title, product code and unique id are derived from the resolved plan, with the unique id inside the 0xFF000-0xFFFFF homebrew block. Co-Authored-By: Claude Opus 5 (1M context) <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.
Adds a Nintendo 3DS host: a QuickJS guest rendering through the PICA200 GPU on the 400×240 top screen, with an end-to-end golden suite driven by the Azahar emulator.
A real PocketJS Solid bundle boots and renders — text at five baked sizes, textures, gradients, rounded borders, d-pad focus traversal — and three captured frames compare byte-exact run to run.
How it is put together
The split follows
hosts/iphone2g, nothosts/psp: citro3d is mostlystatic inline, so C owns the GPU and Rust owns the core.hosts/3ds/core— ano_stdstaticlib built for the built-inarmv6k-nintendo-3dstarget with-Z build-stdon macOS, exporting theui_*C ABI plus the DrawList itself.hosts/3ds/src/gfx.c— walks thatu32DrawList and issues citro3d calls; all 8 ops.hosts/psp/Cargo.tomlalready pins, with three portability flags:JS_NO_NAN_BOXING(the Vita treatment for 32-bit ARM),__TM_GMTOFF=tm_gmtoff(newlib declares the field only under that macro),-Wno-incompatible-pointer-types(devkitARM ships GCC 16).devkitpro/devkitarmcontainer, driven bytools/3ds.ts. Nothing needs devkitPro installed on the host.Contract
Registered as the out-of-registry
3ds-devprofile, hostAbi 7 (tools/3ds-profile.ts), the routesymbian-e7-devandiphone2g-devtake.POCKET_TARGETSis an inventory of hardware-tested hosts and this has only been run on an emulator, so promotion is a follow-up gated on a device pass.The top screen is 400×240 — smaller than 480×272 on both axes — so
integer-fitis arithmetically impossible and the resolver has no scaling fallback by design. The profile declares 400×240native; the stock corpus does not admit, andapps/3ds-demodeclares that viewport.input.touchis deliberately not advertised: the touchscreen is the bottom screen while the UI is on the top, so reporting those contacts as top-screen logical coordinates would be false.The demo is a calibration surface
apps/3ds-demois built so a backend bug is visible rather than subtle: an orientation key whose notch changes quadrant if the vertical flip or the 8×8 Morton tiling is wrong, corner brackets that touch all four edges only at 400×240 density 1, and the raw packed analog word printed so a wrong(x<<8)|yreads straight off a capture.E2E
bun run e2e:3ds(UPDATE_3DS=1to re-record). Azahar has no headless mode, ignores SIGTERM, and does not stop when the guest returns frommain(), so the driver owns its lifetime (SIGKILL on every path) and its user directory.CITRA_USER_DIRis a no-op on macOS, so each run gets a fixture$HOME. Only one run at a time per machine.C3D_SyncDisplayTransferof the PICA200 render target — not a CPU oracle. ReadinggfxGetFramebufferafterC3D_FrameEndreturns the already-swapped buffer, which is black.graphics_api=0andtests/goldens/3ds/AZAHAR-BUILD.txtrecords the build and backend the goldens came from.bun run test: 11/11 stages green.Two bugs found by measuring, not by reading
Both were caught by diffing the PICA200 render against the wasm rasterizer at the same size, and both looked healthy from the outside:
-Dvalues, so a changed capture window or input tape silently lingered in cached objects — a run dumped 3 frames instead of 23 and reported success.hosts/3ds/Makefilenow carries a CFLAGS stamp, the make-side counterpart ofcargo:rerun-if-env-changed.qjs.cnever publishedui.__viewport, soframework/src/index.tsfell back to the 480×272 spec screen and sized its layers there. Everything measured from a row's right edge sat exactly 80px (480−400) too far right. A regression test pins the publication — nothing short of an emulator run would otherwise catch its removal.Residual against the wasm oracle is 5.1%, all ±1–2 in a single channel on colour transitions (glyph AA, rounded-border and gradient blends). It is provably non-geometric: zero differing pixels sit inside a flat 3×3 block of the oracle, which a displaced shape could not manage.
Not verified
setPropBatch,setSprite, the streamed-texture and DevTools ops are marshalled but never exercised by this demo.🤖 Generated with Claude Code