The webview console. WasmCart's SDL3 console shell — but instead of running carts through WAMR, Wasm5 plays each cart's web build inside a WKWebView overlaid on the SDL window: the real Canvas2D + browser-WebAssembly stack, at full fidelity. SDL3 keystrokes are forwarded into the cart as synthetic DOM KeyboardEvents.
Same picker UI as WasmCart. Same carts. Different runtime: a browser, not an interpreter.
Wasm5 is the sister console to WasmCart. The two share one cartridge-picker shell — a SuperBox64Kit SKScene compiled as Embedded Swift and rendered through SDL3 — but they diverge entirely on how they play a cart:
| how a cart runs | renderer | |
|---|---|---|
| WasmCart | the cart's .wasm/.aot through WAMR (interpreter or wamrc AOT) |
SDL3 + Metal (native KitABI backend) |
| Wasm5 | the cart's WEB build (index.html + runtime.js + .wasm + assets) in a WKWebView |
Canvas2D inside WebKit (browser stack) |
So a Wasm5 cart is not a re-implementation of the browser — it is the browser. The cart loads the exact runtime.js Canvas2D host from WasmKit, fulfilling the same KitABI imports it would on a real web page: resolution-independent SVG, color emoji, audio, 60 fps. The cart's WEB build is served over a localhost HTTP server (python3 -m http.server on 127.0.0.1:52900) so fetch() and WebAssembly.instantiate see correct MIME types and no file:// CORS issues.
The shell UI still draws with SDL3+Metal; only the cart pane is WebKit.
When WebKit is linked into the process, SDL3's macOS keyboard path stops working — the mouse still reaches SDL's content view, but keystrokes never do. Wasm5's entire input design is the workaround:
- A single app-wide
NSEventlocal key monitor intercepts every key-down/up. - No cart up (shell): nav keys are translated to SFML codes and pushed straight into the kit's event queue (bypassing SDL's broken path).
- Cart up (webview): keys are injected into the cart's JS as synthetic
KeyboardEvents viaevaluateJavaScript— dispatched on bothwindowanddocument, focus-independent, becauseruntime.jslistens one.code. CTRL+ESCejects the webview back to the shell;CMD+Qquits. Both are grabbed by the monitor even while the webview owns the keyboard.
The host itself is platform-agnostic over a 7-function C ABI; all the AppKit/WebKit/HTTP work lives in per-OS shims (macOS Obj-C webview-shim.m, Linux WebKitGTK webview-shim-linux.c, Windows WebView2 webview-shim-win.cpp).
ONE game source (UFO-Emoji-Arcade: GameScene.swift et al.)
│
▼
┌──────────────────────────────┐
│ SuperBox64Kit │ drop-in SpriteKit for Embedded/wasm Swift
│ SpriteKit emul · KitABI │ + SDL3 backend · Box2D v3 · reSVG vector
└──────────────────────────────┘
│ compiles one tree three ways
┌───────────────────────────┼────────────────────────────────┐
▼ ▼ ▼
full-Swift wasm32 Embedded-Swift wasm32 Embedded-Swift native
(~4.18 MB) (~688 KB) (arm64 single binary)
│ │ │
▼ ▼ ▼
┌─────────────────┐ browser: WasmKit runtime.js SDL3 + Metal direct
│ WasmKit │ ←── Canvas2D host, fulfils KitABI
│ runtime.js + │ (index.html + embedded.html)
│ Canvas2D │
└─────────────────┘
│ the SAME web build is consumed by two native consoles:
│
├───────────────► ┌─────────────┐ .wasm / .aot through WAMR (interp + wamrc AOT)
│ │ WasmCart │ SDL3 + Metal, native KitABI backend
│ └─────────────┘
│
└───────────────► ┌─────────────┐ ★ YOU ARE HERE
│ Wasm5 │ SDL3 shell + WKWebView cart
└─────────────┘ real runtime.js/Canvas2D in WebKit,
SDL keystrokes → synthetic DOM KeyboardEvents
Wasm5 reuses WasmCart's vendored static libSDL3.a and the kit's build driver; it adds nothing to the cart format. A cart is just its web build — the same runtime.js + .wasm from WasmKit and the UFO-Emoji-Arcade web pipeline.
- Reuses WasmCart's SDL3 console shell verbatim — a SuperBox64Kit
SKScenerendered through SDL3, compiled as Embedded Swift — for the cartridge picker (slot labelWASMFIVE). - Plays carts in a
WKWebViewoverlaid on the SDL window: runs the cart's real web build on the genuine Canvas2D + browser-WebAssembly stack instead of WAMR. - Serves each cart over localhost (
python3 -m http.serveron127.0.0.1:52900) sofetch()/WebAssemblyload with correct MIME types and nofile://CORS issues. - Accepts a directory or a
.zip:resolveWebDirunzips (/usr/bin/unzip) and walks one folder deep to findindex.html. - SDL3 keystrokes → synthetic DOM
KeyboardEvents viaevaluateJavaScript— focus-independent, dispatched on bothwindowanddocument(runtime.jslistens one.code). - One app-wide
NSEventkey monitor, mode-switched: shell nav → SFML codes into the kit queue; cart keys → injected into the cart's JS. CTRL+ESCejects the webview back to the shell;CMD+Qquits — both grabbed by the monitor even while the webview owns the keyboard.- Carts folder auto-scanned and re-scanned (~every 180 frames); drag-and-drop a cart at any time; OPEN FILE dialog via
SDL_ShowOpenFileDialog. - Bundled vector-segment
ShellFont(10×14 line-segment glyphs) draws the menu with the same vector lettering as the games it hosts. - Cross-platform shims behind one C ABI: macOS (
WKWebView), Linux (WebKitGTK reparented into the X11 window), Windows (WebView2 child of the HWND). Linux/Windows shims present but untested on the macOS host. - Forces a regular foreground/key state on launch (
wasm5_activate) because linking WebKit/Cocoa can otherwise leave the SDL window un-activated and swallow keystrokes. - Persists the emoji-mode preference to the kit pref store (
store.tsvunderSDL_GetPrefPath) for shell parity with WasmCart — though emoji-mode has no effect on webview carts (the browser renders emoji with its own system font; the toggle is intentionally dropped here).
Wasm5 is one host in a 5-repo constellation that runs one game source through seven real build × runtime permutations. Wasm5 owns the last row.
| # | Build (wasm flavor) | Host / runtime | Renderer | Repo |
|---|---|---|---|---|
| 1 | None — Apple-native SpriteKit | Apple SpriteKit (UIKit/AppKit) | Metal + UIKit | UFO-Emoji-Arcade |
| 2 | Full-Swift wasm32-unknown-wasip1 (~4.18 MB) |
runtime.js in a browser |
Canvas2D | WasmKit |
| 3 | Embedded-Swift wasm32 (~688 KB) |
runtime-embedded-min.js in a browser |
Canvas2D | WasmKit |
| 4 | None — Embedded-Swift native (arm64) | SDL3 backend, single binary | SDL3 + Metal | SuperBox64Kit |
| 5 | .wasm cart |
WAMR interpreter (WasmCart) | SDL3 + Metal | WasmCart |
| 6 | .aot cart (wamrc AOT) |
WAMR AOT (WasmCart) | SDL3 + Metal | WasmCart |
| 7 | The web build (unchanged) | WKWebView (Wasm5) |
Canvas2D in WebKit + SDL3/Metal shell | Wasm5 (this repo) |
Permutation 7 is "the real browser stack inside the native console." The cart binary is byte-for-byte the same web build that ships at UFO-Emoji-Arcade's site — Wasm5 just hosts it locally inside WebKit instead of on a web page.
- macOS (arm64). The shim is compiled with the real macOS SDK (full Cocoa/WebKit), not Embedded Swift.
- Sibling repos checked out beside this one:
../SuperBox64Kit— providesnative/build-native-game.sh(the compiler driver), theKit/kitHostAPI, and the Embedded-Swift framework modules.../WasmCart— must be built first, so its vendored static archive../WasmCart/vendor/libSDL3.aexists.build.shaborts otherwise.
- Swift toolchain
swift-6.3.2-RELEASE.xctoolchain(pinned in the kit's build script; override withSWIFTC=). python3and/usr/bin/unziponPATH(runtime, for serving / unzipping carts).
# 1) build WasmCart first (produces ../WasmCart/vendor/libSDL3.a)
cd ../WasmCart && ./build.sh
# 2) build Wasm5
cd ../Wasm5 && ./build.shWhat build.sh does:
# clang-compile the Cocoa/WebKit shim with the real SDK (NOT Embedded Swift)
clang -c -fobjc-arc -O2 webview-shim.m -o webview-shim.o
# then invoke the kit's native game builder with the shell as the game source
GAME_SRC="$PWD/Sources" \
GAME_MAIN="$PWD/host-main.swift" \
OUT="$PWD/Wasm5" \
SDL_STATIC_A="../WasmCart/vendor/libSDL3.a" \
EXTRA_OBJS="$PWD/webview-shim.o" \
EXTRA_LIBS="-framework WebKit -framework Cocoa -liconv" \
../SuperBox64Kit/native/build-native-game.shKITpath is overridable (KIT=/path/to/SuperBox64Kit ./build.sh); defaults to../SuperBox64Kit.- The kit build targets
arm64-apple-macos14, Embedded Swift-wmo -Osize, and pinsSWIFTCto the 6.3.2 toolchain.
./Wasm5Put web-build carts in ./carts/ — a directory or a .zip containing index.html. Override the carts directory with WASMCART_CARTS:
WASMCART_CARTS=/path/to/my/carts ./Wasm5A sample cart is bundled:
carts/ufoemoji.zip(the UFO Emoji web build —index.html+runtime.js+ufoemoji.wasm+ assets, plus an embeddedufoemoji-embedded.wasmvariant; 626×352 logical canvas). The shim unzips it to a temp dir at runtime; only the.zipis tracked.
| Context | Key | Action |
|---|---|---|
| Shell | ↑ / ↓ |
Select cart |
| Shell | Enter / double-click |
Load selected cart |
| Shell | drop a .zip/dir |
Insert at any time |
| Shell | double-click OPEN FILE | SDL_ShowOpenFileDialog |
| Cart | CTRL+ESC |
Eject back to the shell |
| Anywhere | CMD+Q |
Quit |
The Embedded-Swift host declares these via @_silgen_name in host-main.swift; each per-OS shim implements them.
void wasm5_play_cart(void *nswindow, const char *cartPath); // resolve+serve cart, attach WKWebView
void wasm5_eject(void); // remove webview, kill http server
void wasm5_activate(void *nswindow); // foreground + key the window
void wasm5_pump_runloop(double seconds); // drain NSApp event queue (keys → webview)
int wasm5_eject_requested(void); // polled: CTRL+ESC fired?
int wasm5_quit_requested(void); // polled: CMD+Q fired?
void wasm5_install_keymonitor(void); // install the app-wide NSEvent monitorThe host exports one callback the monitor calls back into:
@_cdecl("wasm5_push_key")
func wasm5_push_key(_ sfcode: Int32) // feeds Kit.shared.pushEvent (shell nav, bypassing SDL)@main enum Main boots the kit, loads Apple Color Emoji + the pref store, presents ShellScene in an SKView, activates/keys the SDL window, installs the key monitor, then runs:
- Game thread — ticks + presents the shell (
shellTick+kitHostPresent) only when no cart is up. - Main thread — the OS event pump, cart insert/eject, and webview management.
The critical rule: while a cart is up, the main loop must NOT pump SDL events. SDL's pump dequeues the key NSEvents and re-focuses its own content view, stealing the keyboard from the webview. Instead the host calls wasm5_pump_runloop, which uses NSApp nextEventMatchingMask/sendEvent (not bare NSRunLoop runMode) to drain the OS event queue so events reach the webview first responder — and the app doesn't beachball.
final class ShellScene: SKScene—didMove/keyDown/mouseDown/update. Scans the carts dir, draws a pick-and-play menu, auto-rescans every ~180 frames.enum ShellFont— 10×14 line-segment glyphs (A–Z 0–9 - .) withdraw/displayName/strokes.
kitHostInit, kitHostPump, kitHostPresent, kitEscapeReserved, kitEscapePressed, kitDroppedFile, and Kit.shared.{ window, pushEvent, setEmojiFont, setEmojiMode, emojiMode, storePath, loadStore, saveStore, storeGet, storeSet }.
webview-shim.m installs one NSEvent local monitor and routes by mode:
key event
│
├─ CMD+Q ────────────────────────► gQuitRequested = 1 (consume)
│
├─ webview up?
│ ├─ CTRL+ESC ───────────────► gEjectRequested = 1 (consume)
│ └─ else ───────────────────► forwardKeyToWebview() (consume)
│ evaluateJavaScript:
│ new KeyboardEvent('keydown'|'keyup', { code, key, bubbles, cancelable })
│ dispatched on window AND document (runtime.js reads e.code)
│
└─ shell (no cart)
└─ macToSf(keyCode) ────────► wasm5_push_key(sfcode) (consume nav keys)
macToDomCode maps macOS virtual key codes to DOM KeyboardEvent.code strings: arrows → ArrowLeft/Right/Up/Down, Space/Enter/NumpadEnter/Escape/Tab/Backspace/ShiftLeft, letters → KeyA…KeyZ, digits → Digit0…Digit9. The injection is focus-independent — runtime.js listens on global keydown/keyup, so no first-responder dance is needed for the cart to feel the keys.
The host talks to the webview only through the 7-function C ABI, so porting is "write another shim":
| OS | Shim | Webview | Status |
|---|---|---|---|
| macOS | webview-shim.m |
WKWebView added to the SDL window's contentView; /usr/bin/unzip; ATS exempted for localhost via Info.plist |
working |
| Linux | webview-shim-linux.c |
borderless GTK3 window holding a WebKitWebView, XReparentWindow'd into the SDL X11 window |
untested, X11 only |
| Windows | webview-shim-win.cpp |
WebView2 control as a child of the SDL HWND; cart extracted via PowerShell Expand-Archive |
untested |
Info.plist declares NSAppTransportSecurity → NSAllowsLocalNetworking = true so the WKWebView can load the cart over http://127.0.0.1 despite App Transport Security.
- SDL keyboard is broken with WebKit linked. Do not assume SDL key events work; the whole monitor-and-forward design exists for this. Mouse works; keys don't.
- Don't pump SDL while a cart is up — it steals the keyboard from the webview. Use
wasm5_pump_runloop. wasm5_activateis required at launch — linking WebKit/Cocoa can leaveNSApplicationun-activated (window visible but not key), which swallows every keystroke in the shell.- The shell still carries WasmCart naming. The carts env var read at runtime is
WASMCART_CARTS(not a Wasm5-specific name), and the pref-store key is literallyWasmCart.emojiMode. The cartridge slot label isWASMFIVE. - EMOJI mode is a no-op for webview carts —
emojiRectis.zero; the browser renders emoji with the system font, so the kit's apple/png/noto mode has no effect. - Build ordering matters — build WasmCart first, or
build.shaborts withERROR: build WasmCart first (need .../vendor/libSDL3.a). - DEBUG logging is left in by design —
webview-shim.m(WASM5 MONITOR/FORWARD) andhost-main.swift(WASM5 GAME-THREAD/SDL-KEYDOWN/…) are noisy on purpose. main.swift.standalone-bakis a superseded earlier design: a plain CLI that took a cart arg (orCARTRIDGEenv), served it on port 52850, and showed it in a standaloneWKWebViewwindow. The current architecture serves on 52900. Gitignored (*.standalone-bak).- Embedded Swift constraints propagate from the kit: the build sed-strips
@MainActor/@preconcurrencybefore compiling — keep that in mind when editinghost-main.swiftorShellScene.swift.
Wasm5/
├── host-main.swift Embedded-Swift host: @main, C-ABI bridge, threads, cart slot
├── Sources/
│ └── ShellScene.swift the console shell SKScene + ShellFont segment glyphs
├── webview-shim.m macOS Cocoa/WebKit shim (the C ABI; real SDK, not Embedded)
├── webview-shim-linux.c Linux WebKitGTK shim (untested)
├── webview-shim-win.cpp Windows WebView2 shim (untested)
├── build.sh build driver (needs ../SuperBox64Kit and ../WasmCart)
├── Info.plist CFBundleName Wasm5, com.superbox64.wasm5, ATS localhost
├── main.swift.standalone-bak superseded standalone CLI (gitignored)
└── carts/
└── ufoemoji.zip bundled sample cart (UFO Emoji web build); extracted copies gitignored
Part of the SuperBox64 / UFO Emoji constellation — one game source, seven build × runtime permutations:
- SuperBox64Kit — the kit. A Swift reimplementation of Apple's SpriteKit that compiles one game-source tree three ways (browser wasm, wasm cartridge, native binary), bundling Box2D v3, an SDL3 backend, and a reSVG/nanosvg vector rasterizer. Owns SpriteKit emulation, the KitABI, Box2D-backed physics, the SDL3 backend, reSVG, and the drop-in story. Wasm5's shell is a kit
SKScene, andbuild.shcalls the kit'snative/build-native-game.sh. - WasmKit — the web runtime. The hand-rolled, no-Emscripten JavaScript host (
runtime.js) that renders WASI/Embedded-Swift wasm games on Canvas2D and fulfils the KitABI env imports in the browser. This is the exact runtime Wasm5's WKWebView carts run — Wasm5 hosts it locally instead of on a web page. - WasmCart — the native console. Wasm5's sister/twin: same SDL3 shell, but it plays
.wasm/.aotcarts through WAMR (interpreter + wamrc AOT) — no browser, no JavaScript. Wasm5 hard-depends on its vendoredvendor/libSDL3.a. - UFO-Emoji-Arcade — the flagship game / demo. Todd Bruss' App Store arcade game UFO Emoji: one 100% Swift SpriteKit codebase shipped to iOS natively, to the browser via WebAssembly, and to native consoles — all from the same unchanged sources. Its web build is Wasm5's bundled sample cart.
- Author: Todd Bruss
- Console & game: part of the SuperBox64 / UFO Emoji project
- Built on SuperBox64Kit (SpriteKit-for-wasm reimplementation, SDL3 backend, Box2D v3, reSVG), the WasmKit
runtime.jsCanvas2D host, and WasmCart's vendored static SDL3 - Repo:
https://github.com/SuperBox64/Wasm5
No LICENSE file is currently present in this repository (or in the sibling consoles). All rights reserved by the author pending an explicit license. Add a LICENSE file to set terms.