瀏覽器中的 Nintendo 3DS:兩天 AI 輔助移植後,真正難題才浮現
Nintendo 3DS in a Browser: Two Days of AI-Assisted Porting, Then the Hard Parts
作者用兩天在 AI 協助下將 Panda3DS 移植至瀏覽器,讓 Super Mario 3D Land 可在瀏覽器運行,並於 Mac 和 iPhone 測試效能。
Cover: AI-generated concept illustration of a hypothetical foldable phone, not a photograph of existing hardware or a screenshot of the emulator.
It started with the idea of a dual-screen iPhone. That form factor seems almost made for Nintendo DS and 3DS games: the main display above, the touchscreen below.
But before thinking about a finished app, I wanted to answer a smaller question: could a 3DS emulator run on an iPhone at a useful speed?
For this experiment, I couldn't rely on giving an ordinary iOS app its own JIT entitlement. So I tried another route: recompile guest ARM code to WebAssembly at runtime, then let the browser engine compile that.
After two days, I had an experimental Panda3DS port running Super Mario 3D Land in a browser. On a Mac, it reached full emulation speed. On an iPhone, things got more interesting.
AI disclosure: AI wrote most of the new port code. I directed the work, ran builds, tested on devices, and helped investigate failures. This article was also prepared and translated with AI assistance. The emulator foundation is Panda3DS, and the CPU recompiler foundation is Dynarmic, built by their respective developers. These two days produced a port of that existing work, not a 3DS emulator from scratch.
Source on GitHub · Try the browser demo
What works today
In Chrome on an Apple Silicon Mac, Super Mario 3D Land and Mario Kart 7 run at 60 fps emulation speed in the tested scenes. Sound, keyboard input, and on-screen controls work.
Actual player screenshot: Super Mario 3D Land on an M-series Mac. This is separate from the imagined hardware in the cover.
Everything runs locally. The server serves the page and emulator; users open their own game dumps. There is no streaming or server-side emulation, and no commercial game data is hosted.
On iPhone, menus and light scenes in Mario 3D Land run at roughly 55–63 fps. The heavy World 1-1 scene only reaches 16–22 fps.
This is still an experiment. Compatibility varies, compiling new shaders can cause noticeable pauses, and some games do not get past startup. Full save states are not implemented, although game-save persistence is available.
You can try the site without a commercial game: it includes free homebrew graphics examples.
How ARM code reaches the browser
Panda3DS uses Dynarmic for dynamic CPU recompilation. The port adds a backend that translates Dynarmic's intermediate representation into WebAssembly.
The path is:
Guest ARM → Dynarmic IR → WebAssembly → machine code compiled by the browser engine.
PICA graphics are rendered through WebGL2.
On iPhone, this runs inside WKWebView without the application receiving its own JIT entitlement: WebKit performs the WebAssembly compilation.
The architecture sounds straightforward. Getting a picture on screen was only the beginning.
Surprise #1: headless testing hid a graphics bottleneck
One scene was bottlenecked by graphics buffer uploads.
In a visible Chrome window on ANGLE/Metal, the existing streaming-buffer path using glBufferSubData took approximately 30–40 microseconds per call. At more than 500 calls per frame, that becomes a substantial part of the frame budget.
Switching to buffer orphaning with glBufferData reduced the upload cost. In one measured scene, emulation speed went from 15–22 to 60 fps.
The awkward part: headless Chrome did not expose the same bottleneck.
For this project, the lesson was practical: automated runs help, but I also need to measure performance with the game actually visible.
Surprise #2: too many small WebAssembly instances
Initially, each compiled guest block had its own WebAssembly instance.
That worked at small scale. In Safari on the Mac, however, performance deteriorated as the number of live instances grew into the tens of thousands.
There was another problem: importing many helper functions consumed executable memory in JavaScriptCore, eventually crashing the page.
The changes were to:
- Pack 256 compiled blocks into one instance.
- Call helpers indirectly through a shared table, reducing per-module imports.
In one heavy Safari-on-Mac scenario, this brought performance from roughly 19 Hz to 44 Hz.
These are measurements from this project, not a universal WebAssembly optimization recipe. On iPhone, the same changes did not fix the main slowdown.
The unresolved iPhone problem
The heavy scene runs at 16–22 Hz on the phone.
Remove all draw calls, while keeping uploads and the rest of the emulator running, and it reaches approximately 59–63 Hz. That points toward the drawing path, but does not by itself distinguish GPU execution, command submission, and synchronization.
To narrow it down, we recorded the WebGL calls for one frame and built a standalone replay page.
The recording contains:
- 8,711 WebGL calls.
- 345 draws: 343 TRIANGLES draws and two TRIANGLE_STRIP draws.
The standalone page replays that frame at approximately 59 Hz on the same phone, even with 9 ms of artificial CPU work added per frame.
Adding a large WASM memory and tens of thousands of live WASM instances does not reproduce the real emulator's slowdown either.
But the recording repeats a single frame. In the real game, texture contents change, render targets are sampled by later draws, and dependencies carry across frames. The replay may be missing an important part of that workload.
The open question is: what needs to change in the replay to make it representative of the live emulator?
The replay is in web/probe-glreplay.html, with the recording in web/glframe.json. Detailed measurements and rejected experiments are documented in the repository.
What two days actually produced
A working browser prototype, with a new recompilation path, graphics, audio, and controls. It can already run games, but it has a long way to go before becoming a mature emulator.
AI made it possible to attempt a volume of changes I would not otherwise have taken on at this pace. A substantial part of the work was verification: does the game boot, does it render correctly, what does a timing number actually measure, and did the latest change improve anything?
The useful result is that the original idea can now be tested on real hardware. Instead of asking whether it is possible at all, I have a running port and specific problems to investigate.
Thanks to the Panda3DS and Dynarmic developers for the foundations of this experiment. The project is GPL-3.0-or-later and is not affiliated with Nintendo or Apple.
If you have profiled WebGL2 on iOS, or seen a similar gap between a captured-frame replay and a live application, I'd love to compare notes.
來源:DEV Community · dev.to
