feat: Run on the web via a WebAssembly build of Box2D#116
Merged
Conversation
This was referenced Jul 19, 2026
erickzanardo
approved these changes
Jul 19, 2026
The same public API now works in the browser under both dart2js and dart2wasm. Box2D and a pointer-based C shim (native/wasm/f2d_shim.c) are compiled with Emscripten into a standalone ~220 KB box2d.wasm, shipped inside the package and loaded by initializeForge2D(). The shim flattens every struct-passing call into scalars and pointers (the wasm C ABI cannot pass structs by value across the JS boundary), copies events into float64 record buffers, and routes the synchronous callbacks (queries, custom filter, pre-solve, debug draw) through fixed host imports supplied at instantiation, so no function tables are needed. On the Dart side, RawBox2DWasm implements the backend seam over dart:js_interop with a scratch arena for arguments and results, and the conditional export picks it up automatically on the web. Dart web apps need no setup; Flutter web apps copy the module once with dart run forge2d:setup_web, or pass wasmUri to initializeForge2D. The API test suite now runs on vm, chrome/dart2js, and chrome/dart2wasm (new CI jobs), and build-wasm.yml rebuilds the module with a pinned emsdk to verify the committed artifact. Along the way chain creation gained proper validation: Box2D requires at least four points, which the native release build silently ignored with asserts compiled out.
Flutter web apps needed a manual setup step to host box2d.wasm. Declaring the module as a package asset makes Flutter bundle it into every consuming app, and the loader now checks the bundled asset path, so the same zero setup applies to Dart web and Flutter web alike. Verified with a real Flutter web app depending on forge2d: it builds, bundles the module, and runs a simulation with no configuration. Also loosens the build hook dependency minimums (hooks ^2.0.0, native_toolchain_c ^0.19.0): the newest patch releases require meta 1.19 while the Flutter SDK pins meta 1.18, which made forge2d unresolvable in Flutter apps altogether.
Shape.geometry returns the sealed ShapeGeometry read from the simulation: circles, capsules, segments, polygons with their convex hull vertices and rounding radius, and chain segments as the segment they collide with. Renderers like flame_forge2d's BodyComponent need this to draw fixtures. Implemented across both backends (new f2d shim exports for the wasm side) and covered by a round-trip test for every kind on vm, dart2js, and dart2wasm. Also finishes the abbreviation cleanup in the wasm backend and the C shim, which the earlier rename missed because positional parameter names are not checked against the interface.
- Mutation safety during step: destroying bodies, shapes, chains, and joints from inside a step (collision callbacks) is deferred until the step returns, restoring the old engine's semantics (compare 0704959); destroy is idempotent, and mid-step creates throw a catchable StateError instead of aborting the process in native code. - World.destroy unregisters the custom filter and pre-solve callbacks, so their closures no longer leak in the backend registries or fire for a recycled world slot. - Body.destroy releases the user data of chains that die with the body, tracked through a chain-to-owner registry. - The wasm backend reads ids as unsigned consistently, keeping handles from creation, queries, and events equal even after 32768 generations of slot reuse. - 64-bit filter masks with the top bit set now split exactly on the wasm seam instead of collapsing to all-ones; -1 remains the all-categories sentinel. - Strings cross into the wasm module through a growable buffer instead of a fixed 4 KB scratch slice; body names are documented as truncated to 31 bytes by Box2D itself. - The C shim heap-allocates its id arrays instead of using caller-sized stack VLAs that could overflow the 64 KB wasm stack. - createBody and createShape validate finiteness, damping, density, and radius in debug mode, restoring the old Dart-side NaN safety net. - The README migration guide documents the (0, -10) default gravity of a bare World() and the new mid-step mutation semantics.
Every public getter and setter on Body, Shape, Chain, and all eight joint types now round-trips through both backends, together with the DebugDraw default no-ops and the math conveniences. Line coverage of the VM-runnable library code rises from 77% to 96%, and the same suite runs on chrome/dart2js and chrome/dart2wasm. The tests immediately caught a real Box2D behavior worth knowing: setting fixedRotation zeroes the body's angular velocity.
spydon
enabled auto-merge (squash)
July 20, 2026 19:15
spydon
added a commit
that referenced
this pull request
Jul 20, 2026
…#117) Stacked on #116 (which is stacked on #115). ## What The old browser examples return as a modern, single-page demo gallery running on the WebAssembly backend, with a GitHub Pages deployment. **Scenes** (all draggable with the pointer via a mouse joint): - **Pyramid** and **Domino tower** (a bullet ball topples it) - **Ball cage** (zero gravity, kinematic spinning paddle, chain-loop cage) - **Circle stress** (320 balls in a container) - **Blob** (a soft body of distance-joint springs) - **Bridge** (revolute-joint planks under load) - **Racer** (wheel-joint car on rolling chain terrain, arrow keys to drive, following camera) **Presentation**: dark theme, palette-tinted shapes through Box2D v3's per-shape custom colors, pill navigation with URL-hash deep links, hint bubble per scene, device-pixel-ratio aware canvas, fixed-timestep loop with an accumulator. Rendering goes through the public `DebugDraw` interface, so the gallery doubles as a demonstration of it. **Deploy**: `deploy-examples.yml` builds with `build_runner --release` and publishes to GitHub Pages on pushes to main. One-time repo setup: enable the GitHub Actions source under Settings > Pages. ## Testing Built and verified locally in headless Chrome: all scenes render and simulate on the wasm backend (screenshots in the PR conversation), `melos run format-check` and `dart analyze --fatal-infos` green.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #115.
What
Web support for the new native-backed forge2d, as designed in the migration plan: the same public API now runs in the browser under both dart2js and dart2wasm, against a bundled WebAssembly build of Box2D v3.1.1.
How
native/wasm/f2d_shim.c): the wasm C ABI passes structs indirectly, so raw Box2D exports are impractical to call from JS. The shim wraps every function the backend needs with flat scalar/pointer signatures (242 exports), copies polled events into float64 record buffers, and routes the synchronous callbacks (ray casts, overlap queries, custom filter, pre-solve, debug draw) through 13 fixed host imports supplied at instantiation, so no function tables or runtime table growth are needed.tool/build_wasm.sh): Emscripten standalone wasm (no JS glue),-O3 -msimd128 -msse2 -DNDEBUG, ~220 KB, committed atlib/src/backend/wasm/box2d.wasmand shipped in the package.build-wasm.ymlrebuilds it with a pinned emsdk (4.0.15) and fails when the committed artifact drifts.raw_box2d_wasm.dart+wasm/wasm_runtime.dart): implements the backend seam overdart:js_interoponly (works on both web compilers), with a fixed scratch arena for arguments/results, a growable bulk buffer for events and vertex lists, WASI stubs, and memory-growth-safe heap views. The conditional export inbackend.dartselects it automatically on the web.initializeForge2D()fetches the module from the package asset path (Dart web tooling serves it automatically), falling back tobox2d.wasmnext to the page. Flutter web apps copy it there once withdart run forge2d:setup_web, or passwasmUri:explicitly.Notable fixes found by the web tests
b2WorldId.index1vsb2ShapeId.world0).Testing
dart test(VM/FFI),dart test -p chrome(dart2js),dart test -p chrome -c dart2wasm; all 84 tests pass on each locally, and cicd.yml gains the two web jobs.build-wasm.ymlverifies artifact reproducibility.