r/EmuDev • u/YuryGagarin • 1h ago
Question WebGL2 emulator on iPhone: ~20 Hz in-game, but a recorded frame replays at 59 Hz — what am I missing?
AI disclosure: most of the new port code was written by AI, with me driving development and testing. The existing emulator and CPU recompiler are Panda3DS and Dynarmic. AI also helped write this post.
I'm working on a browser port of Panda3DS: guest ARM → WebAssembly at runtime, graphics through WebGL2. It also runs in an iPhone WKWebView without a JIT entitlement. I've hit a performance problem that I haven't managed to reproduce outside the emulator.
Environment: iPhone Air, iOS 27.0.1, Low Power Mode off, thermal state nominal during measurement. The rendering path is WebGL2 through ANGLE/Metal in Safari/WKWebView.
A heavy scene in a commercial 3DS game runs at 16–22 Hz on the phone. Here's what I've tried:
• Removing all draw calls, while keeping uploads and the rest of the emulator running, brings it to 59–63 Hz. This points to the drawing path, though it doesn't isolate GPU work from submission or synchronization.
• Recording one frame's WebGL calls and replaying them in a standalone page gives 59 Hz on the same phone, even with 9 ms of added CPU work per frame. The recording contains 8,711 calls and 345 draws: 343 TRIANGLES draws and 2 TRIANGLE_STRIP draws, no TRIANGLE_FAN.
• Adding 256 MB of WASM memory, heap-view uploads, and up to 40,000 live WASM instances to the replay doesn't reproduce the slowdown.
The replay repeats a single captured frame. Suspected differences in the real workload (not established causes):
• Texture contents change every frame, with around 22 texture uploads.
• Render targets are drawn to and then sampled by later draws.
• There is substantial framebuffer churn: 132 bindFramebuffer and 117 framebufferTexture2D calls per frame. The capture includes these calls, but repeating one frame may not reproduce the dependencies across changing frames.
How would you profile this on iOS to distinguish GPU work, WebGL submission overhead, and synchronization? Have you encountered a similar gap between a captured-frame replay and a live emulator? What would you add to the replay to make it representative?
The replay is web/probe-glreplay.html, with the capture in web/glframe.json. Measurements and experiments are in docs/webkit-instances-and-batching.md.
Source: https://github.com/gagarinyury/panda3ds-wasm
Browser demo: https://panda.profy.top — bring your own game dump; no game data hosted.
Happy to run additional experiments or collect traces if someone can suggest a useful next step.
