From cb13420317cc806da268267db4ac7c32412e6c5a Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 11 Aug 2026 15:46:11 +0200 Subject: [PATCH 1/5] feat: Let the length unit be set through initializeForge2D Box2D has a handful of tolerances that are absolute lengths rather than fractions of the shapes they apply to, most visibly the speculative distance of 0.02 m at which contacts start being reported. A world laid out at a much smaller scale than a meter is dominated by them. b2SetLengthUnitsPerMeter scales all of them, but it has to be called before Box2D is touched at all, so it is exposed through the initializeForge2D gate that already has to run first rather than as a free-standing setter. Conflicting values throw instead of silently corrupting the simulation; repeating the value in effect is a no-op, so several games that agree on a scale can each ask for it. Tolerances exposes the derived constants so that the 0.02 is discoverable rather than mysterious. The web backend needs a keepalive wrapper, since b2SetLengthUnitsPerMeter is a plain B2_API function that emcc would otherwise drop. The suites that change the length unit are tagged out of the normal run, because dart test shares one process, and therefore one copy of the native library, between suites. --- .github/workflows/cicd.yml | 10 +++ README.md | 45 ++++++++++ packages/forge2d/dart_test.yaml | 15 ++++ packages/forge2d/lib/forge2d.dart | 3 +- packages/forge2d/lib/src/api/tolerances.dart | 53 +++++++++++ packages/forge2d/lib/src/api/world.dart | 5 +- .../forge2d/lib/src/backend/raw_box2d.dart | 11 +++ .../lib/src/backend/raw_box2d_ffi.dart | 9 ++ .../lib/src/backend/raw_box2d_wasm.dart | 10 +++ packages/forge2d/lib/src/initialize.dart | 72 ++++++++++++++- packages/forge2d/native/wasm/f2d_shim.c | 13 +++ .../forge2d/test/api/length_unit_test.dart | 89 +++++++++++++++++++ .../forge2d/test/api/tolerances_test.dart | 27 ++++++ pubspec.yaml | 11 +++ 14 files changed, 369 insertions(+), 4 deletions(-) create mode 100644 packages/forge2d/lib/src/api/tolerances.dart create mode 100644 packages/forge2d/test/api/length_unit_test.dart create mode 100644 packages/forge2d/test/api/tolerances_test.dart diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index fd0947c6..2cd20fdd 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -69,6 +69,16 @@ jobs: - uses: bluefireteam/melos-action@v3 - run: melos test + test-length-unit: + needs: [format, analyze] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + - uses: bluefireteam/melos-action@v3 + - name: Test the process-wide length unit + run: melos test:length-unit + test-web: needs: [format, analyze] strategy: diff --git a/README.md b/README.md index 0ee85808..5d650b69 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,51 @@ Highlights of the API: - `DebugDraw` can be implemented to render the physics world for debugging. +## Units + +Box2D is tuned for meters, kilograms and seconds, so lay your world out in +meters and aim to keep moving objects roughly between 0.1 and 10 of them, +with 1 being the sweet spot. Rendering scale is a separate concern: decide +how many pixels a meter is worth in your renderer, not in the simulation. + +Some of the tolerances are absolute lengths rather than fractions of the +shapes they apply to, so a world laid out at a much smaller scale behaves +oddly. The most visible one is the speculative distance: Box2D creates +contact points for shapes that are approaching but not yet touching, which +is what stops fast objects from passing through each other, and it means +`beginContact` fires while there is still a gap of up to `0.02` meters. A +shape that is only a couple of centimeters across is therefore permanently +in contact with its neighbors. `Tolerances` exposes these values: + +```dart +Tolerances.linearSlop; // 0.005 +Tolerances.speculativeDistance; // 0.02 +Tolerances.aabbMargin; // 0.05 +``` + +`WorldDef.restitutionThreshold` (1 m/s), `WorldDef.hitEventThreshold` +(1 m/s), `WorldDef.maxContactPushSpeed` (3 m/s), +`WorldDef.maximumLinearSpeed` (400 m/s) and `BodyDef.sleepThreshold` +(0.05 m/s) are absolute in the same way, but they are per world or per body, +so they can simply be set. + +When a world genuinely cannot be laid out at that scale, tell Box2D how many +of your length units make up a meter and every tolerance above moves with it: + +```dart +await initializeForge2D(lengthUnitsPerMeter: 100); +``` + +A good rule of thumb is to pass the height of your player character. You are +then on the hook for gravity, densities and forces being sensible at that +scale. For a length scale factor of `S`, velocities and accelerations scale +by `S`, masses by `S²`, forces and impulses by `S³` and torques by `S⁴`, +while densities, friction, restitution and damping stay as they are. Scaling +lengths and gravity together leaves the timing of the simulation unchanged. + +The length unit is process-wide and cannot change once a `World` exists, +which is why it is set through `initializeForge2D`. + ## Performance The standard [bench2d](https://github.com/joelgwebber/bench2d) benchmark diff --git a/packages/forge2d/dart_test.yaml b/packages/forge2d/dart_test.yaml index f4134dec..a60a6a42 100644 --- a/packages/forge2d/dart_test.yaml +++ b/packages/forge2d/dart_test.yaml @@ -3,3 +3,18 @@ # dart test -p chrome (dart2js) # dart test -p chrome -c dart2wasm platforms: [vm] + +# The length unit is a global inside Box2D, and `dart test` runs suites as +# isolates that share one process and therefore one copy of the library. The +# suites that change it are excluded from the normal run and get a run of +# their own: +# dart test -P length-unit +exclude_tags: length-unit + +tags: + length-unit: + +presets: + length-unit: + exclude_tags: nothing + concurrency: 1 diff --git a/packages/forge2d/lib/forge2d.dart b/packages/forge2d/lib/forge2d.dart index f9033e9b..d1061c9e 100644 --- a/packages/forge2d/lib/forge2d.dart +++ b/packages/forge2d/lib/forge2d.dart @@ -21,5 +21,6 @@ export 'src/api/joints/weld_joint.dart'; export 'src/api/joints/wheel_joint.dart'; export 'src/api/math.dart'; export 'src/api/shape.dart'; +export 'src/api/tolerances.dart'; export 'src/api/world.dart'; -export 'src/initialize.dart' show initializeForge2D; +export 'src/initialize.dart' show debugResetLengthUnitLock, initializeForge2D; diff --git a/packages/forge2d/lib/src/api/tolerances.dart b/packages/forge2d/lib/src/api/tolerances.dart new file mode 100644 index 00000000..f02aecec --- /dev/null +++ b/packages/forge2d/lib/src/api/tolerances.dart @@ -0,0 +1,53 @@ +import 'package:forge2d/src/initialize.dart'; + +/// The Box2D tolerances that scale with the length unit. +/// +/// Box2D is tuned for meters, kilograms and seconds, and a handful of its +/// tolerances are absolute lengths rather than fractions of the objects they +/// apply to. If a world is laid out at a much smaller scale than a meter, +/// those tolerances stop being "visually insignificant" and start dominating +/// the simulation: shapes report contacts before they touch, and the +/// broadphase margin grows larger than the shapes themselves. +/// +/// The usual fix is to lay the world out so that moving objects are roughly +/// 0.1 to 10 meters, with 1 meter being the sweet spot. When that is not +/// possible, tell Box2D what a meter means in your units with +/// `initializeForge2D(lengthUnitsPerMeter: ...)`, and every value here moves +/// with it. +/// +/// These mirror the constants in `src/constants.h` of Box2D v3.1.1. There are +/// further absolute thresholds that the length unit scales but that are +/// per-world or per-body rather than global, so they are fields on the +/// definitions instead: `WorldDef.restitutionThreshold`, +/// `WorldDef.hitEventThreshold`, `WorldDef.maxContactPushSpeed`, +/// `WorldDef.maximumLinearSpeed` and `BodyDef.sleepThreshold`. +abstract final class Tolerances { + /// How many length units make up one meter, mirroring + /// `b2GetLengthUnitsPerMeter`. + /// + /// Defaults to 1, meaning that forge2d lengths are meters. Set it through + /// `initializeForge2D(lengthUnitsPerMeter: ...)`. + static double get lengthUnitsPerMeter => rawBox2D.getLengthUnitsPerMeter(); + + /// The collision and constraint tolerance, `0.005` of a meter. + /// + /// Shapes are allowed to overlap by this much so that contacts stay stable. + static double get linearSlop => 0.005 * lengthUnitsPerMeter; + + /// The separation at which shapes start reporting contacts, `0.02` of a + /// meter, or four times the [linearSlop]. + /// + /// Box2D creates contact points for shapes that are approaching but not yet + /// touching, which is what keeps fast objects from passing through each + /// other and removes most collision jitter. It also means that + /// `beginContact` fires while there is still a visible gap, so shapes that + /// are not comfortably larger than this behave as if they were permanently + /// in contact. + static double get speculativeDistance => 4 * linearSlop; + + /// How much the broadphase fattens shape bounds, `0.05` of a meter. + /// + /// Lets a shape move a little without the dynamic tree having to be + /// rebuilt. + static double get aabbMargin => 0.05 * lengthUnitsPerMeter; +} diff --git a/packages/forge2d/lib/src/api/world.dart b/packages/forge2d/lib/src/api/world.dart index 998f8e58..8594b296 100644 --- a/packages/forge2d/lib/src/api/world.dart +++ b/packages/forge2d/lib/src/api/world.dart @@ -46,7 +46,10 @@ class World { maximumLinearSpeed: definition.maximumLinearSpeed, enableSleep: definition.enableSleep, enableContinuous: definition.enableContinuous, - ); + ) { + // Freezes the length unit: it is baked into this world's tolerances. + markWorldCreated(); + } /// The packed native world id. @internal diff --git a/packages/forge2d/lib/src/backend/raw_box2d.dart b/packages/forge2d/lib/src/backend/raw_box2d.dart index 2798f31d..0f94f659 100644 --- a/packages/forge2d/lib/src/backend/raw_box2d.dart +++ b/packages/forge2d/lib/src/backend/raw_box2d.dart @@ -8,6 +8,17 @@ /// The contract is deliberately restricted so that every implementation can /// provide it cheaply. See README.md in this directory before changing it. abstract interface class RawBox2D { + // Global tuning. + + /// Sets the process-wide length unit, mirroring `b2SetLengthUnitsPerMeter`. + /// + /// Box2D must not have been called before this, which the API layer + /// enforces in `initializeForge2D`. + void setLengthUnitsPerMeter(double lengthUnits); + + /// The process-wide length unit, mirroring `b2GetLengthUnitsPerMeter`. + double getLengthUnitsPerMeter(); + // World lifecycle. /// Creates a world and returns its packed id. diff --git a/packages/forge2d/lib/src/backend/raw_box2d_ffi.dart b/packages/forge2d/lib/src/backend/raw_box2d_ffi.dart index f10c6838..daae1da3 100644 --- a/packages/forge2d/lib/src/backend/raw_box2d_ffi.dart +++ b/packages/forge2d/lib/src/backend/raw_box2d_ffi.dart @@ -61,6 +61,15 @@ final class RawBox2DFfi implements RawBox2D { ..c = cos ..s = sin; + // Global tuning. + + @override + void setLengthUnitsPerMeter(double lengthUnits) => + b2.b2SetLengthUnitsPerMeter(lengthUnits); + + @override + double getLengthUnitsPerMeter() => b2.b2GetLengthUnitsPerMeter(); + // World. @override diff --git a/packages/forge2d/lib/src/backend/raw_box2d_wasm.dart b/packages/forge2d/lib/src/backend/raw_box2d_wasm.dart index 56d2e790..134ec2f0 100644 --- a/packages/forge2d/lib/src/backend/raw_box2d_wasm.dart +++ b/packages/forge2d/lib/src/backend/raw_box2d_wasm.dart @@ -110,6 +110,16 @@ final class RawBox2DWasm implements RawBox2D { _unsigned32(_runtime.readI32(_out + 4)), ); + // Global tuning. + + @override + void setLengthUnitsPerMeter(double lengthUnits) => + _call('f2d_set_length_units_per_meter', [lengthUnits]); + + @override + double getLengthUnitsPerMeter() => + _callF('f2d_get_length_units_per_meter', const []); + // World. @override diff --git a/packages/forge2d/lib/src/initialize.dart b/packages/forge2d/lib/src/initialize.dart index 19e1a6a6..c3898757 100644 --- a/packages/forge2d/lib/src/initialize.dart +++ b/packages/forge2d/lib/src/initialize.dart @@ -1,4 +1,7 @@ +import 'dart:typed_data'; + import 'package:forge2d/src/backend/backend.dart'; +import 'package:meta/meta.dart'; RawBox2D? _rawBox2D; @@ -7,6 +10,17 @@ RawBox2D? _rawBox2D; /// Internal to forge2d; not exported by the package. RawBox2D get rawBox2D => _rawBox2D ??= createRawBox2D(); +bool _worldHasBeenCreated = false; + +/// Records that a world exists, which freezes the length unit. +/// +/// Internal to forge2d; not exported by the package. +void markWorldCreated() => _worldHasBeenCreated = true; + +/// Rounds [value] the way Box2D stores it, so that a length unit that was +/// handed to the native side compares equal when it is read back. +double _toFloat32(double value) => (Float32List(1)..[0] = value)[0]; + /// Initializes forge2d. /// /// On native platforms this completes immediately. On the web it fetches @@ -18,8 +32,62 @@ RawBox2D get rawBox2D => _rawBox2D ??= createRawBox2D(); /// apps, and finally at `box2d.wasm` relative to the page. [wasmUri] /// overrides the lookup for custom hosting setups. /// +/// [lengthUnitsPerMeter] tells Box2D how many of your length units make up +/// one meter, so that its internal tolerances line up with the scale your +/// world is laid out at. See `Tolerances` for what it scales. Prefer laying +/// the world out so that moving objects are roughly 0.1 to 10 meters over +/// reaching for this; it is the escape hatch for worlds that cannot be +/// scaled. Leaving it null keeps whatever value is in effect, which is 1 +/// unless something else has set it. +/// +/// The length unit is process-wide and cannot change once a `World` exists, +/// which is why it lives here rather than in a free-standing setter: this +/// call is the gate that already has to run before Box2D is touched. Passing +/// a value that conflicts with one that is already in effect throws a +/// [StateError] instead of silently corrupting the simulation. Passing the +/// value that is already in effect is always a no-op, so several games that +/// agree on a scale can each ask for it. +/// /// Cross-platform code should always call and await this first. -Future initializeForge2D({Uri? wasmUri}) async { +Future initializeForge2D({ + Uri? wasmUri, + double? lengthUnitsPerMeter, +}) async { await initializeBackend(wasmUri: wasmUri); - _rawBox2D ??= createRawBox2D(); + final backend = _rawBox2D ??= createRawBox2D(); + if (lengthUnitsPerMeter == null) { + return; + } + if (lengthUnitsPerMeter <= 0 || !lengthUnitsPerMeter.isFinite) { + throw ArgumentError.value( + lengthUnitsPerMeter, + 'lengthUnitsPerMeter', + 'must be positive and finite', + ); + } + final current = backend.getLengthUnitsPerMeter(); + if (current == _toFloat32(lengthUnitsPerMeter)) { + return; + } + if (_worldHasBeenCreated) { + throw StateError( + 'The length unit cannot be changed from $current to ' + '$lengthUnitsPerMeter because a World has already been created. Box2D ' + 'bakes the length unit into the defaults of the definitions it hands ' + 'out and into the tolerances of live simulations, so it has to be set ' + 'before the first world exists. Await ' + 'initializeForge2D(lengthUnitsPerMeter: ...) at startup, before any ' + 'world is created.', + ); + } + backend.setLengthUnitsPerMeter(lengthUnitsPerMeter); } + +/// Forgets that a world has been created, so that a test can set a different +/// length unit than an earlier test did. +/// +/// This only clears forge2d's bookkeeping. The value inside Box2D and any +/// world that is still alive are left alone, so destroy the worlds of the +/// previous test first. +@visibleForTesting +void debugResetLengthUnitLock() => _worldHasBeenCreated = false; diff --git a/packages/forge2d/native/wasm/f2d_shim.c b/packages/forge2d/native/wasm/f2d_shim.c index 46c4be1b..30c5ee80 100644 --- a/packages/forge2d/native/wasm/f2d_shim.c +++ b/packages/forge2d/native/wasm/f2d_shim.c @@ -118,6 +118,19 @@ static b2QueryFilter f2d_query_filter(uint32_t category_lo, return filter; } +// Global tuning. +// +// b2SetLengthUnitsPerMeter and b2GetLengthUnitsPerMeter are plain B2_API +// functions, so emcc drops them without a keepalive wrapper. + +F2D_EXPORT void f2d_set_length_units_per_meter(float length_units) { + b2SetLengthUnitsPerMeter(length_units); +} + +F2D_EXPORT float f2d_get_length_units_per_meter(void) { + return b2GetLengthUnitsPerMeter(); +} + // World. F2D_EXPORT uint32_t f2d_create_world( diff --git a/packages/forge2d/test/api/length_unit_test.dart b/packages/forge2d/test/api/length_unit_test.dart new file mode 100644 index 00000000..06152052 --- /dev/null +++ b/packages/forge2d/test/api/length_unit_test.dart @@ -0,0 +1,89 @@ +@Tags(['length-unit']) +library; + +import 'package:forge2d/forge2d.dart'; +import 'package:test/test.dart'; + +// The length unit is a global inside Box2D, shared by every suite in the +// process, so this file is tagged out of the normal run and gets the process +// to itself; see dart_test.yaml. Within the file the tests run in declaration +// order and build on each other. +void main() { + setUpAll(initializeForge2D); + + // Puts the length unit back so that the `length-unit` preset, which runs + // every suite serially in one process, is safe whatever the order is. + tearDownAll(() async { + debugResetLengthUnitLock(); + await initializeForge2D(lengthUnitsPerMeter: 1); + }); + + group('initializeForge2D(lengthUnitsPerMeter:)', () { + test('rejects values that are not positive and finite', () async { + for (final value in [0.0, -1.0, double.nan, double.infinity]) { + await expectLater( + initializeForge2D(lengthUnitsPerMeter: value), + throwsArgumentError, + reason: '$value should be rejected', + ); + } + expect(Tolerances.lengthUnitsPerMeter, 1); + }); + + test('scales the tolerances', () async { + await initializeForge2D(lengthUnitsPerMeter: 100); + + expect(Tolerances.lengthUnitsPerMeter, 100); + expect(Tolerances.linearSlop, closeTo(0.5, 1e-6)); + expect(Tolerances.speculativeDistance, closeTo(2, 1e-6)); + expect(Tolerances.aabbMargin, closeTo(5, 1e-6)); + }); + + test('accepts the value that is already in effect again', () async { + await initializeForge2D(lengthUnitsPerMeter: 100); + expect(Tolerances.lengthUnitsPerMeter, 100); + }); + + test('accepts a value that only round trips through float32', () async { + // 0.04 is not representable in either float32 or float64, so the + // repeat-request check has to compare the way Box2D stores it. + debugResetLengthUnitLock(); + await initializeForge2D(lengthUnitsPerMeter: 0.04); + await expectLater( + initializeForge2D(lengthUnitsPerMeter: 0.04), + completes, + ); + expect(Tolerances.lengthUnitsPerMeter, closeTo(0.04, 1e-9)); + + debugResetLengthUnitLock(); + await initializeForge2D(lengthUnitsPerMeter: 100); + }); + + test('throws when a world already exists and the value differs', () async { + final world = World(); + addTearDown(world.destroy); + + await expectLater( + initializeForge2D(lengthUnitsPerMeter: 50), + throwsA( + isA().having( + (error) => error.message, + 'message', + contains('a World has already been created'), + ), + ), + ); + expect(Tolerances.lengthUnitsPerMeter, 100); + }); + + test('still accepts the unchanged value once a world exists', () async { + final world = World(); + addTearDown(world.destroy); + + await expectLater( + initializeForge2D(lengthUnitsPerMeter: 100), + completes, + ); + }); + }); +} diff --git a/packages/forge2d/test/api/tolerances_test.dart b/packages/forge2d/test/api/tolerances_test.dart new file mode 100644 index 00000000..dfacdbd9 --- /dev/null +++ b/packages/forge2d/test/api/tolerances_test.dart @@ -0,0 +1,27 @@ +import 'package:forge2d/forge2d.dart'; +import 'package:test/test.dart'; + +// The length unit is process-wide, so this file only reads it. The tests that +// change it live in length_unit_test.dart, which gets its own isolate. +void main() { + setUpAll(initializeForge2D); + + group('Tolerances', () { + test('defaults to meters', () { + expect(Tolerances.lengthUnitsPerMeter, 1); + }); + + test('has the Box2D defaults', () { + expect(Tolerances.linearSlop, closeTo(0.005, 1e-9)); + expect(Tolerances.speculativeDistance, closeTo(0.02, 1e-9)); + expect(Tolerances.aabbMargin, closeTo(0.05, 1e-9)); + }); + + test('reports the speculative distance as four times the slop', () { + expect( + Tolerances.speculativeDistance, + closeTo(4 * Tolerances.linearSlop, 1e-9), + ); + }); + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index 5c8a4fb2..593bc274 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,6 +48,17 @@ melos: exec: dart pub run dartdoc description: Run dartdoc checks for all packages. + test:length-unit: + exec: + command: dart test -P length-unit + concurrency: 1 + packageFilters: + dirExists: test + description: > + Run the suites that change the process-wide Box2D length unit. They + are excluded from `melos test` because `dart test` shares one process + between suites, so this preset runs everything serially instead. + coverage: steps: - melos exec -- dart test --coverage From 3804b9f8d1021679c5d10060859d84b976db37cc Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 11 Aug 2026 16:21:55 +0200 Subject: [PATCH 2/5] chore: Rebuild box2d.wasm for the length unit exports --- .../forge2d/lib/src/backend/wasm/box2d.wasm | Bin 227788 -> 227886 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/forge2d/lib/src/backend/wasm/box2d.wasm b/packages/forge2d/lib/src/backend/wasm/box2d.wasm index 037e3e46ac9914ed3c4ef1d4a59108c92dacdcbb..948327362723e5ec2b3360990a4b25f2090abcff 100755 GIT binary patch delta 6301 zcmZ`d30RcX+UGrIn04w zKLEr=^hrrc*Ay1%sUg`#5)&%EhP+3I%Y332Clzh_>Cj@`w4$79_dN(Fk&Y5o2Y@%WP`y) zO(qlCx1w(I&2WnaOmM4p6frZCo|yEb`Mr9NuBEJn@~QehGdHqc6H6+~qoG)NBvdyk zR#o0q7mh{C8$ylcwV_z3k-P{B$E5$CZEAo$!6>-xs;s5$wqv2S%zK*=XoksV}9gr%k zR>*S3V~xSO=*0TQTJk1%FH)3DhwDPY#`4LbNPUGI_s%%FQmZTIZ-MKge6XP*(p-*e zD?;RLwRnofAm3n2fp_>eqnqu5O#Ztulak&1tg!&z<$hB(dk@n22~%=zMW{|L{2n#S z7lTpLq!IV>nWprS@5d!UcDpWE8zLXXA!T0`tbX-KW6g40se={ zGwwr3zKkzg6>JDa$w#2C4Ms2-WCOcgQMIh1yrRAd6@7e#utJNHgS^Hpz$d)TETn!K z=bY?EZ7>=ohxiV&4-WIM%)YxmyMiIs+z=v1;*d(=WaQB}BsOz!O0XtGK37}(Vu@Ak zF+Senoq0U2JL0>RoQSKg?3B{W<*};9P_(KZFVnx`=(71M>0iXfjh?8wE6JB?+bX_P zhNJ27Jt1GI?X4hHM)EaxS~KArUS#c6crwm^R5hU~5(&k~w{Zq!2Q>6|d>RV=o3Ft2 zKU}i*g75i3Tz}xdShH+D#(T}dmhAlr(&XZ5SF0l2SRr3rxx(aUKGx=eQ#@kxq@Pxs z`QiU(~esTt-#=SH9ntlJQ?P!3s(lX_cWmynxN*>=Jug`fsY*OVo+s zh}`GD^S(&^Bc3W3L3NM($se%0uRRy9St(IyQY~H+a$cP?s!~O)Dj2Rq<0|Smxl|d9mo)W6IFAoWO!quO zl>wuYm=A5-NX= zRUKNJR)u5bp~)C;lsv{~Ij({ZzR8hOwwT5b{t7vAvV>kX_*EoVV^j{8rsYze=u989 zO!Zl%$r-%p^Kz<;d6gnZgZCj8jx{L(iTnYl7ar#`oW0lzn#yN7Q?84WCo~cDDZ-|Y z1?wt8<;ox+D`|ZDsnyp=tN0Jj^lP8gTv4TK@uBDuT}?CMI=7)Q9IXw;!WEiyCofC# z!Wuq4$?aKtnT{z{p-@EQdWv@>xvp8Kra~QCSlG!y`Qdmv9xEq7X@Y0?{-iW`7S|Lu zfvo3{oCOVoS>L&-;stI^pW^*~d ziIYO)H4WuXb;9eY!)vOokKqN_MtzE=G_Qkg*Y*oi9ju$?DfrE)F7}4D{}lH-RGwrx zr-}ycVSE$4D0aK}RcUtE$p@w7u(yyCF%C4}6v4N*e&R&zIr1`}la`AiWh(63)68f# zlP9{}Chdi#y=02DHaN8$9el7UMBd>xcQ)+eMeg3Po8N=$yL^^A6YX^3`WAoP?T457 z3AZ2B_U7B&9{xWWc6gN+r}u&P`1o{zZABAn-7br)R5i1Qcc%Add(rE)=`Q|xx}CjG zbNKCEH$ReXW4m!L<2$@MUX)>DAJD56nkPMl?Zf>gnt_i&+5NOYp-UOb>;Ue2=wIRa zLq%5_pXRf}M?Bl(hmSdUGkCej3J2v~VpD_sbr=cPhGXOt48KSE1&@J!s!6}^vBM#h z&NQfXJB*Tf@ENYD=w$+LPqn}iew(*|9i{o21{ZvebitrTd`z*Wd?H2guHi@2am)%v zY=}05LY3qMcV*_ozxa?$KYYQ9vr>3RmQKzFY*$XJGM&)PH)j^&C(lt_zwF_$$AXP= zJ7A&lJ>0FJZ|dd5qS-LXEM9$VS8u*W z@FZT2-NRe5bMV1_CR<=X;GK8)Jn|ifqx`#UKUTu->w{qq#`OrF=<~ymyw#V*enLMS zJ{RxySln?b(KAD)vtVd zjvxNZ*W~2DSzIy8IX3u>f0yH9zsvVF*KO239sl4(xnB5_kIKzVJEsm~?Rz&E#fK-@ zOwRL`+;rN)_{!YO!r4rDN>mp4X=-T1FPZwt zz)WEv0k90o%l}Ki{sRUofA$R>*8I@4&?0yYI$$ZkBQL+t1v-n(Ve{C0wty{Ut?W_O z#@g8;_83cif<4WiVb3xvc+CeoZJk}ux{v$vFq0vXKzEHANP(+L~!HL-V|l)j?D!q={kLUu@zQ! z&HH|20)oA#-b#g)5;ws+7Fa3XmjcJ}I3yK5Ny}QvZXl%J4NGtMEb->DWm~Uh4MIZT zbCbXVKapz3DygIp0>h9y7J+TdvXlulV5|;M$1-tf0}Lo$jzL%iqo6~)quQ~QSp;1G zM(D~^K5&REKx7tKOfWk{@#zK_2Ft}S8lb4~apu!ImNAspSNAaa^r&4#I|A)ZjR8Y+ z05uBw0HDRP2cf{XoGGVE9MC9pBno=L900-OST40a2(wL4DkV;X`;Ab-3lF74iY#S^< zaHtJx5M1a0LHwf)GQ^qfa3@?Ne$)=7h<7c5g$PzJQn<{I!4m?iw+h6u3PAx~V_Gr@ z7qmqjb;^2;xbra>jxweWMHCJA#H_`r`GXE9g2fV=8Yeq4ZYewo2v0AAs~K=fSONVh z!n;;Ny%P$hx1Wcz4B_@IkZwVJhql6O1oyutyCUs+4I&mvI0V<|5IN>33_vjNDBOs& z_l`ohkZdM_Zl^#7QDQO5+X70Y*7GpR01L!o15FcuNTACu-%E%u1Sk?8Fwz1jk{&hE zsL|9{Ab|zC8b@Ez;H3A$LNVy1LuZx<5O7yJCL!ou?MOw?ui7Ca%W4$lKI~Q*djz43 zK8a9fKC9v9f{O1zDC5^4l=+|6@ULk2T^jx)4S!t4&kX#e5&qN&3#h8-afGs>rxD7A zw`llo4Zlajf2!e6YWOo6zJ;lJIuOcwRw0!2Y|!wpD0l+|cFF{4T@qcKioMXB|(W%mcS~}kV8ze^)y#-*S zc;93ig$*Je1CnD3wIfc-no9BiSBml?>7HhKHT9O_K$$txMl2S=(wh<=39vCZBy`x> zt>T(#G?dbcLjro!oXbfW(`hNRiGQ6=Ber(r&EXp`p8pq1ck2Z7vZ zBS=n^@28_(`txDhK+%WVS@b&kD2UxJ(V^nL7HSjE;Gb=@6~z0S=_u*r+4LU3ycEur z!)X(n=g}~wJ&-c9t36@z!aLc>N3bXqq}las;Up})z2 zx9_G0DU`0S+(UK99@a=+BUw8C867I0ha98FWqRgU^tnu&vAZbC zNQG76u{4$^BOlq=N$c8KEk4)X%ob2Nb#+ z!I?bvb`SqY`RoshmD*Rp-h$`E<$c(F@SJq24-2w_CvZs1>e<;T%07;3O_dlX=`WFc7r44?$3_DQJrB?t)) z9Qz44&cEr)hMQL~tSA+9j-_J%VzvjkNK05g&ZUA9c9_CyF`+*T_FBShkYPbL0G}7U zqPXIP$5p<@tHrtf+27zvsowzR0qB&*4rGl0Ys5DPVfbsr)@#{1SSu9{W)D$#O4>Mt zEeBX9mJMS8STD{V#)jgldl)NBUC(?L!BFip2nn~^u%)ooERIf5SIV-HC9jkf!qeio zQg$<}6+27Wy5Z{>GPwl1qZ2oQzCr>zXb!Zr=%Nmpgcze>!db2GS@3aE_=Fz5Zi0N` zBW3IkSRx)SV;vZGO@K`V{5-ITKVFY*@a^?%I?X^a7HFY0J{GtDw<^~N6DlxtZsv{b zDYNE>1J8X%Fn_XUh9ZwH(4v|Nj$pZ%r1>LQvL2oiSB_#+y_e)vZz&cL1r-0qqS34v z8H1zQST8Cx26WXvqhKG6=7mJLTLN?N#YK~QN3#rMJUN>6gHADX47(RkQ^sJLI>k4} zFt@uCKMQ1kt%AM=JHU+bn{V=<Is=@qNvGL5=dKAFy=>w+ffSN!V zAz-H{4yqB|c^g}d1^xOqmIh-LEhfR-QwlBio>HJmLl22zqXtM%S?q$br!4XLl1AUo z@IUulyyH&RAM3L4PPW?so5VZrXBF;E%tr(|(gxN9`I^ug!SJ9@=h!5^e?J?6Oey18 Sv2hd2rf6i7G-f<|*!#ci3YAm< delta 5927 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 Date: Tue, 11 Aug 2026 16:32:18 +0200 Subject: [PATCH 3/5] ci: Upload the box2d.wasm artifact before checking it emcc output is only reproducible per host platform, so a rebuild on macOS or Windows does not match the Linux build byte for byte even on the pinned emsdk. Uploading first makes the Linux build downloadable exactly when the check fails and you need it to commit. --- .github/workflows/build-wasm.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-wasm.yml b/.github/workflows/build-wasm.yml index d6139c6a..2c47b017 100644 --- a/.github/workflows/build-wasm.yml +++ b/.github/workflows/build-wasm.yml @@ -21,14 +21,21 @@ jobs: version: 4.0.15 - name: Rebuild box2d.wasm run: packages/forge2d/tool/build_wasm.sh + # Uploaded before the check so that the fresh build is downloadable + # exactly when the check fails and you need it. + - uses: actions/upload-artifact@v4 + with: + name: box2d-wasm + path: packages/forge2d/lib/src/backend/wasm/box2d.wasm - name: Verify the committed artifact is up to date run: | if ! git diff --exit-code --stat -- packages/forge2d/lib/src/backend/wasm/box2d.wasm; then echo "The committed box2d.wasm differs from a fresh build." - echo "Rebuild it with tool/build_wasm.sh using emsdk 4.0.15." + echo + echo "emcc output is only reproducible on the same host platform," + echo "and this check builds on Linux, so a rebuild on macOS or" + echo "Windows will not match byte for byte even with emsdk 4.0.15." + echo "Download the box2d-wasm artifact from this run and commit it" + echo "instead of the locally built one." exit 1 fi - - uses: actions/upload-artifact@v4 - with: - name: box2d-wasm - path: packages/forge2d/lib/src/backend/wasm/box2d.wasm From efe6b9c77f60864bf52f30b0c86cf3cac860ccd1 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 11 Aug 2026 16:34:29 +0200 Subject: [PATCH 4/5] chore: Use the Linux build of box2d.wasm The build-wasm check compares against a Linux build, and emcc output is only reproducible per host platform, so the macOS rebuild differed from it despite being the same size and functionally identical. --- .../forge2d/lib/src/backend/wasm/box2d.wasm | Bin 227886 -> 227886 bytes packages/forge2d/tool/build_wasm.sh | 6 ++++++ 2 files changed, 6 insertions(+) diff --git a/packages/forge2d/lib/src/backend/wasm/box2d.wasm b/packages/forge2d/lib/src/backend/wasm/box2d.wasm index 948327362723e5ec2b3360990a4b25f2090abcff..e2d06b12795c21aba6f3dfa1c6bf41d937ba49c3 100755 GIT binary patch delta 1047 zcmY+CX>3$g6vyBHKX0aEUWZN>%Q6NYXiT-sjA(@dV>_V)(vpfF8kYz$#>Q?crY5G+ zT9#VI+GQRy^d^mTrY6`#8myUwYX}mzs4w-Aehg*D@2>U1$^Kl{A3`|$;+C_y+5Ih_7G~=kHxy;-yMNy%GJ3i(Pmuhs zgi>>2t`~@8W8a5H5!Ksw;Yq(foWg404fV}IbO^SN;HA6)*afkoR@)ofv)c>~=$R2* z^FTO?Be$FWMK$FNL2Jch6&dd4t>ht#4pswHYj%R^;w8f}8w3`Q}a<}z3= zCI4nnE6I)H^CdFKEs=Ix{Wgwey5lW`+zW>f;}C@96DadwP(SewDk)4%;)Q%vA3KSw z9#reUKEhH<1{6+XuV7@_9jAF3?Us7)JeH`h_92Rh&g+FGd(fdi{3DA{5_jlfy*%qu&E%|&L z3H3=nquMsC(E9I3uN80Y}@V z{KX@C@P`=*;ef8HV^fag(~oeSiD7-Vk=y*zRl0-qCKCGPr#az;qgFI?R&cqQR|OYa zcubIK<&5<1Y~!PHgZ`qOme5TX>tCN^#*#o9!}@q9%XmlB_QZNN2Mo)ytr8lmjF_jv ztg%ho2s(;gY`WcnWut_5-Kx5}SdFAQ(Z%*aN-9hnwqKf3s}`Hry#yuO8v%m55G!c8aEipU?6~ukgs%%=HN0Kf?(MpE}2=8;{NLg6n&J I;=8H;0CM;{_W%F@ delta 1068 zcmY+Ce`u6-9LJyc=bpQrJUe##(bN|BxT2Q3dFyJqsq1#2s9T9bhXyGkjrOZ)EQsaS zbk22m?wPkA_(d~Y6b#aIanQG9MpV=g%~cR-mYS6)3I-MiMZKTXMScJHzP_LD`}6*M zzR$zw$j!i!n}LZR%|avB=QDi%|16CBX08kvT;eXk2xRKLb@OLG{7BWKi>hlDFIigq zSm#UI8V9ilX^dvxxPO@ssmw2-R0$r+Jiqd6ZYHmJX;G#j{%K*Rq5Ic06l=eT=Mf{{ zmkDzw&-Ve5OzdLGenj<#ZFt(RdZ!RaM1MO4iz)i26ZnfcigAaIpTdyI%gbNl6_8uL z#@}B3!A?YgA$6?}9mJ+xcp*3pyC7E7VtZpdwwsL9A4!CCupeJ}Fsv^naZH$cZ~zJ7 zVj87tm4j70KQRQ`J>RWPId~Eks&o)h-ftd6lDIaAR$~1g?6=t?*EWI&hy^k>Yq**l z8ydm(>|UV?(pbjib?K~?U1-&85O?43<|tkR8QY7H2P3+DAF70W>s4&Y$3k874sLnK zz`K}hvC0FJ=q28s%vLErjy6lTpTUU`5~{{0hyR5h$&rOXSbgl51qkcwe(CVfsDR7HWzp6jYml*(PTprKC$5?@Gb4Q6~5OU+U_TCXr-! z^p$RT2Kf$bV|l=!oX7df2|9XXk8Bmb;q(q!g5ApMl^-Mp^ Date: Tue, 11 Aug 2026 16:46:37 +0200 Subject: [PATCH 5/5] ci: Fold the length unit tests into the normal test run They were tagged out into their own job so that they could not run alongside suites that would see the length unit they change. Serializing the suites achieves the same thing, and the whole suite takes about two seconds either way, so the tag, the preset, the melos script and the extra job were not buying anything. --- .github/workflows/cicd.yml | 10 --------- packages/forge2d/dart_test.yaml | 21 +++++++------------ .../forge2d/test/api/length_unit_test.dart | 14 +++++-------- .../forge2d/test/api/tolerances_test.dart | 2 +- pubspec.yaml | 11 ---------- 5 files changed, 13 insertions(+), 45 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 2cd20fdd..fd0947c6 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -69,16 +69,6 @@ jobs: - uses: bluefireteam/melos-action@v3 - run: melos test - test-length-unit: - needs: [format, analyze] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - - uses: bluefireteam/melos-action@v3 - - name: Test the process-wide length unit - run: melos test:length-unit - test-web: needs: [format, analyze] strategy: diff --git a/packages/forge2d/dart_test.yaml b/packages/forge2d/dart_test.yaml index a60a6a42..c51a6ac3 100644 --- a/packages/forge2d/dart_test.yaml +++ b/packages/forge2d/dart_test.yaml @@ -4,17 +4,10 @@ # dart test -p chrome -c dart2wasm platforms: [vm] -# The length unit is a global inside Box2D, and `dart test` runs suites as -# isolates that share one process and therefore one copy of the library. The -# suites that change it are excluded from the normal run and get a run of -# their own: -# dart test -P length-unit -exclude_tags: length-unit - -tags: - length-unit: - -presets: - length-unit: - exclude_tags: nothing - concurrency: 1 +# The Box2D length unit is a process-wide global, and `dart test` runs suites +# as isolates that share one process and therefore one copy of the library, so +# a suite that changes it would be visible to any suite running alongside it. +# Suites run one at a time instead, and the ones that change the length unit +# put it back when they are done. The whole suite takes a couple of seconds +# either way, so there is nothing to win by relaxing this. +concurrency: 1 diff --git a/packages/forge2d/test/api/length_unit_test.dart b/packages/forge2d/test/api/length_unit_test.dart index 06152052..03e6bbbf 100644 --- a/packages/forge2d/test/api/length_unit_test.dart +++ b/packages/forge2d/test/api/length_unit_test.dart @@ -1,18 +1,14 @@ -@Tags(['length-unit']) -library; - import 'package:forge2d/forge2d.dart'; import 'package:test/test.dart'; -// The length unit is a global inside Box2D, shared by every suite in the -// process, so this file is tagged out of the normal run and gets the process -// to itself; see dart_test.yaml. Within the file the tests run in declaration -// order and build on each other. +// The length unit is a global inside Box2D that every suite in the process +// shares, so suites run one at a time; see dart_test.yaml. Within this file +// the tests run in declaration order and build on each other. void main() { setUpAll(initializeForge2D); - // Puts the length unit back so that the `length-unit` preset, which runs - // every suite serially in one process, is safe whatever the order is. + // Puts the length unit back, so that the suites that run after this one see + // the default whatever order they end up in. tearDownAll(() async { debugResetLengthUnitLock(); await initializeForge2D(lengthUnitsPerMeter: 1); diff --git a/packages/forge2d/test/api/tolerances_test.dart b/packages/forge2d/test/api/tolerances_test.dart index dfacdbd9..cbb83220 100644 --- a/packages/forge2d/test/api/tolerances_test.dart +++ b/packages/forge2d/test/api/tolerances_test.dart @@ -2,7 +2,7 @@ import 'package:forge2d/forge2d.dart'; import 'package:test/test.dart'; // The length unit is process-wide, so this file only reads it. The tests that -// change it live in length_unit_test.dart, which gets its own isolate. +// change it live in length_unit_test.dart, which puts it back afterwards. void main() { setUpAll(initializeForge2D); diff --git a/pubspec.yaml b/pubspec.yaml index 593bc274..5c8a4fb2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,17 +48,6 @@ melos: exec: dart pub run dartdoc description: Run dartdoc checks for all packages. - test:length-unit: - exec: - command: dart test -P length-unit - concurrency: 1 - packageFilters: - dirExists: test - description: > - Run the suites that change the process-wide Box2D length unit. They - are excluded from `melos test` because `dart test` shares one process - between suites, so this preset runs everything serially instead. - coverage: steps: - melos exec -- dart test --coverage