← Back to context

Comment by DonHopkins

6 hours ago

I've been porting Micropolis (SimCity Classic) to WASM / WebGPU / Svelte 5. Emscripten + Embind compile the C++ engine and glue it to TypeScript/Svelte/Runes/Reactivity; TypeScript owns UI, rendering, and callback handlers.

I agree with the article's main lessons: wasm32 pointer size, don't serialize structs with pointers, debug native 32-bit when you can, WebGL/WebGPU is stricter than desktop GL, Emscripten export flags still bite. I hit some of the same categories; the parts that were actually tricky for Micropolis are below.

Svelte 5 runes ($state, $derived, etc.) work in plain .ts modules, not just .svelte templates. That matters because the WASM bridge is a reactive module the HUD, command bus, and Vitest all import -- not a component-only trick. The file has to be MicropolisReactive.svelte.ts so runes compile under the same Vite/SvelteKit pipeline as the app; plain .ts breaks in Node with "$state is not defined".

Embind API surface -- what to expose and what to leave out:

https://github.com/SimHacker/MicropolisCore/blob/main/packag...

  // This file uses emscripten's embind to bind C++ classes,
  // C structures, functions, enums, and contents into JavaScript,
  // so you can even subclass C++ classes in JavaScript,
  // for implementing plugins and user interfaces.
  //
  // Wrapping the entire Micropolis class from the Micropolis (open-source
  // version of SimCity) code into Emscripten for JavaScript access is a
  // large and complex task, mainly due to the size and complexity of the
  // class. The class encompasses almost every aspect of the simulation,
  // including map generation, simulation logic, user interface
  // interactions, and more.

The comments in that file go on to describe the strategy for wrapping: Core Simulation Logic, Memory and Performance Considerations, Direct Memory Access, User Interface and Rendering, Callbacks and Interactivity, and Optimizations.

The engine callback virtual interface bridged C++ to JS via JSCallback:

https://github.com/SimHacker/MicropolisCore/blob/main/packag...

In the old NeWS/Hyperlook, TCL/Tk/X11, SWIG/Python/PyGTK, and SWIG/Python/TurboGears/AMF/Flash versions, this callback interface used to be a stringly typed general purpose event callback interface, which I tightened up into a strict C++ interface and corresponding typescript interface, so embind could help me integrate it safely and cleanly with TypeScript and Svelte Runes.

TypeScript handlers that update rune-backed state (sendMessage, didTool, budget hooks, etc.):

https://github.com/SimHacker/MicropolisCore/blob/main/apps/m...

Simulator attach/detach, singleton engine load, wiring JSCallback into Micropolis:

https://github.com/SimHacker/MicropolisCore/blob/main/apps/m...

The pattern: C++ fires callbacks with enough context for the UI; TS updates $state; components read micropolisReactive (peek / poke / memory / getSnapshot) instead of calling Embind or touching HEAP* directly. That is where the rubber hits the road for interactivity.

Heap access is its own footgun. Emscripten may expose Module.wasmMemory, HEAPU16, or neither until init; some getters throw if you read too early. Centralized helper:

https://github.com/SimHacker/MicropolisCore/blob/main/apps/m...

Bridge design, Vitest against real WASM, teardown order with Embind lifetimes:

https://github.com/SimHacker/MicropolisCore/blob/main/docume...

Map rendering: WebGPU tile renderer with canvas fallback (legacy WebGL frozen, now reimplementing in WebGPU). The renderer reads 16 bit flags + tile indices from direct simulator memory views into WASM linear memory (mapData / mopData), not per-frame Embind copies.

https://github.com/SimHacker/MicropolisCore/blob/main/packag...

https://github.com/SimHacker/MicropolisCore/blob/main/docume...

City saves are a defined binary format (.cty), not fwrite of engine structs. Live map data is views into WASM linear memory (mapData / mopData), not embedded native pointers -- same idea as the article's side-table fix, but that is how this codebase is already structured.

Why I find this stack interesting: original SimCity engine lineage, narrow Embind surface on purpose, reactive TS facade so automation and UI share one sim without reviving the old Python/SWIG/pyGTK path. Sprites (trains, choppers, generic orange monsters wrecking chaos and havoc -- definitely not Godzilla [TM], but possibly Trump adjacent) simulate in C++; compositing them in the WebGPU path is still work in progress.

The WebGPU renderer is being built as a general stack with pluggable layers, including Sims content rendering (characters, animations, terrain, objects, walls, floors, ui effects, etc).

Character animation demo:

https://vitamoo.space

VitaMoo code:

https://github.com/SimHacker/MicropolisCore/tree/main/packag...

Unified WebGPU Renderer:

https://github.com/SimHacker/MicropolisCore/blob/main/docume...

Render Core Package:

https://github.com/SimHacker/MicropolisCore/blob/main/docume...

Renderer Plugin Roadmap:

https://github.com/SimHacker/MicropolisCore/blob/main/docume...

Live Micropolis tile renderer and simulator demo (no other ui yet, work in progress):

https://micropolisweb.com

Demo of the simulator, cellular automata, and tile engine to Jerry Martin's music:

https://www.youtube.com/watch?v=319i7slXcbI

Repo:

https://github.com/SimHacker/MicropolisCore