Building ZapEngine: From Plant Simulator to WebGPU Game Engine in One Week
By Andrei Roman
Principal Architect, The Foundry
Monday: Built a plant simulator to teach my son biology.
Tuesday: Rebuilt it as tile-based tree with particle sap flow. Hit PixiJS performance ceiling.
Wednesday: Ported iOS game to WebGPU + WASM in one day.
Thursday: Extracted game engine, built working examples.
Friday: Put finishing touches on the examples. Catching my breath to write about it. This all happened in one week. Here is what happened.
Day 1: Plant Simulator (Educational Tool)
Started with educational goal. My 12-year-old son studies biology. We built a plant growth simulator. Four quadrants: air (above ground), soil (below ground), world map, evolution UI. TypeScript, PixiJS, real photosynthesis equations. Calvin cycle inputs. N-P-K nutrient tracking. Water transpiration. Not game theater. Actual biology.
Fixed seedling starvation (photosynthesis vs respiration balance). Implemented auto-zoom camera (macro lens for seedlings, wide angle for trees). Root rendering with proportional recursive branching. Seasonal phenology (autumn leaf drop, winter dormancy, spring regrowth).
Worked. Educational value delivered. But wait, he wanted something else.
Day 2: Tile-Based Tree (Performance Problem)
Rebuilt as tile-based system. Tree trunk and branches as grid tiles. Particle system for sap flow. Visualize water moving from roots to leaves. Xylem transport. Phloem distribution.
Problem: Slow. PixiJS struggled with particle count. 1000 particles at 60fps was choppy. This is where the architectural constraint became visible. JavaScript on main thread cannot handle heavy simulation.
Diagnosis: PixiJS is presentation layer. Cannot fix simulation performance by changing graphics library. Need to move simulation off main thread.
Day 3: The Constraint Becomes Opportunity
I had been thinking about this for a while. ZapZap: iOS puzzle game built with Swift and Metal. High-speed connection puzzle. Rotate tiles to create conductive paths. Neon glowing aesthetics. HDR effects on XDR displays. Fluid dynamics simulation. AI bot opponents.
Challenge: Native game uses multi-threading and direct GPU access. JavaScript is single-threaded. Standard web port would be sluggish clone.
Decision: Port it properly. Treat browser as high-performance operating system. 120 FPS or bust. BUT HOW?
ZapZap Native (The Architecture)
Built host-guest engine in one day. Noon to evening. Gemini for planning. Claude Code for execution.
The Guest (Simulation Core): Rust compiled to WASM. ECS architecture. Data arrays (Vec<Rotation>), not objects (class Tile). Optimizes CPU cache usage. Eliminates garbage collection pauses. Runs game logic (AI, connection checks, gravity). Isolated in Web Worker. Heavy calculations never freeze UI.
The Bridge (Zero-Copy Sync): SharedArrayBuffer. Rust writes directly to RAM block. JavaScript main thread reads same block. No JSON serialization. Synchronization takes 0.0ms.
The Host (Renderer): Strategy pattern for device spectrum. Primary: WebGPU with compute shaders and storage buffers. HDR/EDR support via rgba16float canvas. Pixel values greater than 1.0 replicate blinding neon glow on XDR displays. 10,000 sprites in single draw call. Fallback: Canvas 2D for browsers without WebGPU (Firefox, older hardware). Reads same Rust memory. Standard SDR appearance but perfect gameplay.
Deployed to AWS CloudFront + S3 via CDK. Injected Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers to enable SharedArrayBuffer (browsers block by default to prevent Spectre attacks).
Result: https://zapzap.bijup.com
Works on phones. With HDR. 60 FPS even on my iPhone 13 Pro Max.
Day 4: ZapEngine (The Extraction)
Realized the engine is reusable. Extracted ZapZap game logic from rendering engine. Created headless, data-oriented 2D game engine for modern web.
Architecture:
Core: zap-engine crate. Pure Rust. No WASM dependencies. Game trait, EngineContext, GameConfig. Components (Entity, Sprite, Layer, Emitter, Mesh). Systems (Physics via rapier2d, Effects, Rendering, Lighting). Renderer (RenderInstance, Camera2D, SDF buffers). Bridge (SharedArrayBuffer protocol). Input (InputQueue, InputEvent). Assets (Manifest parser, SpriteRegistry).
Bridge: zap-web crate. WASM glue via wasm-bindgen. GameRunner connects Rust to JavaScript.
Runtime: TypeScript engine. WebGPU + Canvas2D renderers with shaders. Web Worker (loads WASM, runs game loop). React integration (useZapEngine hook). Asset loading (Manifest, normal maps). Audio (SoundManager).
Examples: Basic demo (spinning sprite). Physics playground (Angry Birds-style slingshot + tower). Chemistry lab (SDF molecule visualization with raymarched spheres). ZapZap mini (circuit puzzle with dynamic lighting and normal maps).
Tools: Asset manifest generator. Normal map generator (Sobel operator). Font atlas generator (browser-based). Check it out on github.
The Technical Innovations
Host-Guest Architecture: Simulation (Rust/WASM in Web Worker) fully decoupled from presentation (WebGPU/Canvas2D on main thread). Zero jank. 60+ FPS.
SharedArrayBuffer Protocol: 10-float header + instance data + effects data. Zero-copy synchronization via Atomics. Main thread reads at 120Hz. Worker writes at 120Hz. Double buffering prevents tearing.
Progressive Fallback: WebGPU HDR-EDR (rgba16float, display-p3, extended tone mapping) → WebGPU HDR-sRGB (rgba16float, no HDR features) → WebGPU SDR (bgra8unorm, basic sRGB) → Canvas 2D (software rendering). Probe test on disposable canvas prevents context locking.
Data Transport Fallback: SharedArrayBuffer (zero-copy, Atomics sync) → postMessage (structured clone copies, 8-50 KB per frame). Buffer layout identical. Only transport mechanism differs.
Fat Entity System: Single Entity struct with optional components (Position, Sprite, PhysicsBody, Emitter, Mesh). Simpler than full ECS for educational and casual games. Memory layout compatible with SharedArrayBuffer (repr(C)).
Hybrid Rendering: Sprites (2D textured quads), VFX (additive blending for HDR glows), SDF Meshes (raymarched signed distance fields for mathematically perfect 3D shapes). Three paradigms in single render pass.
Physics Integration: rapier2d in Rust. Physics steps in Worker. Engine syncs Rapier positions to render buffer automatically. Physics determines position. Game logic applies forces, not coordinates.
What This Demonstrates
AI as architecture partner. Gemini planned the host-guest separation. Claude Code executed the implementation. One day from iOS game to web engine.
I didn't even know how things work but I had Gemini explain.
Why Rust? Baked-in memory safety. Browser as OS. WebGPU provides Metal-equivalent graphics API. WebAssembly provides native-performance computation. SharedArrayBuffer provides zero-copy IPC. Web Workers provide true parallelism. The browser is not a toy runtime. It is a high-performance operating system.
Constraints drive architecture. PixiJS performance ceiling forced the worker split. SharedArrayBuffer requirement forced the strict COOP/COEP headers. HDR support forced the progressive fallback chain. Each constraint improved the final design.
Extract early. ZapZap Native worked Wednesday evening. ZapEngine extracted by Friday morning. Generalization cost was low because architecture was clean. Three working examples (Physics Playground, Chemistry Lab, ZapZap Mini) proved reusability immediately.
The Ryo Lu Principle
Ryo Lu, Head of Design at Cursor, posted his rules for using AI coding tools effectively. Rule one: Using Cursor well equals fast, clean code. Using it wrong equals AI spaghetti you will be cleaning up all week.
The rules: Set 5-10 clear project rules upfront. Be specific in prompts (spell out tech stack, behavior, constraints like mini spec). Work file by file (generate, test, review in small focused chunks). Write tests first, lock them, generate code until all tests pass. Always review AI output and hard-fix anything that breaks. Use @ file, @ folders, @ git to scope attention. Keep design docs and checklists in .cursor/ so agent has full context. If code is wrong, just write it yourself.
This week demonstrated those principles in practice.
Clear project rules: Host-guest architecture. Zero-copy sync. Progressive fallback. Established upfront on Wednesday planning session with Gemini.
Specific prompts: Not make me a game engine. Instead: Rust WASM module with Game trait, EngineContext dependency injection, Entity struct with repr(C) for SharedArrayBuffer compatibility, WebGPU renderer with rgba16float HDR support and Canvas 2D fallback, rapier2d physics with automatic position sync.
Work file by file: Built ZapZap Native Thursday. Extracted engine Friday. Three working examples same day. Each module isolated, tested, reviewed before moving to next.
Tests first: 70 Rust unit tests. Grid operations, state transitions, animations, physics joints, collision detection. Tests locked. Code generated until tests passed.
Review and fix: Canvas locking bug on WebGPU probe failure. Fixed manually. Told Claude Code to use probe pattern in future renderers.
Design docs in .cursor/: VISION.md, DECISIONS.md (22 ADRs), MASTERPLAN.md tracking all 6 phases. Full context available to AI at all times.
Write it yourself when wrong: SharedArrayBuffer protocol layout. Wrote the memory structure manually. AI filled in serialization code.
The Difference Between Fast Code and AI Spaghetti
AI spaghetti: Ask AI to build game engine. Receive monolithic codebase mixing simulation, rendering, input, audio. No tests. Hardcoded constants. Single-threaded. Canvas 2D only. Works on developer machine. Breaks in production.
Fast clean code: Specify host-guest architecture. Rust simulation in worker. WebGPU rendering on main thread. SharedArrayBuffer zero-copy sync. Progressive fallback (HDR → SDR → Canvas 2D). 70 unit tests. Deployed to CloudFront with COOP/COEP headers. Works on phones with HDR.
The difference is not AI skill. The difference is architecture knowledge. AI amplifies clarity. If you know what production-quality looks like (dependency injection, zero-copy IPC, progressive degradation, test coverage), AI executes correctly. If you do not know what quality looks like, AI produces garbage at scale.
Ryo Lu also posted: 16-year-olds will out-build senior engineers. True. But only if the 16-year-old understands architecture. Age does not matter. Judgment matters. Clear specifications. Knowing what good looks like. That determines output quality.
What Ships Next Week
I don't know. The candidate is Hexmanos for sure. It's also hitting js limits.
Maybe more example games. Demonstrate sprite name lookup from manifest. World coordinate conversion. Joint API (fixed, spring, revolute). Custom events (React to Rust). Prove engine works for real games.
UPDATE: deployed an examples page.
The Stack
Rust (compiled to wasm32-unknown-unknown). WebGPU via wgpu crate. rapier2d for physics. Web Workers for parallelism. SharedArrayBuffer for IPC. React for UI. TypeScript for host runtime. AWS CDK for infrastructure. Vite for dev server with COOP/COEP headers.
The Test Suite
Rust unit tests. Cover grid operations, game state transitions, animations, board generation, bonus mechanics, bot AI, visual effects, registry load, sprite lookup, custom events, joint creation (fixed, spring, revolute), collision detection.
Why This Matters
Solo developer with AI tools can ship a game engine in one week. No team. No meetings. Just focus, clarity, and iteration.
Demos: https://zapengine.bijup.com/
Discussion (0)
No comments yet. Be the first!