Uh oh!
There was an error while loading. Please reload this page.
feat!: Replace the pure-Dart engine with native Box2D v3 bindings - #115
Merged
Conversation
…ndings Vendors the Box2D v3.1.1 C sources under packages/forge2d/third_party/box2d (refreshable with tool/vendor_box2d.sh), compiles them with a native assets build hook (hook/build.dart, native_toolchain_c), and generates dart:ffi @Native bindings with ffigen (standalone tool package in tool/ffigen since ffigen and melos have conflicting cli_util constraints). A smoke test proves the end-to-end pipeline: build hook, dynamic library, bindings, simulation. Workspace SDK constraint is bumped to ^3.12.0.
The native assets build now runs in the test job, so the matrix covers the three desktop toolchains (gcc/clang, Xcode clang, MSVC). A new ffigen-check job regenerates the FFI bindings and fails if the committed file drifts from the vendored headers.
Deletes the ported Box2D 2.x implementation (collision, dynamics, particle system, callbacks, browser helpers) along with the old examples, benchmark harness, and mock-based tests. Lands the platform backend seam: a primitives-only RawBox2D interface with a dart:ffi implementation and an UnsupportedError stub selected by conditional export, plus initializeForge2D() (async, a no-op on native) so the same public API can later be backed by a WebAssembly build on the web. Adds the first slice of the new v3-shaped public API: Rot, Transform, Aabb, BodyType/ShapeType/JointType, shape geometry classes, and def classes mirroring the native defaults. BREAKING CHANGE: the entire forge2d 0.14 API is replaced. The particle system is removed. Web support is temporarily unavailable and returns in an upcoming release via a WebAssembly backend.
Idiomatic wrappers over the native handles: worlds are created from WorldDef/gravity, bodies and shapes from their defs with native default values, and all wrappers are cheap value-like handles with equality over their ids. User data lives in Dart-side registries keyed by id, so the native userData pointers stay untouched. Explicit destroy() everywhere, matching Box2D v3 semantics. Includes a runnable console example (example/bin/hello_world.dart) and tests covering body/shape/chain lifecycle, def round-trips, native defaults, mass properties, forces and impulses.
Distance, filter, motor, mouse, prismatic, revolute, weld, and wheel joints, each with a def class carrying the native defaults and typed accessors on the wrapper. The Joint base class exposes the shared API (bodies, anchors, collideConnected, constraint force/torque, user data, destroy) and Body gains a joints getter with typed wrappers. The old gear, pulley, rope, friction, and constant-volume joints have no Box2D v3 counterpart and are gone for good.
Contact, sensor, and body-move events are polled from the world after each step as Dart-side copies, matching Box2D v3's event-driven design. Ray casts come in closest, collect-all, and callback-controlled variants, plus AABB overlap queries and radial explosions. Custom filter and pre-solve callbacks bridge into Dart through static isolate-local NativeCallables. Also renames abbreviated identifiers across the API and backend seam: definition instead of def, worldAndGeneration instead of wg.
DebugDraw is an abstract class with the b2DebugDraw toggles and one method per primitive; World.draw bridges it through static isolate-local NativeCallables. A recording implementation drives the tests. The car and domino tower demos return as runnable console examples, and ChainDef now documents Box2D v3's one-sided chain winding rule (solid side to the right of the winding direction), which the old two-sided edge shapes did not have.
…ation bench2d keeps its classic configuration (40-high pyramid, 256 warm-up plus 256 measured frames) but now runs natively: 0.51 ms/frame against 9.37 ms/frame for the pure-Dart engine on the same machine, an 18x speedup. The README describes the new bindings architecture, platform requirements, and a migration guide from forge2d 0.14. Melos scripts drop the webdev harnesses until the WebAssembly backend lands and gain an ffigen script.
MSVC only exports symbols marked dllexport, so the library built on Windows contained no visible functions. Defining box2d_EXPORTS makes B2_API expand to dllexport on MSVC and default visibility elsewhere, matching Box2D's own shared library build.
ffigen formats its output differently depending on the running SDK, which made the drift check fail on CI. Running dart format over the generated file after generation (locally, in the melos ffigen script, and in the check itself) keys the style to the package language version instead.
This was referenced Jul 19, 2026
erickzanardo
approved these changes
Jul 19, 2026
Uh oh!
There was an error while loading. Please reload this page.
spydon added a commit
that referenced
this pull request
Jul 20, 2026
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 - **C shim** (`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. - **Module build** (`tool/build_wasm.sh`): Emscripten standalone wasm (no JS glue), `-O3 -msimd128 -msse2 -DNDEBUG`, ~220 KB, committed at `lib/src/backend/wasm/box2d.wasm` and shipped in the package. `build-wasm.yml` rebuilds it with a pinned emsdk (4.0.15) and fails when the committed artifact drifts. - **Dart backend** (`raw_box2d_wasm.dart` + `wasm/wasm_runtime.dart`): implements the backend seam over `dart:js_interop` only (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 in `backend.dart` selects it automatically on the web. - **Loading**: `initializeForge2D()` fetches the module from the package asset path (Dart web tooling serves it automatically), falling back to `box2d.wasm` next to the page. Flutter web apps copy it there once with `dart run forge2d:setup_web`, or pass `wasmUri:` explicitly. ## Notable fixes found by the web tests - Chain creation now validates Box2D's requirements (at least four points, one material or one per point). The native release build compiles asserts out and silently accepted a three-point open chain; the assert-enabled wasm build hung on it. - The simulation callback dispatch had a 1-based/0-based world index mismatch (`b2WorldId.index1` vs `b2ShapeId.world0`). ## Testing - The full API suite runs on three platforms now: `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.yml` verifies artifact reproducibility.
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.
drrnbrns
commented
Jul 21, 2026
Contributor
Yikes!! 🥲 Where is the migration guide? The change log for 0.15 has this comment and link: Upgrading from 0.14 requires code changes throughout: see the Forge2D migration guide That redirects to https://docs.flame-engine.org/latest/ which doesn't contain any such guide. Maybe the URL will be there soon? |
spydon added a commit
to flame-engine/flame
that referenced
this pull request
Aug 13, 2026
Migrates `flame_forge2d` (and everything in the monorepo that uses it) to the new forge2d, the native Box2D v3.1.1 bindings that shipped in forge2d 0.15.0 through flame-engine/forge2d#115 (the native rewrite) and flame-engine/forge2d#116 (web support through a WebAssembly build). `flame_forge2d` now depends on the published `forge2d: ^0.15.1`, which includes `initializeForge2D(lengthUnitsPerMeter:)` and `Tolerances` from flame-engine/forge2d#120. `flame_forge2d` stays in the `customer_testing.dart` exclusions, because forge2d compiles Box2D from source through the Dart build hooks and so needs a C toolchain on the flutter/flutter presubmit runner; the reasoning is documented next to the exclusion. ## Core package - `Forge2DWorld` steps the world with `physicsWorld.step(dt, subStepCount: subStepCount)` and then dispatches the polled contact and sensor events through the new overridable `ContactEventsDispatcher` (replacing `WorldContactListener`, since listener interfaces no longer exist). It keeps a Dart-side `bodies` set (upstream no longer exposes one) which the gravity setter uses to wake bodies, and exposes the new query API (`castRayClosest`, `castRay`, `castRayAll`, `overlapAabb`) plus forwarding setters for `preSolveCallback` and `customFilterCallback`. - `ContactCallbacks` keeps its familiar `beginContact(Object other, Contact contact)` shape through a new lightweight flame-side `Contact` class that wraps both contact and sensor events. - `BodyComponent` renders from the new `Shape.geometry` read-back (`Circle`, `Capsule`, `Segment`, `Polygon`; chain segments arrive as `Segment`s), with `renderShape`/`renderSegment` and a new `renderCapsule`. `fixtureDefs` is replaced by `shapeSpecs` (a list of `ShapeSpec`, pairing a `ShapeGeometry` with an optional `ShapeDef`). The default `createBody()` auto-enables contact/sensor events on shapes whose body or shape userData is a `ContactCallbacks`, since the new engine only generates events for shapes that opted in. - SDK floors raised to Dart `>=3.12.0` / Flutter `>=3.44.0` (root workspace, melos bootstrap, package, and `FLUTTER_MIN_VERSION` in CI). - `Forge2DGame` awaits `initializeForge2D()` in its `onLoad`, and `Forge2DWorld` creates its physics world lazily so that this can happen first. Without it every `Forge2DGame` throws on the web, since that call is what loads the Box2D WebAssembly module. Code that creates a world outside of a `Forge2DGame` has to await it itself. - Tests rewritten against real physics worlds (mocked `Fixture`/`Contact`/`Manifold` are gone) and all goldens regenerated. `flame`'s `MultiTapDispatcher.handleTapDown` annotation changed from `@internal` to `@visibleForTesting` so the tests can use it without ignores. ## Examples and docs - The examples stories, `padracing`, and the package example are migrated. The examples for joints that no longer exist in Box2D v3 (gear, pulley, rope, friction, constant-volume) and the blob example are removed. - `doc/bridge_packages/flame_forge2d/forge2d.md` and `joints.md` are rewritten for the new API (including the new filter and wheel joints). ## Verification - `flutter test` in `packages/flame_forge2d` (45 tests, compiles native Box2D through build hooks), goldens visually inspected. - `dart analyze` clean across the whole workspace. - `flutter build web` of the examples app confirms the Box2D wasm module is bundled automatically at the package asset path. ## Notes for the forge2d review (found during this migration) - `Shape.geometry` read-back was added upstream during this work and is what makes `BodyComponent` rendering possible without a Dart-side geometry registry. - There is no upstream way to enumerate a world's bodies, hence the Dart-side set in `Forge2DWorld`. - Behavior change to be aware of: destroying a body clears its userData registries, so removed `BodyComponent`s no longer receive a final `endContact` for contacts that end due to the destruction (the old engine fired those synchronously inside destroy).
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 freeto 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.
What
Rewrites forge2d from a pure-Dart port of Box2D 2.x into Dart bindings for the native Box2D v3.1.1 C library, using Dart FFI and native assets (build hooks).
One commit per stage:
third_party/box2d, refreshable withtool/vendor_box2d.sh),hook/build.dartcompiling the 35 C sources withnative_toolchain_c(no CMake), ffigen-generated@Nativebindings (committed; regenerate viamelos ffigen), and an end-to-end smoke test. Workspace SDK bumped to^3.12.0.RawBox2Dbackend seam (dart2js-safe, no 64-bit ints, ids as two int32s, polling instead of callbacks where possible) is selected by conditional export, so a WebAssembly backend can implement the same interface in a follow-up without public API changes.initializeForge2D()is async from day one for the same reason.World,Body,Shape,Chainas cheap value-like handles over native ids, def classes mirroring the native defaults, Dart-side user data registries, explicitdestroy()semantics.NativeCallables.DebugDrawabstract class bridged tob2DebugDraw, plus ported car and domino-tower console examples.Performance
bench2d (40-high pyramid, 256 frames), same machine:
Breaking changes
The entire 0.14 API is replaced (Fixture is gone, events are polled, 13 joints become 8, particles removed). Web support is temporarily unavailable and returns via a WASM backend behind the unchanged public API. See the README migration section.
Testing
melos test: 80+ tests including physics behavior tests per feature area, green locally on Linux; CI matrix covers macOS and Windows toolchains.dart run bin/hello_world.dart/car.dart/domino_tower.dartinpackages/forge2d/exampleexercise the build hook throughdart run.melos benchmarkruns the native bench2d.