avbd-babylon 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # AVBD for Babylon.js
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/avbd-babylon.svg)](https://www.npmjs.com/package/avbd-babylon)
4
+ [![npm downloads](https://img.shields.io/npm/dt/avbd-babylon.svg)](https://www.npmjs.com/package/avbd-babylon)
5
+ ![license](https://img.shields.io/npm/l/avbd-babylon)
6
+
3
7
  **TRY ME!!!** [**babylon-avbd.netlify.app**](https://babylon-avbd.netlify.app/)
4
8
 
5
9
  `avbd-babylon` runs AVBD rigid-body physics and collision detection on WebGPU,
@@ -11,6 +15,8 @@ the resulting poses; `AvbdPhysics` owns the physics world and its GPU solver.
11
15
 
12
16
  **Jump to:**
13
17
 
18
+ - [Benchmark sample code](#benchmark-sample-code)
19
+ - [Demo source and initialization](#demo-source-and-initialization)
14
20
  - [Install](#install)
15
21
  - [What this package adds](#what-this-package-adds)
16
22
  - [Feature coverage](#feature-coverage)
@@ -26,6 +32,8 @@ the resulting poses; `AvbdPhysics` owns the physics world and its GPU solver.
26
32
  - [Large pile with thin instances](#large-pile-with-thin-instances)
27
33
  - [Direct GPU rendering](#direct-gpu-rendering)
28
34
  - [Native physics with any renderer](#native-physics-with-any-renderer)
35
+ - [Choose a solver backend once](#choose-a-solver-backend-once)
36
+ - [Backend differences and benchmarks](#backend-differences-and-benchmarks)
29
37
  - [Native 3D world](#native-3d-world)
30
38
  - [Native 2D world](#native-2d-world)
31
39
  - [Bulk edits and GPU dispatches](#bulk-edits-and-gpu-dispatches)
@@ -43,6 +51,11 @@ the resulting poses; `AvbdPhysics` owns the physics world and its GPU solver.
43
51
  - [Slingshot siege demo](#slingshot-siege-demo)
44
52
  - [Manual stepping and cleanup](#manual-stepping-and-cleanup)
45
53
  - [Worker integration](#worker-integration)
54
+ - [Static terrain: hills, caves and overhangs](#static-terrain-hills-caves-and-overhangs)
55
+ - [Ships, water and destruction](#ships-water-and-destruction)
56
+ - [Worker and shared-memory benchmarks](#worker-and-shared-memory-benchmarks)
57
+ - [Reusable GPU and worker utilities](#reusable-gpu-and-worker-utilities)
58
+ - [D20 particle capacity](#d20-particle-capacity)
46
59
  - [Benchmark replication](#benchmark-replication)
47
60
  - [Babylon renderer comparison](#babylon-renderer-comparison)
48
61
  - [Script-tag usage](#script-tag-usage)
@@ -50,6 +63,54 @@ the resulting poses; `AvbdPhysics` owns the physics world and its GPU solver.
50
63
  - [Validation and attribution](#validation-and-attribution)
51
64
  - [First release checks](docs/RELEASE.md)
52
65
 
66
+ ## Benchmark sample code
67
+
68
+ **Start here:** [Benchmark replication](#benchmark-replication) contains copyable
69
+ JavaScript examples. Run `npm start`, then open the
70
+ [browser benchmark page](tests/performance.html) to run the scenes yourself.
71
+
72
+ | What you want to reproduce | Sample code and runnable source |
73
+ | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
74
+ | Physics timings, including the AVBD paper scenes | [Copyable physics example](#benchmark-replication), [scene measurement code](tests/performance.js), [package timing examples](tests/package-performance.js), [package runner](scripts/benchmark-package.mjs) |
75
+ | Scene setup: piles, walls, joints, cloth and stress tests | [Demo scene registry and builders](demo/lab/scenes.js), [browser benchmark controller](tests/performance.js) |
76
+ | Babylon rendering versus direct GPU rendering | [Rendering examples and commands](#babylon-renderer-comparison), [measurement code](tests/renderer-performance.js), [runner](scripts/benchmark-renderers.mjs) |
77
+ | Main thread versus dedicated worker and shared memory | [Copyable worker example and commands](#worker-and-shared-memory-benchmarks), [worker page](tests/workers.html), [worker runner](scripts/benchmark-workers.mjs) |
78
+ | Batched body edits, pose reads and queries | [Bulk-edit examples](#bulk-edits-and-gpu-dispatches), [measurement code](tests/native-edit-performance.js), [runner](scripts/check-native-performance.mjs) |
79
+ | CPU compatibility versus WebGPU | [Settings and results](#backend-differences-and-benchmarks), [measurement code](tests/compatibility-performance.js), [runner](scripts/benchmark-backends.mjs) |
80
+ | Sleeping and waking in large connected scenes | [Sleeping example](#queries-callbacks-and-sleeping), [connected-wall runner](scripts/benchmark-connected-sleep.mjs) |
81
+ | Ships, buoyancy and water | [Scene explanation](#ships-water-and-destruction), [scene builder](demo/naval/scene.js), [runner](scripts/benchmark-naval.mjs) |
82
+ | Grid versus HPLOC++ collisions | [Collision guide](docs/HPLOC.md), [paired scene measurements](tests/performance.js), [tree refit runner](scripts/benchmark-hploc-refit.mjs) |
83
+ | D20 particle collisions | [Capacity benchmark](#d20-particle-capacity), [runner](scripts/benchmark-particles.mjs), [reusable GPU particles](src/gpu/surfaceParticles.js), [collision shader](src/gpu/surfaceParticles.wgsl) |
84
+ | Movement solver and GPU scheduling | [Movement runner](scripts/benchmark-movement-solve.mjs), [color-bucket runner](scripts/benchmark-color-buckets.mjs), [small-group runner](scripts/benchmark-small-groups.mjs) |
85
+ | Uploads and command batching | [GPU upload runner](scripts/benchmark-webgpu-uploads.mjs), [command encoding runner](scripts/benchmark-command-encoding.mjs) |
86
+
87
+ This README includes package-usage examples and benchmark console snippets;
88
+ each benchmark snippet names the page where it runs. Files under `tests/`
89
+ implement the browser measurements; files under `scripts/` run those measurements
90
+ and save results. [tests/README.md](tests/README.md) lists the validation and
91
+ benchmark commands. Saved reports and screenshots live in `test-results/`.
92
+
93
+ ## Demo source and initialization
94
+
95
+ **[Demo source guide](demo/README.md)** lists each demo's configuration, builder,
96
+ controls and shaders. Each demo has its own folder: for example,
97
+ [Galton board](demo/galton-board/scene.js), [ships](demo/naval/scene.js),
98
+ [slingshot](demo/slingshot/scene.js), [terrain](demo/terrain/scene.js) and
99
+ [tearable cloth](demo/tearable-cloth/scene.js).
100
+
101
+ Open `scene.js` to change bodies, constraints and solver settings; `index.js`
102
+ initializes demos with their own controls. The [lab registry](demo/lab/scenes.js)
103
+ selects scenes, while [the shared runtime](demo/shared/runtime/simulationSession.js)
104
+ initializes their solver and renderer for workers. Related size or feature presets
105
+ share a `scenes.js` builder. Rendering and execution helpers live in `demo/shared/`.
106
+
107
+ Build the bodies and constraints together, create one solver, and reuse its GPU
108
+ buffers. Initial shapes and sensors select the required shaders before compilation;
109
+ disabled collision groups are excluded from the initial contact-capacity estimate.
110
+ The [startup check](scripts/check-demo-startup.mjs) initializes every demo in a
111
+ paused worker with `--all` and records setup times separately from simulation
112
+ performance. Shader caches, device checks and scene size affect those times.
113
+
53
114
  ## Install
54
115
 
55
116
  With a published release and your project's ESM bundler:
@@ -67,12 +128,14 @@ Babylon 9 is an optional peer dependency; integration examples are checked with
67
128
  9.13.0. Installing AVBD alone does not install Babylon. For TypeScript Babylon
68
129
  projects, install `@babylonjs/core` alongside AVBD as shown above.
69
130
  For portable 2D/3D physics without Babylon, import from `avbd-babylon/native`.
70
- That entry includes `AvbdScene2D`, `AvbdScene3D` and `createWebGPUDevice`, with
131
+ That entry includes `AvbdScene2D`, `AvbdScene3D`, `createSolverPipeline` and `createWebGPUDevice`, with
71
132
  independent declarations. Current TypeScript DOM libraries include the WebGPU
72
133
  types; older compilers may need `@webgpu/types` in the project's `types` setting.
73
134
  Your application does not need a WGSL loader or the repository's TypeScript sources.
74
135
  WebGPU requires a supported browser on localhost or HTTPS. Small-scene drawing
75
- can use Babylon's WebGL or WebGPU engine; physics uses WebGPU in either case.
136
+ can use Babylon's WebGL or WebGPU engine. WebGPU is the default physics backend;
137
+ native worlds also offer CPU compatibility. The repository demos include an
138
+ experimental WebGL2 solver, which is excluded from the npm package.
76
139
 
77
140
  The scene browser runs a small 3D floor-contact check once per GPU device before
78
141
  opening a 3D GPU scene. A box, sphere and capsule must settle on a thin floor.
@@ -93,6 +156,10 @@ The [physical-phone floor check](test-results/connected-phone-floor.json) verifi
93
156
  boxes, spheres and capsules in Android Brave. `npm run test:connected-phone`
94
157
  checks 2D/3D collisions, joints, bounce, triggers, masks, sleeping and queries
95
158
  on an authorized, unlocked Android phone running the local demo.
159
+ The current [Galaxy S23 GPU checks](test-results/connected-phone-contacts.json)
160
+ cover both 2D and 3D in Brave. The [phone scene checks](test-results/connected-phone-demos.json)
161
+ include hinges, sensors, terrain, cloth and 40,000-marble flow, with screenshots
162
+ and short measurements. Run these with `npm run test:connected-demos`.
96
163
 
97
164
  `AvbdPhysics.create()` prepares its device automatically. Native 3D applications
98
165
  can qualify a new device when creating it, or prepare a renderer-owned device
@@ -137,8 +204,22 @@ portable 2D/3D APIs and a Babylon integration:
137
204
  equations, timestep and iteration count. Direct GPU pose buffers let a
138
205
  compatible renderer draw many bodies without downloading every pose. When
139
206
  CPU poses are needed, callers can request only the bodies they use.
207
+ Large 2D and 3D worlds automatically use bitsets to group bodies for solving,
208
+ reducing GPU scheduling work while preserving body order.
209
+ Large rigid 3D scenes also update a contact's points in parallel. The shared
210
+ contact-point layout stores only the needed entries of symmetric body matrices,
211
+ reducing movement-solver work with the same contacts and equations.
212
+ It also groups frequently read pose fields during solving, then restores the
213
+ public body-buffer format before rendering, queries or body edits.
214
+ Closed jointless 3D worlds of up to 32 bodies batch all movement iterations in
215
+ one workgroup with shared body poses and the same reductions and equations.
216
+ CPU command setup reuses settings and offset storage, uploads only changed
217
+ step/sleep/bounce settings and skips repeated buffer bindings within a compute
218
+ pass.
140
219
  - **Sleeping and live editing.** Per-body sleep permission, support checks and
141
- impact checks allow resting objects to sleep and wake again. Velocities,
220
+ impact checks allow resting objects to sleep and wake again. Large jointless desktop
221
+ 3D worlds retain conservative connected groups so impacts can wake a wall
222
+ without waking unrelated walls on the same floor. Velocities,
142
223
  impulses, teleports, filters and motor targets can change during simulation.
143
224
  Welds capture their rest rotation from live GPU poses, and released
144
225
  constraint slots can be reused.
@@ -153,7 +234,7 @@ portable 2D/3D APIs and a Babylon integration:
153
234
  behavior, collider pair coverage, queries, constraints, sleeping and failure
154
235
  recovery. Collision overflow or conflicting parallel groups hold a step and
155
236
  report the problem instead of silently dropping work. Browser tests cover all
156
- 68 scenes, controls, screenshots and stage timings; replication examples
237
+ 69 scenes, controls, screenshots and stage timings; replication examples
157
238
  distinguish physics, drawing and the paper's measurements.
158
239
 
159
240
  The underlying solver and retained upstream code are credited in
@@ -168,50 +249,90 @@ It is an alternative physics integration, **not a drop-in Havok/Physics V2
168
249
  plugin**: use `AvbdPhysicsAggregate`, rather than Babylon's `PhysicsAggregate`
169
250
  and `scene.enablePhysics()`.
170
251
 
171
- | Feature | 3D GPU | 2D GPU | Babylon / package usage |
172
- | ------------------------------------------------------ | -------------------- | --------------------- | -------------------------------------------------------------------------------------------------------- |
173
- | Static and dynamic bodies, rotation, inertia, friction | Yes | Yes | Aggregates in 3D; native 2D boxes, circles and capsules. |
174
- | Boxes | Yes | Yes | Native collision shapes. |
175
- | Spheres / circles | Analytic | Analytic | 3D `addSphere()`; 2D `addCircle()`. Exact rounded surfaces. |
176
- | Capsules | Analytic | Analytic | 3D local Y, 2D local X. Full height / length includes both caps. |
177
- | Finite lines / infinite planes | Use static colliders | Yes | 2D `addSegment()` is two-sided; `addPlane()` defines a solid half-space. |
178
- | Cylinders | Convex hulls | No | Polygonal 3D approximations from mesh vertices. |
179
- | Custom convex hulls | Yes | Yes | `addHull()` in both dimensions. Up to 32 boundary vertices. |
180
- | Concave triangle surfaces | No | No | Convex proxies fill concavities. |
181
- | Body velocity, forces, impulses and teleporting | Yes | Yes | Native `gpu.body()` handles or indexed solver methods in both dimensions; live GPU poses. |
182
- | Batched body commands and packed velocity edits | Yes | Yes | `editBodies()` groups motion and property edits; packed linear/angular updates use one dispatch. |
183
- | Automatic Babylon stepping and disposal | Yes | Yes | `AvbdPhysics` or native `gpu.attachToScene(scene)` in either dimension. |
184
- | Manual fixed steps / worker execution | Yes | Yes | No renderer or DOM dependency in either solver. |
185
- | Mobile browsers with WebGPU | Yes | Yes | Verified on Android Brave. Babylon's async factory and the scene browser prepare 3D automatically; native 3D apps use `validate3D:true` or `prepareWebGPUDevice3D()`. Requires WebGPU and HTTPS or localhost. |
186
- | Automatic grid / HPLOC++ choice | Yes | Grid | 3D auto selection uses body sizes and workload. |
187
- | GPU work sharing | Yes | Yes | Retains the configured physics and iteration counts. |
188
- | Mesh / instance rendering | Yes | Yes | Use batch drawing for large scenes; portable builders do not create meshes. |
189
- | Direct GPU pose drawing | Yes | Yes | Renderer and solver must share a GPUDevice. Body layouts differ by dimension. |
190
- | Ball joints / planar hinges | Yes | Yes | 3D `addJoint()`; 2D joint with free angular row. |
191
- | World anchors | Yes | Yes | Null body A in 3D; body index `-1` in 2D. |
192
- | Welds and breakable joints | Yes | Yes | 3D `addWeld()`; 2D joint with all three rows locked and optional `breakForce`. |
193
- | Springs, linked ropes and ragdolls | Yes | Yes | 3D helpers; 2D bodies linked with joints/springs. |
194
- | Hinges | Axis + angle limits | Planar + angle limits | `addHinge()` in both dimensions; optional angle bounds relative to creation. |
195
- | Torque-limited motors | Yes | Yes | Both dimensions expose motor handles and live `gpu.setMotor()` controls. |
196
- | Elastic / tearable fabric | Yes | Build spring networks | 3D `addFabric()` has point collisions and tensile fracture. The demo preserves material after tearing. |
197
- | Per-body sleeping / waking | Yes | Yes | World `sleeping`, body `allowSleep`, live `setSleepEnabled()`; supported bodies rest, impacts wake them. |
198
- | Restitution / bounce | Yes | Yes | Coefficient 0..1. Zero adds no bounce passes. |
199
- | Collision masks | Yes | Yes | Both bodies must allow the pair. Includes sensors. |
200
- | Contact / trigger events | Yes | Yes | Babylon `onContact()`; native `gpu.watchContacts()` + `readContactEvents()` in both dimensions. |
201
- | Non-solving triggers | Yes | Yes | `isTrigger:true`; no contact forces. Static–static pairs are not scanned. |
202
- | Raycasts | Yes | Yes | `physics.raycast()` or portable `gpu.raycast()`; masks and ignored bodies. |
203
- | Sphere / circle casts | Yes | Yes | 3D `sphereCast()`; 2D `circleCast()`; exact rounded corner tests. |
204
- | Continuous collision detection | No | No | Discrete contacts; timestep matters for fast objects. |
205
- | Full Babylon Physics V2 / Havok plugin | No | No | Use AVBD aggregates, rather than `scene.enablePhysics()`. |
252
+ | Feature | 3D GPU | 2D GPU | Babylon / package usage |
253
+ | ------------------------------------------------------ | --------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
254
+ | Static and dynamic bodies, rotation, inertia, friction | Yes | Yes | Aggregates in 3D; native 2D boxes, circles and capsules. |
255
+ | Boxes | Yes | Yes | Native collision shapes. |
256
+ | Spheres / circles | Analytic | Analytic | 3D `addSphere()`; 2D `addCircle()`. Exact rounded surfaces. |
257
+ | Capsules | Analytic | Analytic | 3D local Y, 2D local X. Full height / length includes both caps. |
258
+ | Finite lines / infinite planes | Use static colliders | Yes | 2D `addSegment()` is two-sided; `addPlane()` defines a solid half-space. |
259
+ | Cylinders | Convex hulls | No | Polygonal 3D approximations from mesh vertices. |
260
+ | Custom convex hulls | Yes | Yes | `addHull()` in both dimensions. Up to 32 boundary vertices. |
261
+ | Static heightfields and concave mesh terrain | Tiled convex contacts | Use static segments / hulls | Native and Babylon `addHeightfield()` / `addStaticMesh()`. Each triangle becomes a small static prism, preserving openings and overhangs. |
262
+ | Body velocity, forces, impulses and teleporting | Yes | Yes | Native `gpu.body()` handles or indexed solver methods in both dimensions; live GPU poses. |
263
+ | Batched body commands and packed velocity edits | Yes | Yes | `editBodies()` groups motion and property edits; packed linear/angular updates use one dispatch. |
264
+ | Automatic Babylon stepping and disposal | Yes | Yes | `AvbdPhysics` or native `gpu.attachToScene(scene)` in either dimension. |
265
+ | Manual fixed steps / worker execution | Yes | Yes | No renderer or DOM dependency in either solver. |
266
+ | Mobile browsers with WebGPU | Yes | Yes | Verified on Android Brave. Babylon's async factory and the scene browser prepare 3D automatically; native 3D apps use `validate3D:true` or `prepareWebGPUDevice3D()`. Requires WebGPU and HTTPS or localhost. |
267
+ | Automatic grid / HPLOC++ choice | Yes | Grid | 3D auto selection uses body sizes and workload. |
268
+ | GPU work sharing | Yes | Yes | Retains the configured physics and iteration counts. |
269
+ | Mesh / instance rendering | Yes | Yes | Use batch drawing for large scenes; portable builders do not create meshes. |
270
+ | Direct GPU pose drawing | Yes | Yes | Renderer and solver must share a GPUDevice. Body layouts differ by dimension. |
271
+ | Ball joints / planar hinges | Yes | Yes | 3D `addJoint()`; 2D joint with free angular row. |
272
+ | World anchors | Yes | Yes | Null body A in 3D; body index `-1` in 2D. |
273
+ | Welds and breakable joints | Yes | Yes | 3D `addWeld()`; 2D joint with all three rows locked and optional `breakForce`. |
274
+ | Springs, linked ropes and ragdolls | Yes | Yes | 3D helpers; 2D bodies linked with joints/springs. |
275
+ | Hinges | Axis + angle limits | Planar + angle limits | `addHinge()` in both dimensions; optional angle bounds relative to creation. |
276
+ | Torque-limited motors | Yes | Yes | Both dimensions expose motor handles and live `gpu.setMotor()` controls. |
277
+ | Elastic / tearable fabric | Yes | Build spring networks | 3D `addFabric()` has point collisions and tensile fracture. The demo preserves material after tearing. |
278
+ | Per-body sleeping / waking | Yes | Yes | World `sleeping`, body `allowSleep`, live `setSleepEnabled()`; supported bodies rest, impacts wake them. |
279
+ | Restitution / bounce | Yes | Yes | Coefficient 0..1. Zero adds no bounce passes. |
280
+ | Collision masks | Yes | Yes | Both bodies must allow the pair. Includes sensors. |
281
+ | Contact / trigger events | Yes | Yes | Babylon `onContact()`; native `gpu.watchContacts()` + `readContactEvents()` in both dimensions. |
282
+ | Non-solving triggers | Yes | Yes | `isTrigger:true`; no contact forces. Static–static pairs are not scanned. |
283
+ | Raycasts | Yes | Yes | `physics.raycast()` or portable `gpu.raycast()`; masks and ignored bodies. |
284
+ | Sphere / circle casts | Yes | Yes | 3D `sphereCast()`; 2D `circleCast()`; exact rounded corner tests. |
285
+ | Continuous collision detection | No | No | Discrete contacts; timestep matters for fast objects. |
286
+ | Full Babylon Physics V2 / Havok plugin | No | No | Use AVBD aggregates, rather than `scene.enablePhysics()`. |
206
287
 
207
288
  See the [API reference](docs/BABYLON_API.md) and [GPU validation guide](tests/README.md)
208
289
  for supported methods, numerical checks and limitations.
209
290
 
210
291
  ## Playable feature demos
211
292
 
293
+ Also try [ships, waves and sinking](index.html?demo=naval),
294
+ [hills, caves and overhangs](index.html?demo=canonical&scene=3d-static-terrain&backend=gpu)
295
+ and the [interactive worker comparison](tests/workers.html).
296
+
297
+ Three more physics playgrounds offer thousands of independently simulated bodies:
298
+
299
+ | Playground | What you can do |
300
+ | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
301
+ | [Tornado town](index.html?demo=canonical&scene=3d-tornado-town&backend=gpu) | Send swirling wind through twenty houses made from 4,400 breakable pieces. Stop the wind and let debris settle. The visible funnel marks an analytic wind field. |
302
+ | [Domino spiral](index.html?demo=canonical&scene=3d-domino-spiral&backend=gpu) | Start a chain reaction through 2,048 tiles, or drag tiles to make your own branches. |
303
+ | [Galton board](index.html?demo=canonical&scene=3d-marble-cascade&backend=gpu) | Open the visible hopper to release 40,000 marbles through twenty staggered rows of pegs into thirty-three bins in a shallow board. Gravity and collisions control the discharge and spread. Watch the live bin chart, shake the board or reset it. |
304
+
305
+ These scenes support direct WebGPU and Babylon thin-instance drawing, dedicated
306
+ workers and optional shared input. The force-field demo adds one GPU pass to the
307
+ existing physics command buffer; positions stay on the GPU. CPU compatibility
308
+ also retains the actions, with the expected cost of simulating thousands of bodies.
309
+ The main scene browser and the naval demo default to a dedicated worker when
310
+ OffscreenCanvas is available. Physics and drawing run in the worker while the
311
+ scene selector and controls stay on the page. Choose Main thread, or add
312
+ `scope=main` to the scene URL, to compare execution. Specialized scene editors
313
+ are available through the scene editing link in the sidebar.
314
+
315
+ Every AVBD example has a Sleeping toggle for comparing the same scene with and
316
+ without sleeping. Large rigid scenes enable it by default. Add `sleeping=1` or
317
+ `sleeping=0` to choose explicitly; moving water and wind still keep affected
318
+ bodies awake. The half-million wall demos keep the original two-ball benchmark
319
+ until you click **Bowl every row**, which launches balls into all sixteen lanes.
320
+
321
+ The Galton board runs fixed physics steps against elapsed time, catching up
322
+ across frames when needed. Catch-up is bounded when the GPU is overloaded.
323
+ Click or tap a peg to retract it; its coral cap restores it. **Restore all pegs**
324
+ returns the original layout without releasing the hopper or resuming a paused
325
+ simulation. Peg movements and collision-filter changes are batched GPU edits.
326
+ [Playground checks](test-results/playgrounds.json) cover standing structures,
327
+ chain propagation, the 40,000-marble distribution and worker rendering. Run `npm run test:playgrounds`.
328
+
329
+ ![Tornado crossing a breakable neighborhood](test-results/screenshots/3d-tornado-town.png)
330
+ ![A spiral of falling dominoes](test-results/screenshots/3d-domino-spiral.png)
331
+ ![Thousands of marbles falling through a Galton board](test-results/screenshots/3d-marble-cascade-flow.png)
332
+
212
333
  Run `npm start` and open the links below. Every scene uses GPU physics. The
213
334
  [scene browser and benchmark](tests/performance.html)
214
- includes all 68 presets and lets you play them before measuring performance.
335
+ includes all presets and lets you play them before measuring performance.
215
336
  The [scene check report](test-results/report.html) contains
216
337
  screenshots and results after `npm run test:web`.
217
338
 
@@ -935,8 +1056,8 @@ export async function createDirectGpuExample(canvas, count = 4096) {
935
1056
 
936
1057
  The helper renders centered **boxes** in GPU order, including the floor. Hulls,
937
1058
  capsules and other graphics need their own geometry and any center/principal-frame
938
- conversion. The [demo renderer](demo/canonicalRenderer.js) shows those paths.
939
- 2D uses a different packed layout; see [the 2D renderer](demo/gpuRenderer2D.js).
1059
+ conversion. The [demo renderer](demo/shared/rendering/canonicalRenderer.js) shows those paths.
1060
+ 2D uses a different packed layout; see [the 2D renderer](demo/shared/rendering/gpuRenderer2D.js).
940
1061
 
941
1062
  For an existing Babylon world, pass `() => physics.getRenderBinding(device)`
942
1063
  to `createGpuBoxRenderer()`. Configure that world with `syncMeshes: false` and
@@ -957,9 +1078,9 @@ const gatePosition = poses.subarray(40, 43);
957
1078
 
958
1079
  ## Native physics with any renderer
959
1080
 
960
- Import `avbd-babylon/native` for WebGPU physics without Babylon. These worlds
1081
+ Import `avbd-babylon/native` for physics without Babylon. These worlds
961
1082
  also work in module workers. Create bodies and constraints in a scene builder,
962
- then create its GPU solver once. Rendering and simulation can share a GPUDevice.
1083
+ then create its solver once. WebGPU rendering and simulation can share a GPUDevice.
963
1084
  The canonical web demos and solver benchmarks use these native solvers by default.
964
1085
 
965
1086
  Both dimensions provide `gpu.body()`, `editBodies()`, `advance()`,
@@ -968,19 +1089,189 @@ shape queries. Native joints, welds, springs, hinges, motors, ropes, ragdolls an
968
1089
  fabric use the builders shown in the following sections. They do not require
969
1090
  Babylon meshes.
970
1091
 
1092
+ ### Choose a solver backend once
1093
+
1094
+ `createSolverPipeline()` initializes the execution backend. Its default is
1095
+ `webgpu`; choose `cpu` to run without a graphics device. Both use the same scene
1096
+ builders, body handles, ordered bulk edits, joints, triggers, bounce, collision
1097
+ masks, queries, sleeping and fixed-step calls. A pipeline can own several worlds.
1098
+ Changing backend means creating a new pipeline and rebuilding the world.
1099
+ Initialization requests supported limits for convex hulls and large scenes;
1100
+ buffers are allocated only when a world needs them. The backends return the
1101
+ same promise-based state and query results. GPU scheduling options such as
1102
+ `broadphase` select the WebGPU collision search.
1103
+ Timestep and iterations apply to both backends.
1104
+
1105
+ ```js
1106
+ import {
1107
+ AvbdScene2D,
1108
+ AvbdScene3D,
1109
+ createSolverPipeline,
1110
+ } from "avbd-babylon/native";
1111
+
1112
+ const pipeline = await createSolverPipeline({ backend: "webgpu" }); // or "cpu"
1113
+
1114
+ const scene3D = new AvbdScene3D({ iterations: 5 });
1115
+ scene3D.addBox([20, 1, 20], { density: 0, position: [0, -0.5, 0] });
1116
+ const ball = scene3D.addSphere(0.5, { position: [0, 3, 0], restitution: 0.6 });
1117
+ const world3D = scene3D.createSolver(pipeline);
1118
+
1119
+ const scene2D = new AvbdScene2D({ iterations: 5 });
1120
+ scene2D.addPlane([0, 1]);
1121
+ const circle = scene2D.addCircle(0.5, { position: [0, 3], restitution: 0.6 });
1122
+ const world2D = scene2D.createSolver(pipeline);
1123
+
1124
+ // The selected backend does not change these calls.
1125
+ world3D.editBodies([{ body: ball, impulse: [2, 0, 0] }]);
1126
+ world2D.editBodies([{ body: circle, impulse: [2, 0] }]);
1127
+ world3D.body(ball).setTrigger(false).setCollisionGroups(1, 1);
1128
+ world2D.body(circle).setTrigger(false).setCollisionGroups(1, 1);
1129
+ world3D.advance(1 / 60);
1130
+ world2D.advance(1 / 60);
1131
+ const state3D = await world3D.body(ball).readState();
1132
+ const state2D = await world2D.body(circle).readState();
1133
+
1134
+ // At application shutdown; destroys the worlds and a pipeline-owned GPU device.
1135
+ pipeline.destroy();
1136
+ ```
1137
+
1138
+ Rendering resources belong to the backend. Direct WebGPU drawing uses
1139
+ `pipeline.device` and the world's `bodyBuffer`; CPU worlds provide poses through
1140
+ the same `readSelectedBodies()` calls instead. Supply `{ device }` when sharing
1141
+ an existing WebGPU renderer's device; the pipeline then leaves that device alive
1142
+ on disposal. Existing `scene.createSolver(device)` calls remain supported.
1143
+
1144
+ The browser benchmark includes an **experimental WebGL2
1145
+ solver for repository comparisons**. Playable demos offer WebGPU and CPU. WebGL2 is excluded from the npm package and
1146
+ cannot be selected through the published `createSolverPipeline()` API. Its
1147
+ measured performance was too poor for a production backend; see the
1148
+ [side-by-side results](#backend-differences-and-benchmarks).
1149
+
1150
+ The [slingshot siege](https://babylon-avbd.netlify.app/?demo=slingshot) also has
1151
+ a physics selector. CPU and WebGPU modes include the same ropes, motors, breakable joints,
1152
+ collision masks, sensors and bounce, with all three fort sizes available.
1153
+ CPU mode draws with Canvas 2D and does not require WebGPU.
1154
+
1155
+ | Experimental WebGL2 benchmark preview with Babylon WebGL drawing | Experimental WebGL2 cloth benchmark preview |
1156
+ | ----------------------------------------------------------------- | ------------------------------------------------------------------------------- |
1157
+ | ![WebGL2 box stack](test-results/screenshots/webgl2-3d-stack.png) | ![WebGL2 tearable cloth](test-results/screenshots/webgl2-3d-tearable-cloth.png) |
1158
+
1159
+ [Slingshot compatibility checks](test-results/cpu-slingshot.json) cover launching,
1160
+ boosting and adding another ball on CPU and WebGPU.
1161
+
1162
+ The [shared API checks](test-results/backend-contract.json) run the same 2D/3D
1163
+ operations on the published CPU and WebGPU backends. Separate [experimental WebGL2 checks](test-results/webgl-contract.json) include cloth points, live constraint frames, sensors, masks, restitution and sleeping. The [CPU scene checks](test-results/cpu-demos.json)
1164
+ also cover the original scene collection, renderer switching and startup without
1165
+ WebGPU.
1166
+
1167
+ | CPU 3D with Babylon WebGL drawing | CPU 2D with Canvas drawing |
1168
+ | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
1169
+ | ![CPU box stack rendered with Babylon WebGL](test-results/screenshots/cpu-3d-stack.png) | ![CPU box stack rendered on Canvas](test-results/screenshots/cpu-2d-stack.png) |
1170
+
1171
+ | CPU rope | CPU chainmail |
1172
+ | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
1173
+ | ![CPU jointed rope](test-results/screenshots/cpu-3d-rope.png) | ![CPU chainmail with ring geometry](test-results/screenshots/cpu-showcase-chain-mail-1.6k.png) |
1174
+
1175
+ [CPU feature checks](test-results/cpu-features.json) include these scenes, cloth,
1176
+ hinges, sensors, collision masks, mixed shapes and demolition.
1177
+
1178
+ ### Backend differences and benchmarks
1179
+
1180
+ The published native 2D/3D API supports **WebGPU and CPU**. The WebGL2 column
1181
+ describes the repository experiment, which remains playable in the demos and
1182
+ is **excluded from the npm package**. The same scene commands are used in these
1183
+ comparisons; execution, rendering and memory costs differ.
1184
+
1185
+ | Capability | WebGPU (default) | WebGL2 (experimental benchmark only) | CPU |
1186
+ | ----------------------------------------------------------------- | -------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------ |
1187
+ | Included in the npm package | Yes | No | Yes |
1188
+ | 2D/3D colliders, joints, welds, springs, cloth, motors and limits | Supported | Supported | Supported |
1189
+ | Bounce, triggers, masks, sleeping and batched body commands | Supported | Supported | Supported |
1190
+ | Simulation collision detection and AVBD solving | GPU compute | GPU texture/draw passes | JavaScript on the CPU |
1191
+ | Direct drawing from public GPU pose buffers | Yes; renderer shares the GPU device | Poses must be downloaded | Renderer reads CPU poses |
1192
+ | Selected body reads | GPU gathers requested bodies | GPU gathers requested bodies | Copies requested CPU records |
1193
+ | Ray and circle/sphere casts | GPU queries | Downloads current poses, then CPU queries | CPU queries |
1194
+ | Contact events | GPU results downloaded when enabled | GPU results downloaded when enabled | CPU results |
1195
+ | Babylon aggregate API (`AvbdPhysics`, `AvbdPhysicsAggregate`) | Supported | Use native worlds and synchronize renderer poses | Use native worlds and synchronize renderer poses |
1196
+ | Native worlds with Babylon or another renderer | Supported | Supported | Supported |
1197
+ | Module workers | WebGPU device in worker | Offscreen WebGL2 compute context in worker | JavaScript worker |
1198
+ | Large-scene limits | GPU buffer limits and available memory | More memory per candidate; default 2 GiB contact-texture budget | Browser memory and CPU time |
1199
+
1200
+ WebGPU is the best starting point for large scenes and direct GPU rendering.
1201
+ CPU can be quicker for a handful of objects because it avoids GPU submission
1202
+ costs. The WebGL2 experiment needs `EXT_color_buffer_float` and uses more memory
1203
+ than WebGPU.
1204
+ The 100K and half-million benchmarks can exceed WebGL2's default budget. Increasing
1205
+ `maxTextureBytes` only helps when the device has enough memory.
1206
+
1207
+ <!-- backend-comparison-results -->
1208
+
1209
+ **Measured on an NVIDIA GeForce RTX 4070 Laptop GPU and Intel Core Ultra 7 155H CPU**,
1210
+ using headless Chrome 149 on Windows, October 10, 2026. Each scene uses
1211
+ a 1/60-second timestep and 10 solver iterations. There are 60 warmup steps
1212
+ and 60 measured steps per run, with three fresh-world repeats: **180 samples
1213
+ per scene and backend**. Drawing and sleeping are disabled.
1214
+
1215
+ **Mean physics time per step — lower is faster:**
1216
+
1217
+ | Scene | Objects, including fixed bodies | Iterations | WebGPU: GPU ms | Experimental WebGL2: GPU ms | CPU: JavaScript ms |
1218
+ | -------------------------------------------------------------------------- | ------------------------------: | ---------: | -------------: | --------------------------: | -----------------: |
1219
+ | [2D stack](index.html?demo=canonical&scene=2d-stack&backend=gpu) | 21 | 10 | 1.810 | 38.241 | 0.299 |
1220
+ | [2D joint grid](index.html?demo=canonical&scene=2d-joint-grid&backend=gpu) | 625 | 10 | 3.614 | 51.396 | 4.323 |
1221
+ | [3D stack](index.html?demo=canonical&scene=3d-stack&backend=gpu) | 11 | 10 | 2.862 | 25.716 | 0.794 |
1222
+ | [3D pyramid](index.html?demo=canonical&scene=3d-pyramid&backend=gpu) | 137 | 10 | 4.056 | 57.057 | 14.776 |
1223
+
1224
+ GPU columns measure the GPU interval for collisions, scheduling and solving.
1225
+ The CPU column measures JavaScript executing the complete physics step.
1226
+ Startup, drawing and waiting for timing results are excluded. These numbers
1227
+ are physics timings; a rendered frame also needs CPU submission and drawing.
1228
+
1229
+ **Additional JavaScript time to submit GPU work, in ms per step:**
1230
+
1231
+ | Scene | WebGPU submission | Experimental WebGL2 submission |
1232
+ | ------------- | ----------------: | -----------------------------: |
1233
+ | 2D stack | 0.164 | 41.617 |
1234
+ | 2D joint grid | 0.197 | 55.267 |
1235
+ | 3D stack | 0.207 | 27.073 |
1236
+ | 3D pyramid | 0.666 | 54.471 |
1237
+
1238
+ Submission can overlap GPU execution, so these two tables should not be added
1239
+ together or converted directly into FPS. The raw report also records the
1240
+ time spent waiting for each measured step and the startup cost.
1241
+
1242
+ CPU wins the two small-stack cases here. WebGPU is quicker for the 625-object
1243
+ joint grid and the 137-object pyramid. The crossover depends on contact density
1244
+ and joints as well as object count. WebGL2 is the slowest of the three in these
1245
+ runs; its many texture/draw passes carry substantial CPU submission overhead.
1246
+ Its performance was too poor for inclusion in the main package. WebGL2 remains
1247
+ available in the experimental benchmark; WebGPU is the default and
1248
+ CPU is the packaged compatibility option.
1249
+
1250
+ <!-- /backend-comparison-results -->
1251
+
1252
+ Reproduce this comparison with `npm run benchmark:backends`, or open the
1253
+ [browser benchmark](tests/performance.html) and choose each physics backend.
1254
+ The script rotates backend order between repeats, builds a fresh world for each
1255
+ run, verifies matching object counts and solver settings, and rejects collision
1256
+ overflow, conflicting solve groups and invalid poses. It saves the raw samples
1257
+ in [backend-comparison.json](test-results/backend-comparison.json).
1258
+
971
1259
  ### Native 3D world
972
1260
 
973
1261
  This builds a box pile, reusable capsule projectile, trigger volume and motorized
974
1262
  hinge. The default gravity axis is Y. Keep builder body references for bulk
975
- edits; `gpu.body(reference)` creates a convenient handle only when requested.
1263
+ edits; `solver.body(reference)` creates a convenient handle only when requested.
976
1264
 
977
1265
  ```js
978
1266
  // src/portable-3d.js
979
- import { AvbdScene3D, createWebGPUDevice } from "avbd-babylon/native";
980
-
981
- export async function createPortable3D({ device, count = 4096 } = {}) {
982
- const ownsDevice = !device;
983
- if (!device) ({ device } = await createWebGPUDevice());
1267
+ import { AvbdScene3D, createSolverPipeline } from "avbd-babylon/native";
1268
+
1269
+ export async function createPortable3D({
1270
+ backend = "webgpu",
1271
+ device,
1272
+ count = 4096,
1273
+ } = {}) {
1274
+ const pipeline = await createSolverPipeline({ backend, device });
984
1275
  const world = new AvbdScene3D({ gravity: -9.81, iterations: 5 });
985
1276
  world.addBox([100, 1, 100], { mass: 0, position: [0, -0.5, 0] });
986
1277
  const bodies = [];
@@ -1008,37 +1299,37 @@ export async function createPortable3D({ device, count = 4096 } = {}) {
1008
1299
  });
1009
1300
  const gate = world.addBox([4, 0.3, 0.3], { position: [0, 12, 0] });
1010
1301
  const hinge = world.addHinge(null, gate, {
1302
+ anchorA: [0, 12, 0],
1011
1303
  axisA: [0, 0, 1],
1012
1304
  axisB: [0, 0, 1],
1013
1305
  minAngle: -0.7,
1014
1306
  maxAngle: 0.7,
1015
1307
  motor: { speed: 1, maxTorque: 10 },
1016
1308
  });
1017
- const gpu = world.createSolver(device, {
1309
+ const solver = world.createSolver(pipeline, {
1018
1310
  broadphase: "auto",
1019
1311
  sleeping: true,
1020
1312
  });
1021
1313
  return {
1022
- device,
1023
- gpu,
1314
+ device: pipeline.device,
1315
+ solver,
1024
1316
  bodies,
1025
1317
  hinge,
1026
- projectile: gpu.body(projectile),
1027
- sensor: gpu.body(sensor),
1318
+ projectile: solver.body(projectile),
1319
+ sensor: solver.body(sensor),
1028
1320
  dispose() {
1029
- gpu.destroy();
1030
- if (ownsDevice) device.destroy();
1321
+ pipeline.destroy();
1031
1322
  },
1032
1323
  };
1033
1324
  }
1034
1325
  ```
1035
1326
 
1036
- `projectile.applyImpulse([20, 0, 0])` uses its current GPU centre. A world-point
1327
+ `projectile.applyImpulse([20, 0, 0])` uses its current centre. A world-point
1037
1328
  argument also adds the appropriate rotation. `teleport(position, quaternion?)`
1038
1329
  preserves velocity and clears old contact warm starts for that body. Position
1039
1330
  means centre of mass; custom hulls use their principal frame. Numeric arguments
1040
- to `gpu.body()` are **GPU slots**; use builder references or `bodyIndex()` to
1041
- account for the initial spatial sorting.
1331
+ to `solver.body()` are **live solver slots**; use builder references or
1332
+ `bodyIndex()` to account for any initial spatial sorting.
1042
1333
 
1043
1334
  ### Native 2D world
1044
1335
 
@@ -1049,11 +1340,14 @@ a spring and a breakable weld.
1049
1340
 
1050
1341
  ```js
1051
1342
  // src/portable-2d.js
1052
- import { AvbdScene2D, createWebGPUDevice } from "avbd-babylon/native";
1053
-
1054
- export async function createPortable2D({ device, count = 4096 } = {}) {
1055
- const ownsDevice = !device;
1056
- if (!device) ({ device } = await createWebGPUDevice());
1343
+ import { AvbdScene2D, createSolverPipeline } from "avbd-babylon/native";
1344
+
1345
+ export async function createPortable2D({
1346
+ backend = "webgpu",
1347
+ device,
1348
+ count = 4096,
1349
+ } = {}) {
1350
+ const pipeline = await createSolverPipeline({ backend, device });
1057
1351
  const world = new AvbdScene2D({ gravity: -9.81, iterations: 5 });
1058
1352
  world.addPlane([0, 1]);
1059
1353
  world.addSegment([-20, 4], [-16, 4]);
@@ -1095,17 +1389,16 @@ export async function createPortable2D({ device, count = 4096 } = {}) {
1095
1389
  stiffness: [Infinity, Infinity, Infinity],
1096
1390
  breakForce: 100,
1097
1391
  });
1098
- const gpu = world.createSolver(device, { sleeping: true });
1392
+ const solver = world.createSolver(pipeline, { sleeping: true });
1099
1393
  return {
1100
- device,
1101
- gpu,
1394
+ device: pipeline.device,
1395
+ solver,
1102
1396
  bodies,
1103
1397
  hinge,
1104
- projectile: gpu.body(projectile),
1105
- sensor: gpu.body(sensor),
1398
+ projectile: solver.body(projectile),
1399
+ sensor: solver.body(sensor),
1106
1400
  dispose() {
1107
- gpu.destroy();
1108
- if (ownsDevice) device.destroy();
1401
+ pipeline.destroy();
1109
1402
  },
1110
1403
  };
1111
1404
  }
@@ -1118,7 +1411,7 @@ projectile.setTrigger(false).setRestitution(0.5).setSleepEnabled(false);
1118
1411
  projectile.setCollisionGroups(2, 1 | 2);
1119
1412
  // 3D uses xyz; use [20, 0] for the 2D projectile.
1120
1413
  projectile.applyImpulse([20, 0, 0]);
1121
- gpu.setMotor(hinge.motor.slot, { speed: -1, maxTorque: 10 });
1414
+ solver.setMotor(hinge.motor.slot, { speed: -1, maxTorque: 10 });
1122
1415
  ```
1123
1416
 
1124
1417
  ### Bulk edits and GPU dispatches
@@ -1150,7 +1443,7 @@ For 100,000 bodies this example uploads 200,000 commands together, rather than
1150
1443
  submitting a GPU job per command. Each GPU invocation owns one edited body and
1151
1444
  executes its commands in order. Command/range buffers grow when needed and are
1152
1445
  reused. Velocity/force batches dispatch only edited bodies; teleport batches
1153
- also scan previous manifolds to clear affected contact warm starts. Native 3D
1446
+ also scan previous manifolds to clear affected contact warm starts. Native 3D
1154
1447
  teleport cleanup shares the edit submission.
1155
1448
 
1156
1449
  ```js
@@ -1176,16 +1469,19 @@ field; motion commands retain their order.
1176
1469
  ```js
1177
1470
  // src/portable-properties.js
1178
1471
  export function configureBodies(gpu, bodies) {
1179
- gpu.editBodies((function* () {
1180
- for (const body of bodies) yield {
1181
- body,
1182
- isTrigger: false,
1183
- restitution: 0.4,
1184
- group: 2,
1185
- collidesWith: 1 | 2,
1186
- allowSleep: true,
1187
- };
1188
- })());
1472
+ gpu.editBodies(
1473
+ (function* () {
1474
+ for (const body of bodies)
1475
+ yield {
1476
+ body,
1477
+ isTrigger: false,
1478
+ restitution: 0.4,
1479
+ group: 2,
1480
+ collidesWith: 1 | 2,
1481
+ allowSleep: true,
1482
+ };
1483
+ })(),
1484
+ );
1189
1485
  gpu.step(); // Applies the edits, waking, bounce and solving in one submission.
1190
1486
  }
1191
1487
  ```
@@ -1196,6 +1492,10 @@ time. Batch motor changes by calling `setMotor()` for each motor and stepping
1196
1492
  once; the uploads touch the target fields and preserve live constraint forces.
1197
1493
  Removing or reusing a constraint applies its earlier edits first.
1198
1494
 
1495
+ WebGPU also reuses its solver-setting upload storage. Unchanged settings stay
1496
+ on the GPU; live changes to gravity, timestep, wind and solver options are
1497
+ uploaded automatically. This applies to both native and Babylon integrations.
1498
+
1199
1499
  For frequent velocity updates, use packed arrays. This submits one dispatch,
1200
1500
  after flushing earlier queued edits. Each index must be a unique GPU slot in
1201
1501
  ascending order. The arrays can be reused each frame; values are copied into GPU
@@ -1231,13 +1531,21 @@ use compact uploads when the batch contains only linear and angular replacements
1231
1531
  ```js
1232
1532
  // src/portable-motion.js
1233
1533
  export function createMotionController(gpu, bodies, dimension = 3) {
1234
- const indices = Uint32Array.from(bodies, body => gpu.bodyIndex(body)).sort();
1534
+ const indices = Uint32Array.from(bodies, (body) =>
1535
+ gpu.bodyIndex(body),
1536
+ ).sort();
1235
1537
  const linear = new Float32Array(indices.length * dimension);
1236
1538
  const angular = new Float32Array(indices.length * (dimension === 3 ? 3 : 1));
1237
1539
  return {
1238
- indices, linear, angular,
1239
- update() { gpu.setVelocities(indices, { linear, angular }); },
1240
- updateSpin() { gpu.setAngularVelocities(indices, angular); },
1540
+ indices,
1541
+ linear,
1542
+ angular,
1543
+ update() {
1544
+ gpu.setVelocities(indices, { linear, angular });
1545
+ },
1546
+ updateSpin() {
1547
+ gpu.setAngularVelocities(indices, angular);
1548
+ },
1241
1549
  };
1242
1550
  }
1243
1551
  ```
@@ -1252,15 +1560,15 @@ Saved 100,000-body edit measurements from the machine with an **RTX 4070 Laptop
1252
1560
  GPU**, using five samples after warmup:
1253
1561
 
1254
1562
  | World | Individual body calls, CPU time | `editBodies()`, CPU time | Packed arrays, CPU time | Upload for each method |
1255
- | ----- | ------------------------------: | ----------------------: | ----------------------: | ---------------------: |
1256
- | 2D | 11.8 ms | 21.0 ms | 3.2 ms | 1.2 MB |
1257
- | 3D | 39.8 ms | 70.9 ms | 5.2 ms | 1.6 MB |
1563
+ | ----- | ------------------------------: | -----------------------: | ----------------------: | ---------------------: |
1564
+ | 2D | 11.8 ms | 21.0 ms | 3.2 ms | 1.2 MB |
1565
+ | 3D | 39.8 ms | 70.9 ms | 5.2 ms | 1.6 MB |
1258
1566
 
1259
1567
  These times measure CPU work to prepare and submit velocity edits. They exclude
1260
1568
  physics solving and drawing; the GPU completion measurements are recorded
1261
1569
  separately. This compares three ways to submit the same edits within this package.
1262
1570
  Selected full body records matched exactly. Run `npm run benchmark:native-edits`
1263
- to measure your own computer. Caller arrays and body handles are prepared before
1571
+ to measure your own computer. Caller arrays and body handles are prepared before
1264
1572
  timing and reused. [Raw results](test-results/native-api-performance.json).
1265
1573
 
1266
1574
  The [native API report](test-results/package-performance.html) also measures
@@ -1365,11 +1673,11 @@ orientations. A spring without `rest` captures the current anchor separation.
1365
1673
  `breakOnPull` adds linear force to the angular-force break test; it is not a
1366
1674
  separate tensile-only material model. Joint changes and commands wake sleeping
1367
1675
  bodies. Constraints can be created before initialization or during simulation.
1368
- Springs do not fracture; a finite `breakForce` on a Babylon spring is rejected.
1676
+ Springs support tensile fracture through `breakForce` and `breakStrain`.
1369
1677
 
1370
1678
  For large scenes, the portable builders below avoid making one Babylon mesh per
1371
1679
  collider. They are also exported by the package, require no custom shader loader,
1372
- and never step a CPU solver. The first portable example uses **z up** explicitly;
1680
+ and use WebGPU by default. The first portable example uses **z up** explicitly;
1373
1681
  the rope, ragdoll and fabric helpers below use **y up**.
1374
1682
 
1375
1683
  ### Ball joints, welds and springs
@@ -1485,7 +1793,7 @@ For the complete rendered presets, open [Rope](index.html?demo=canonical&scene=3
1485
1793
  [Ragdolls on a rigid net](index.html?demo=canonical&scene=showcase-ragdolls-on-cloth-24k&backend=gpu),
1486
1794
  [Breakable](index.html?demo=canonical&scene=3d-breakable&backend=gpu)
1487
1795
  and [Castle siege](index.html?demo=cannon). The
1488
- [castle builder](demo/castleScene.js) shows connected mortar and roof joints.
1796
+ [castle builder](demo/castle/scene.js) shows connected mortar and roof joints.
1489
1797
 
1490
1798
  ### Tearable cloth
1491
1799
 
@@ -1561,9 +1869,9 @@ one per simulated point. Intact edges stretch together; broken edges keep each
1561
1869
  point's share of the material, following its live pose. Detached patches remain
1562
1870
  visible and physical indefinitely. There is no automatic debris lifetime.
1563
1871
  This is a point-sampled cloth model, not an independent rigid-body shard model.
1564
- The web demo's [cloth renderer](demo/tearableClothRenderer.js) reads those
1872
+ The web demo's [cloth renderer](demo/tearable-cloth/renderer.js) reads those
1565
1873
  stiffness values on the GPU, so drawing does not download cloth poses or joint
1566
- states. Its [scene builder](demo/tearableClothScene.js) records which
1874
+ states. Its [scene builder](demo/tearable-cloth/scene.js) records which
1567
1875
  connections support each triangle. The renderer is application code, excluded
1568
1876
  from the package.
1569
1877
 
@@ -1790,7 +2098,8 @@ physics.setGravity(new Vector3(0, -9.81, 0));
1790
2098
  Commands apply at the next step against live GPU mass, inertia and position.
1791
2099
  Impulses queued for the same body are combined. Omitting the application point
1792
2100
  applies an impulse through the center of mass. Teleport retains velocity unless
1793
- you also set it. To remove an object, dispose its aggregate and its mesh:
2101
+ you also set it; omitting the quaternion preserves the live GPU rotation.
2102
+ The position is the mesh origin. To remove an object, dispose its aggregate and its mesh:
1794
2103
 
1795
2104
  ```js
1796
2105
  fallingBox.dispose(); // removes collisions; does not dispose the visual mesh
@@ -1893,7 +2202,13 @@ console.log(await physics.readSleepStats());
1893
2202
  ```
1894
2203
 
1895
2204
  Impulses, teleports, constraint edits, new bodies and removed supports wake the
1896
- world. Impacts and moving joint endpoints also wake it conservatively. Under
2205
+ world. Jointless desktop WebGPU worlds with at least 4,096 bodies automatically track conservative connected
2206
+ groups, so an impact wakes the touched group while unrelated piles stay asleep.
2207
+ The static floor does not connect separate piles. Connections persist while
2208
+ bodies sleep and reset on explicit world edits; a group can therefore include
2209
+ objects that touched earlier. Smaller worlds and mobile compatibility paths use
2210
+ whole-world impact waking. Set `sleeping: { islands: true }` to opt into connected
2211
+ groups in a smaller 3D world, or `islands: false` to use whole-world waking. Under
1897
2212
  gravity, unsupported slow-moving bodies stay awake; supports extend upward
1898
2213
  through resting contacts. This does not guarantee that every suspended or
1899
2214
  jointed object will sleep. Sleeping is off by default unless a body explicitly
@@ -1902,6 +2217,32 @@ world makes bodies eligible by default; `allowSleep:false` excludes a body.
1902
2217
  Paper comparisons keep sleeping disabled. Portable 3D solvers accept these
1903
2218
  same world and initial-body options.
1904
2219
 
2220
+ Small jointless 3D worlds with up to 32 bodies also batch all movement iterations
2221
+ into one GPU workgroup. Body poses stay in shared memory; contacts remain in GPU
2222
+ storage. The equations, contact order, lane reductions and iteration count stay
2223
+ the same. Adding bodies beyond that limit or adding a joint automatically selects
2224
+ the ordinary movement pipeline. Mobile compatibility paths retain their qualified
2225
+ pipeline. `smallGroups: false` disables this batching for a controlled comparison.
2226
+
2227
+ In two RTX 4070 Laptop GPU runs of the half-million-wall scene, connected-group
2228
+ waking took **9.90–10.14 ms** per later impact step, versus **55.05–55.28 ms**
2229
+ with whole-world waking. Both used four iterations and the same sleep thresholds;
2230
+ 457,095 bricks remained asleep with connected groups. Fully awake steps took
2231
+ 53.91–54.24 ms with connected groups and 53.39 ms with whole-world waking.
2232
+ These numbers include sleep checks and exclude drawing. This is a sleeping
2233
+ comparison, separate from the paper's awake workload. See the
2234
+ [measurements](test-results/connected-sleep-performance.json) and
2235
+ [visual comparison](test-results/solver-performance.html).
2236
+
2237
+ ![Half-million wall physics times with whole-world and connected-group waking on an RTX 4070 Laptop GPU](test-results/screenshots/solver-connected-sleep-1440.png)
2238
+
2239
+ ```sh
2240
+ # From a repository checkout: two runs, 480 steps per world, 4 iterations, no drawing.
2241
+ npm run benchmark:connected-sleep
2242
+ # Compare identical small-world physics with separate and batched movement.
2243
+ npm run benchmark:small-groups
2244
+ ```
2245
+
1905
2246
  The [browser performance page](tests/performance.html) has a separate sleeping
1906
2247
  benchmark, including the cost of sleep checks while bodies are awake. It reports
1907
2248
  the number of boxes asleep and the difference in their final positions. See
@@ -2136,8 +2477,7 @@ nearest-contact matching and no extra position-correction pass. The original
2136
2477
  and defaults to `alpha: 0.99` plus a final correction pass. For an explicit
2137
2478
  comparison, construct `AvbdScene2D({ alpha: 0.99, postStabilize: true,
2138
2479
  matchNearest: false })`. This changes solver settings, not the execution
2139
- backend; the package still steps on the GPU. Neither option changes the core
2140
- AVBD method.
2480
+ backend selected by the pipeline. These settings preserve the core AVBD method.
2141
2481
 
2142
2482
  ## 2D sleeping and hinge stops
2143
2483
 
@@ -2220,11 +2560,19 @@ checks, and shows the cost while every body is awake as well as after settling.
2220
2560
 
2221
2561
  After `npm start`, open [Slingshot siege](index.html?demo=slingshot).
2222
2562
  Pull the red ball back and release; Space fires, Next ball reloads and Rebuild
2223
- restores the forts. Choose 1,000, 5,000 or 10,000 bricks. Target counts and bricks
2563
+ restores the forts. A dashed trajectory line follows your aim; the full game
2564
+ also marks the first predicted obstacle using batched circle casts. Mouse and
2565
+ touch dragging work in the full game and the worker comparison view, including
2566
+ shared input. Choose 1,000, 5,000 or 10,000 bricks. Larger scenes use finer courses
2567
+ with the same practical building height. Select a target and press
2568
+ **Aim at target** for a high arc, or aim manually. Targets turn gold when
2569
+ knocked out by strong hits from the ball, falling debris, or a long fall.
2570
+ Smaller impacts accumulate damage, and defeated enemies stay defeated. Clear
2571
+ every target to win. Target counts and bricks
2224
2572
  moved show the impact; the collapsible display shows frame rate, contacts,
2225
2573
  sleeping bodies and drawing time. Toggle Sleeping to compare the same scene.
2226
2574
  Scroll to zoom; Shift-drag pans the view. Boost gives each launched ball one
2227
- extra impulse and a short thrust. The fixed timestep is 1/120 second with ten
2575
+ extra impulse and a short thrust. The fixed timestep is 1/120 second with sixteen
2228
2576
  solver rounds; the meter shows the actual physics rate alongside frame rate.
2229
2577
 
2230
2578
  The level uses every supported category of 2D physics:
@@ -2233,7 +2581,7 @@ The level uses every supported category of 2D physics:
2233
2581
  | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
2234
2582
  | Boxes, circles, capsules, convex hulls, segments and planes | Bricks, balls, chain links, roof tiles, launcher and ground |
2235
2583
  | Joints, world anchors and ropes | Hanging targets and linked ragdoll limbs |
2236
- | Welds and fracture | Small masonry panels and roof tiles use locked joints with a break force; impacts break the mortar |
2584
+ | Welds and fracture | Connected masonry walls, foundations and roof tiles use locked joints with a break force; impacts break the mortar |
2237
2585
  | Springs | Elastic platforms under the small ragdolls |
2238
2586
  | Limited hinges and motors | Powered blue gates reverse at their angle stops; toggle Powered gates |
2239
2587
  | Friction and restitution | Bricks grip their supports; Bouncy ball changes the projectile's bounce |
@@ -2246,8 +2594,8 @@ The renderer draws the GPU body buffer directly. Periodic diagnostic copies coun
2246
2594
  moved bricks, update gate controls and show broken supports; drawing does not
2247
2595
  download poses. Aiming queries run only while aiming. Predictions sample the
2248
2596
  current scene and do not provide continuous collision detection.
2249
- The source is in `demo/slingshotScene2D.js` (level construction) and
2250
- `demo/slingshotDemo.js` (GPU controls, events and queries).
2597
+ The source is in `demo/slingshot/scene.js` (level construction) and
2598
+ `demo/slingshot/index.js` (GPU controls, events and queries).
2251
2599
  Its `2d-slingshot-siege` scene is also in the all-scene benchmark,
2252
2600
  which keeps sleeping off for consistent solver measurements.
2253
2601
 
@@ -2362,6 +2710,388 @@ setup and submission off the UI thread; they do not accelerate GPU kernels.
2362
2710
  For large scenes, keep direct GPU rendering in that worker instead of copying
2363
2711
  every pose back to the page.
2364
2712
 
2713
+ ## Static terrain: hills, caves and overhangs
2714
+
2715
+ The 32-vertex limit applies to one convex collider. A terrain can contain many
2716
+ small static pieces, preserving hills, concavities, openings and overhangs.
2717
+ Play [Hills, cave and overhang](index.html?demo=canonical&scene=3d-static-terrain&backend=gpu).
2718
+
2719
+ ![Rigid bodies on a heightfield beside an open stone shelter](test-results/screenshots/3d-static-terrain-gpu.png)
2720
+
2721
+ Heightfields use two triangular prisms per grid cell, extending to a solid base.
2722
+ Mesh terrain uses one thin prism per nondegenerate triangle, with backing behind
2723
+ the triangle's winding. The original front surface stays at the supplied vertices.
2724
+ Both use the existing GPU convex contacts and grid/HPLOC collision search; this
2725
+ is a tiled collision representation, not a dedicated triangle-mesh collision kernel.
2726
+ Internal tile edges can influence sliding contacts. Choose a collision mesh with
2727
+ enough detail for the bodies it supports, rather than using every rendering triangle.
2728
+
2729
+ ```js
2730
+ import { AvbdScene3D, createSolverPipeline } from "avbd-babylon/native";
2731
+
2732
+ const scene = new AvbdScene3D({ timeStep: 1 / 120, iterations: 5 }); // y up
2733
+ const columns = 65,
2734
+ rows = 65;
2735
+ const heights = Float32Array.from({ length: columns * rows }, (_, i) => {
2736
+ const x = i % columns,
2737
+ z = Math.floor(i / columns);
2738
+ return 2 * Math.sin(x / 12) * Math.cos(z / 15);
2739
+ });
2740
+ const terrain = scene.addHeightfield({
2741
+ heights,
2742
+ columns,
2743
+ rows,
2744
+ spacing: 2,
2745
+ origin: [-64, 0, -64],
2746
+ baseHeight: -5,
2747
+ friction: 0.65,
2748
+ // Optional holes: one nonzero entry per cell removes both triangles.
2749
+ });
2750
+ scene.addSphere(0.5, { position: [0, 8, 0] });
2751
+ const pipeline = await createSolverPipeline({ backend: "webgpu" });
2752
+ const solver = scene.createSolver(pipeline, { broadphase: "hploc" });
2753
+ solver.advance(1 / 60); // two fixed steps
2754
+ // terrain.dispose(); disables collisions without shifting body indices.
2755
+ // pipeline.destroy(); releases the solver and its owned GPU device.
2756
+ ```
2757
+
2758
+ For an arbitrary static mesh, use world-space vertices and triangle indices:
2759
+
2760
+ ```js
2761
+ const cave = scene.addStaticMesh({
2762
+ positions: collisionVertices, // xyz triples; concave geometry is retained
2763
+ indices: collisionIndices, // omit for nonindexed triangles
2764
+ thickness: 0.15, // finite backing, in world units
2765
+ friction: 0.6,
2766
+ });
2767
+ ```
2768
+
2769
+ Add terrain before creating the solver. Static mesh backing is finite; it does
2770
+ not fill an entire mountain or implement continuous collision detection. Heights
2771
+ are row-major, `origin` offsets the grid, and `upAxis` defaults to 1 (Y); use 2
2772
+ for Z-up worlds. Terrain also works with the CPU pipeline, with higher costs.
2773
+
2774
+ Terrain cooking uses each triangle prism's known topology and computes its mass
2775
+ properties in a local frame. All source triangles and their backing thickness
2776
+ are retained, including caves and overhangs.
2777
+
2778
+ In a Babylon world, the same heightfield helper is available. An existing Babylon
2779
+ mesh can supply its triangles directly, including its current world transform:
2780
+
2781
+ Keep the original mesh visible. `addStaticMesh()` cooks its triangles into static
2782
+ collision pieces; it does not replace the rendered asset with those pieces or
2783
+ turn a cave into one solid convex hull. The terrain demo draws the original
2784
+ ground and cave surfaces with smooth lighting, while the collision pieces stay
2785
+ hidden. Its **Load OBJ or terrain JSON** control accepts OBJ files (Y up) or JSON containing
2786
+ `positions` and `indices` (Z up), centered and scaled for the preview. The demo
2787
+ import is limited to 10,000 triangles; production projects can load assets with
2788
+ their usual Babylon loader and pass the resulting mesh to the helper below.
2789
+
2790
+ ```js
2791
+ const physics = await AvbdPhysics.create({
2792
+ scene: babylonScene,
2793
+ capacity: 20_000, // includes terrain pieces and dynamic bodies
2794
+ });
2795
+ const ground = physics.addStaticMesh(groundMesh, {
2796
+ thickness: 0.2,
2797
+ friction: 0.7,
2798
+ });
2799
+ // Or: physics.addHeightfield({ heights, columns, rows, spacing: 2 });
2800
+ // Add dynamic aggregates before the first render/initialize.
2801
+ // ground.dispose(); removes collision participation; groundMesh remains yours.
2802
+ ```
2803
+
2804
+ Terrain is static and does not follow later mesh edits. Each piece has a body
2805
+ identity for masks, contacts and queries; there is no per-frame mesh synchronization
2806
+ for terrain. Reserve enough `capacity` before initialization. For large assets,
2807
+ `cookHeightfield()` and `cookStaticMesh()` return serializable geometry that can
2808
+ be prepared in another worker and registered with `addTerrain(cooked, material)`.
2809
+ Cooking happens once, outside the simulation loop.
2810
+
2811
+ ![Smooth hills and an open rock cave, with hidden collision pieces](test-results/screenshots/terrain-organic.png)
2812
+
2813
+ The Babylon mesh helper uses vertex normals to keep the backing behind the
2814
+ visible surface, including mirrored transforms. Meshes without normals use
2815
+ their triangle winding to define the front surface.
2816
+
2817
+ ## Ships, water and destruction
2818
+
2819
+ Play [Little ships, big trouble](index.html?demo=naval). Two composite ships trade
2820
+ cannon fire on a 128 × 128 shallow-water grid. Individual planks, deck strips,
2821
+ cabins and masts are welded rigid bodies. Hits release nearby pieces and leave hull
2822
+ openings. Each member's own submerged solid volume produces buoyancy; welds
2823
+ combine the lift from wooden pieces to support a dense keel. Released wood
2824
+ retains its buoyancy while the unsupported keel sinks. There is no flooding
2825
+ timer or trapped-air multiplier. Hull fragments persist until Reset.
2826
+
2827
+ ![Two composite ships on simulated water](test-results/screenshots/naval-afloat.png)
2828
+ ![A ship breaking against the island](test-results/screenshots/naval-island.png)
2829
+ ![Detached wood floating above a sunken keel](test-results/screenshots/naval-separated.png)
2830
+
2831
+ Water height and flow feed four buoyancy samples per piece. Lift, drag and their
2832
+ lever arms produce translation, pitch and roll. Water, ship forces, damage and
2833
+ AVBD collisions run on WebGPU; drawing reads GPU poses directly. Wakes use
2834
+ approximate momentum forcing. This surface-water model does not resolve detailed
2835
+ underwater flow or watertight fluid inside arbitrary mesh interiors.
2836
+
2837
+ The rocky island has real static collision shapes. Hard contact loads can break
2838
+ the hull's welds. Use the island buttons to drive either ship ashore, or release
2839
+ the Coral hull's welds to compare floating wood with its sinking keel. Each sail
2840
+ is a spring-connected cloth grid attached to its yard and pushed by gusting wind.
2841
+ Sail particles have lightweight, buoyant material with an effective displacement
2842
+ thickness, so detached fabric floats and drifts rather than sinking. Water drag
2843
+ damps submerged fabric, and submerged portions receive less wind force.
2844
+
2845
+ The demo defaults to a dedicated worker when OffscreenCanvas is available. Its
2846
+ scope selector also offers main-thread execution and optional shared input.
2847
+ Automatic solid-collision selection uses HPLOC for this scene’s mix of large
2848
+ island rocks, ship parts and small sail particles. The existing joint graph is
2849
+ colored at startup to avoid unnecessary solver dispatches; live contacts still
2850
+ use GPU coloring. Both explicit grid and HPLOC modes remain available.
2851
+ See the [scene builder](demo/naval/scene.js), [water integration](demo/naval/water.js)
2852
+ and [water/force shader](demo/naval/water.wgsl). These are demo components; the
2853
+ general package API remains the rigid-body and constraint API.
2854
+
2855
+ ```sh
2856
+ npm run test:naval # settling, cannon impacts, grounding and floating debris
2857
+ npm run benchmark:naval # matched grid / HPLOC physics, water and drawing timings
2858
+ ```
2859
+
2860
+ ## Worker and shared-memory benchmarks
2861
+
2862
+ Open the [interactive worker comparison](tests/workers.html). It can play the
2863
+ canonical scenes using direct GPU drawing or Babylon WebGPU thin instances,
2864
+ with physics and drawing together on the page or in a dedicated worker. CPU
2865
+ comparisons use Canvas 2D drawing in 2D and WebGPU drawing with pose uploads in
2866
+ 3D. CPU 2D worker execution does not require WebGPU.
2867
+
2868
+ The scene browser and slingshot demo include an Execution selector that opens
2869
+ the matching scene in this worker play/benchmark view. Main thread, dedicated
2870
+ worker and shared-input worker can each be played or measured individually.
2871
+ The shared-input choice appears only when the host supports SharedArrayBuffer
2872
+ and cross-origin isolation. The Full scene controls link returns to the demo.
2873
+
2874
+ The main benefit of a CPU worker is responsive controls. It runs the same solver;
2875
+ moving it to a worker does not turn it into a parallel solver. The tables below
2876
+ show simulation time and page responsiveness separately. Shared input had no
2877
+ consistent additional speed advantage in these runs. Measurements include
2878
+ the naval scene, 100K boxes, a 10K 2D pile and Babylon rendering:
2879
+ [recorded results](test-results/worker-performance.json).
2880
+
2881
+ Worker timings are estimates and can fluctuate with browser scheduling,
2882
+ background activity and GPU clock speeds. Repeat runs before judging small
2883
+ differences. Workers are generally less affected by busy page controls and other
2884
+ main-thread work, so updates can be steadier. They still share the computer’s
2885
+ resources and do not guarantee faster or more consistent GPU timings.
2886
+
2887
+ <!-- worker-results:start -->
2888
+ Physics per step / drawing per frame, in **milliseconds**. Each cell shows the range of the two runs’ medians. Compare execution modes within a row; different rows use different workloads.
2889
+
2890
+ | Scene · physics · drawing | Main thread | Dedicated worker | Worker + shared input |
2891
+ | --- | ---: | ---: | ---: |
2892
+ | Hills and overhang · WebGPU · Direct drawing | 1.12–3.53 / 0.03–0.09 | 0.75–0.79 / 0.02 | 1.74–2.26 / 0.05 |
2893
+ | Ships, island and cloth sails · WebGPU · Direct drawing | 2.31–3.19 / 0.11–0.19 | 1.97–2.03 / 0.14–0.16 | 2.06–2.10 / 0.11 |
2894
+ | 100K box columns · WebGPU · Direct drawing | 3.05–3.13 / 1.02 | 3.02–3.38 / 1.02–1.05 | 3.06–3.32 / 1.02–1.07 |
2895
+ | 10K 2D pile · WebGPU · Direct drawing | 3.28–4.35 / 0.02–0.03 | 4.22–4.47 / 0.03 | 3.40–4.49 / 0.02–0.03 |
2896
+ | 3D pyramid · WebGPU · Babylon thin instances | 1.82–2.06 / 0.09 | 1.60–2.53 / 0.15 | 2.13–2.14 / 0.10 |
2897
+ | 2D pyramid · WebGPU · Babylon thin instances | 2.57–2.64 / 0.09–0.12 | 3.19–3.23 / 0.12–0.13 | 3.08–3.10 / 0.12 |
2898
+ | 3D pyramid · CPU · Direct drawing | 15.66–17.55 / 0.05 | 15.57–17.80 / 0.05–0.08 | 17.37–18.38 / 0.06–0.08 |
2899
+ | 2D joint grid · CPU · Canvas 2D | 4.96–5.20 / 0.57–0.62 | 4.97–5.19 / 0.58–0.68 | 4.49–4.74 / 0.58–0.64 |
2900
+
2901
+ Page delay p95, in **milliseconds**. Lower means the page’s controls get more frequent opportunities to respond. This includes browser timer scheduling.
2902
+
2903
+ | Scene · physics · drawing | Main thread | Dedicated worker | Worker + shared input |
2904
+ | --- | ---: | ---: | ---: |
2905
+ | Hills and overhang · WebGPU · Direct drawing | 1.40–1.53 | 1.04–1.12 | 1.14–1.20 |
2906
+ | Ships, island and cloth sails · WebGPU · Direct drawing | 1.10–1.66 | 1.08–1.38 | 1.17–1.36 |
2907
+ | 100K box columns · WebGPU · Direct drawing | 1.45–1.58 | 1.00–1.18 | 0.98–1.29 |
2908
+ | 10K 2D pile · WebGPU · Direct drawing | 1.29–1.54 | 1.04–1.36 | 0.93–1.14 |
2909
+ | 3D pyramid · WebGPU · Babylon thin instances | 1.54–1.75 | 1.34–1.51 | 1.03–1.08 |
2910
+ | 2D pyramid · WebGPU · Babylon thin instances | 1.65–1.75 | 1.22–1.26 | 1.05–1.57 |
2911
+ | 3D pyramid · CPU · Direct drawing | 19.54–20.61 | 1.09–1.18 | 1.01–1.24 |
2912
+ | 2D joint grid · CPU · Canvas 2D | 6.42–7.08 | 1.13–1.64 | 1.11–1.60 |
2913
+
2914
+ Recorded on **Intel Core Ultra 7 155H / RTX 4070 Laptop GPU**. All modes use the same scene, timestep, iterations, canvas and renderer. Physics and rendering run together in the selected scope. SharedArrayBuffer carries input only; it does not share GPU buffers or parallelize the solver.
2915
+ <!-- worker-results:end -->
2916
+
2917
+ ![Worker comparisons with recorded CPU physics and page-delay results](test-results/screenshots/workers.png)
2918
+
2919
+ ![Main-thread, worker and shared-input timings for direct and Babylon drawing](test-results/screenshots/solver-workers-1440.png)
2920
+
2921
+ ```sh
2922
+ npm run benchmark:workers
2923
+ npm run test:workers
2924
+ ```
2925
+
2926
+ The comparison warms up for 30 frames and measures 120 frames at one physics
2927
+ step per frame. It sends 120 pointer updates per second; main, message-worker
2928
+ and shared-input modes use the same scene, canvas, timestep and iterations.
2929
+ WebGPU times come from GPU timestamps; CPU physics uses wall-clock timing.
2930
+ Repeat on the target device before choosing a mode.
2931
+
2932
+ Shared input uses a small atomic mailbox for the latest pointer position and
2933
+ wheel total. Clicks, taps, keys, resize and commands are forwarded separately.
2934
+ Discrete event boundaries prevent newer pointer positions from overtaking a
2935
+ click. Shared memory does not share GPUBuffer objects or speed up GPU kernels.
2936
+ Keeping rendering in the physics worker avoids transferring all poses each frame.
2937
+
2938
+ SharedArrayBuffer requires cross-origin isolation. `npm start` supplies COOP/COEP
2939
+ headers, and [netlify.toml](netlify.toml) supplies the same headers when hosted.
2940
+ The ordinary message mode remains usable without shared memory. Cross-origin
2941
+ assets must be served with compatible CORS/CORP headers under this configuration.
2942
+
2943
+ A worker pool is useful for independent startup jobs, such as cooking terrain
2944
+ chunks. Parallelizing the CPU collision search or constraint solver itself
2945
+ requires changes to ownership, scheduling and result merging; the package does
2946
+ not currently run one physics world across a worker pool. Shared pose arrays
2947
+ could reduce copying if rendering lives in a separate thread, but that is not
2948
+ what the shared-input benchmark measures.
2949
+
2950
+ The demo's reusable [simulation client](demo/shared/runtime/simulationClient.js),
2951
+ [worker runtime](demo/shared/runtime/simulationRuntime.js) and [input mailbox](src/utils/sharedInput.js)
2952
+ show lifecycle, pointer forwarding, resize, batched steps and explicit readbacks.
2953
+
2954
+ ## Reusable GPU and worker utilities
2955
+
2956
+ The reusable helpers used by the demos ship in the package. Import them from
2957
+ `avbd-babylon/utils` or the main entry; script-tag builds expose them on `AVBD`.
2958
+ They have no Babylon or DOM dependency and work in a dedicated worker.
2959
+
2960
+ | Utility | Purpose |
2961
+ | --- | --- |
2962
+ | `GpuSurfaceParticles` | The D20 demo's GPU particle sweeps, live particle buffer and optional asynchronous counters. |
2963
+ | `GpuTimer` | Nonblocking GPU timestamps for a compute or render pass. |
2964
+ | `GPUBufferRecycler` | Reuse released buffers with compatible usage and a free-memory budget. |
2965
+ | `GpuContactSample` | Gather a bounded set of contact dots directly on the GPU, rather than downloading the full contact set. |
2966
+ | `seedConstraintColoring` | Optional startup coloring for an existing native constraint graph; GPU coloring continues to check live contacts. |
2967
+ | `addGpuStepPass` | Append custom GPU forces or copies to the solver's existing submission, in 2D or 3D. Multiple features can register without overwriting each other. |
2968
+ | `WorkerCanvas` | DOM-free sizing and input-event adapter around an offscreen canvas. |
2969
+ | Shared input helpers | A nonblocking latest-state mailbox for continuous pointer input, with a message fallback when shared memory is unavailable. |
2970
+
2971
+ The active HPLOC collision tree, automatic work scheduling, bulk edits and
2972
+ selective pose reads are already part of the normal solver pipeline under
2973
+ [`src/`](src/). Example scenes, cameras, game rules and page controls remain
2974
+ in [`demo/`](demo/README.md).
2975
+
2976
+ ```js
2977
+ import { createWebGPUDevice } from "avbd-babylon";
2978
+ import { GpuSurfaceParticles } from "avbd-babylon/utils";
2979
+
2980
+ const { device } = await createWebGPUDevice();
2981
+ const particles = new GpuSurfaceParticles(device, {
2982
+ capacity: 100_000,
2983
+ triangleCount: 1,
2984
+ readback: false, // No CPU counters needed for drawing.
2985
+ });
2986
+ particles.writeTriangles(new Float32Array([
2987
+ -10, -10, 0, 0, // a, padded to vec4
2988
+ 10, -10, 0, 0, // b
2989
+ 0, 10, 0, 0, // c
2990
+ ]));
2991
+ particles.step({ dt: 1 / 60, count: 100_000, speed: 80, spawnRadius: 32 });
2992
+ // Bind particles.particleBuffer as read-only storage in your renderer.
2993
+ // Each 48-byte record contains position/life, velocity/radius, then a vec4u seed.
2994
+ // Use the same GPUDevice; no position downloads are required.
2995
+ // Call step() from your animation loop, then destroy() when the effect is removed.
2996
+ particles.destroy();
2997
+ device.destroy();
2998
+ ```
2999
+
3000
+ These particles spawn on a sphere and aim toward the origin. A surface hit or
3001
+ expired lifetime respawns the particle; particles ignore one another. Five
3002
+ offset rays approximate each sweep. See [the sprite renderer](demo/collision-benchmark/sprites.js)
3003
+ for a complete example of drawing the live buffer. This helper is separate
3004
+ from the rigid-body solver and its collision masks.
3005
+
3006
+ Custom GPU work can share the physics submission. This is the path used by
3007
+ the naval buoyancy and playground force demos:
3008
+
3009
+ ```js
3010
+ import { addGpuStepPass } from "avbd-babylon/utils";
3011
+
3012
+ // world is a WebGPU solver created from AvbdScene2D or AvbdScene3D.
3013
+ // Create your forcePipeline and forceBindings on the same GPUDevice.
3014
+ const removeForces = addGpuStepPass(world, {
3015
+ before(encoder, solver) {
3016
+ const pass = encoder.beginComputePass({ label: "Custom forces" });
3017
+ pass.setPipeline(forcePipeline);
3018
+ pass.setBindGroup(0, forceBindings);
3019
+ pass.dispatchWorkgroups(Math.ceil(solver.bodyCount / 64));
3020
+ pass.end();
3021
+ },
3022
+ });
3023
+ world.step(); // Forces and AVBD use one command submission.
3024
+ removeForces(); // Remove this registration before destroying its resources.
3025
+ ```
3026
+
3027
+ Callbacks run synchronously and must end their own passes. Do not finish or
3028
+ submit the provided encoder. An `after` callback can gather results after
3029
+ physics. Recreate your bindings if the solver grows and replaces a buffer;
3030
+ wake affected bodies when activating a new force. CPU solvers reject GPU passes.
3031
+
3032
+ ```js
3033
+ import { createSharedInput, writeSharedInput, readSharedInput } from "avbd-babylon/utils";
3034
+
3035
+ const shared = createSharedInput(); // null without COOP/COEP support
3036
+ if (shared) {
3037
+ // Pass shared.buffer to your worker through postMessage, without transferring it.
3038
+ writeSharedInput(shared, { clientX: 120, clientY: 80, buttons: 1 });
3039
+ // In the worker, use new Int32Array(message.input) and retain the last sequence.
3040
+ const state = readSharedInput(shared, -1);
3041
+ if (state) console.log(state.event, state.sequence);
3042
+ }
3043
+ ```
3044
+
3045
+ The mailbox has one writer and one reader. Keep clicks, releases and key events
3046
+ in ordered messages; coalescing them would lose discrete actions. Send normal
3047
+ messages when `createSharedInput()` returns null.
3048
+
3049
+ `WorkerCanvas` wraps your transferred `OffscreenCanvas` with `clientWidth`,
3050
+ `clientHeight`, `resize()` and an event surface. Call `dispatch(type, event)`
3051
+ for mirrored pointer events. The [worker initialization](demo/shared/runtime/simulationWorker.js)
3052
+ and [page-side input bridge](demo/shared/runtime/simulationClient.js) use these
3053
+ same package helpers; their scene catalog and controls stay in the demo.
3054
+
3055
+ For timestamps, call `timer.begin()`, spread `timer.descriptor(slot)` into your
3056
+ pass descriptor, and call `timer.encode(encoder, slot)` after ending the pass.
3057
+ Submit the encoder, then call `timer.resolve(slot)`. `latestMs` stays null when
3058
+ timestamp queries are unavailable. The demo [particle pass](src/gpu/surfaceParticles.js)
3059
+ shows this sequence. Buffer recycling preserves contents: overwrite or clear
3060
+ reused storage before expecting zeroes, and release it only after encoding its
3061
+ last use. Destroy helpers when their owning world or renderer is disposed.
3062
+
3063
+ ## D20 particle capacity
3064
+
3065
+ Run `npm run benchmark:particles` to search for the particle count that maintains
3066
+ 60 FPS, refine the boundary and repeat both sides. It uses the normal visible
3067
+ sprites at 1440 × 900 with unchanged particle radius, speed and collision work.
3068
+ The [saved report](test-results/particle-capacity.html) separates collision GPU
3069
+ time, sprite GPU time and total frame rate. A drawing-disabled run helps show
3070
+ whether rendering contributes to the limit.
3071
+
3072
+ <!-- particle-capacity-results:start -->
3073
+ Saved reference: **NVIDIA GeForce RTX 4070 Laptop GPU**, 1440 × 900, device pixel ratio 1. 8,000,000 visible particles maintained about 60 FPS in every recorded run at that count, including three longer confirmation runs. Some runs missed the target from 8,792,000 particles upward. Near the limit, browser scheduling and GPU clocks can change the result.
3074
+ <!-- particle-capacity-results:end -->
3075
+
3076
+ Open the [particle demo](index.html?demo=benchmark) to play or set counts manually.
3077
+ For a fixed-count measurement, use its browser console:
3078
+
3079
+ ```js
3080
+ await __COLLISION_BENCHMARK__.measure({
3081
+ count: 200_000,
3082
+ warmupSeconds: 2,
3083
+ seconds: 8,
3084
+ draw: true,
3085
+ });
3086
+ ```
3087
+
3088
+ The result reports actual elapsed frame times, GPU pass timings, live particle
3089
+ counts and errors. A sustained-60 result allows display timing tolerance:
3090
+ at least 59 FPS and 95% of frames below 25 ms. Repeat runs near the boundary;
3091
+ resolution, GPU clocks, browser scheduling and sprite coverage affect capacity.
3092
+ This measures surface-only particle sweeps, rather than AVBD solving or
3093
+ particle-to-particle contacts.
3094
+
2365
3095
  ## Benchmark replication
2366
3096
 
2367
3097
  Start this repository with `npm start`. Play scenes while the benchmark is
@@ -2440,7 +3170,18 @@ Each run starts fresh and checks contact storage, parallel-work conflicts and
2440
3170
  finite poses. GPU stages measure finding contacts, preparing them, planning
2441
3171
  parallel work and moving objects. CPU submission and startup have separate
2442
3172
  fields; wall time also includes waiting for measurements. These are physics
2443
- timings, not rendered FPS. The [integration guide](docs/README.md#reproduce-the-collision-and-solver-benchmarks)
3173
+ timings, not rendered FPS. Interactive playground benchmarks automatically start
3174
+ their action (wind, domino tipping or marble release). GPU stage timings cover
3175
+ the solver; submission and completion times also include their force/feed work.
3176
+ The Galton board holds all 40,000 marbles visibly in its hopper until the gate
3177
+ opens. One GPU dispatch enables their collisions; gravity drains the hopper
3178
+ without teleporting marbles into the peg field. Its benchmark records how many
3179
+ marbles are still in the hopper and how many have passed the outlet, so early
3180
+ discharge samples are distinguishable from runs with all marbles below it.
3181
+ The hopper has a narrow central outlet and a short vertical throat above the
3182
+ first peg. The bin chart counts actual marble positions and overlays a fitted
3183
+ normal curve. Crowding and retracted pegs can change the distribution.
3184
+ The [integration guide](docs/README.md#reproduce-the-collision-and-solver-benchmarks)
2444
3185
  includes downloads and profiling commands.
2445
3186
 
2446
3187
  Saved reports were recorded on an **NVIDIA GeForce RTX 4070 Laptop GPU**. Live
@@ -2468,8 +3209,13 @@ storage buffers to the vertex shaders. Instance matrices are uploaded once.
2468
3209
  Colors are copied again only when edited. Camera controls, contact overlays,
2469
3210
  shape sizes and cloth tearing use the viewer's existing geometry and shaders.
2470
3211
 
2471
- The [Babylon renderer source](demo/babylonGpuRenderer.js) and
2472
- [WGSL adapter](demo/babylonShaders.js) show the integration. This is a custom
3212
+ The direct WebGPU viewer also batches unique hull geometry into a single draw.
3213
+ In scenes with many cloth particles or hidden collision pieces, its box draw
3214
+ contains only visible boxes. Box-only stress scenes use a contiguous
3215
+ instance layout.
3216
+
3217
+ The [Babylon renderer source](demo/shared/rendering/babylonGpuRenderer.js) and
3218
+ [WGSL adapter](demo/shared/rendering/babylonShaders.js) show the integration. This is a custom
2473
3219
  Babylon shader renderer; ordinary Babylon materials use the matrix-update path
2474
3220
  shown in [Large pile with thin instances](#large-pile-with-thin-instances).
2475
3221
  The demo isolates its access to Babylon 9's internal `_device` field in
@@ -2501,9 +3247,7 @@ try {
2501
3247
  babylonWebGPU: result.babylon,
2502
3248
  });
2503
3249
  } finally {
2504
- await __BENCHMARK_VIEW__.preview.load(
2505
- document.getElementById("scene").value,
2506
- );
3250
+ await __BENCHMARK_VIEW__.preview.load(document.getElementById("scene").value);
2507
3251
  }
2508
3252
  ```
2509
3253
 
@@ -2515,10 +3259,10 @@ RTX 4070 Laptop GPU and links the raw measurements and every scene screenshot.
2515
3259
 
2516
3260
  ![Renderer timing comparison](test-results/screenshots/renderer-comparison-1440.png)
2517
3261
 
2518
- | Babylon: 2D shapes | Babylon: torn fabric |
2519
- | --- | --- |
2520
- | ![Babylon 2D mixed shapes](test-results/screenshots/babylon-2d-mixed-shapes.png) | ![Babylon torn cloth with detached patch](test-results/screenshots/babylon-cloth-torn.png) |
2521
- | Babylon: ragdolls on a net | Babylon: 100K columns |
3262
+ | Babylon: 2D shapes | Babylon: torn fabric |
3263
+ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
3264
+ | ![Babylon 2D mixed shapes](test-results/screenshots/babylon-2d-mixed-shapes.png) | ![Babylon torn cloth with detached patch](test-results/screenshots/babylon-cloth-torn.png) |
3265
+ | Babylon: ragdolls on a net | Babylon: 100K columns |
2522
3266
  | ![Babylon ragdolls and rigid net](test-results/screenshots/babylon-showcase-ragdolls-on-cloth-24k.png) | ![Babylon 100K box columns](test-results/screenshots/babylon-showcase-box-columns-100k.png) |
2523
3267
 
2524
3268
  See the [live comparison controls](test-results/screenshots/renderer-live-comparison.png),
@@ -2590,6 +3334,10 @@ npm run test:web # GPU scenes, benchmarks, controls and reports
2590
3334
  npm run test:features # Playable 2D/3D hinge, motor, trigger, mask and cast checks
2591
3335
  npm run benchmark:native # Bulk edits, sleeping and selected reads; three repeats
2592
3336
  npm run benchmark:native-edits # 100K motion edits, 10K property changes and batched queries
3337
+ npm run benchmark:gpu-uploads # WebGPU setting preparation and upload overhead
3338
+ npm run benchmark:gpu-scheduling # GPU color-bucket timing and exact-order checks
3339
+ npm run benchmark:movement # Movement stages, paired timings and exact state checks
3340
+ npm run benchmark:gpu-encoding # CPU setup for GPU steps, excluding GPU/drawing time
2593
3341
  ```
2594
3342
 
2595
3343
  Results and screenshots are generated in `test-results/`. Normal physics tests