openvibers/openvibe.games Commits

Games: physics loads as a wasm file in the browser (M6: no 4.4 MB base64 chunk) (#61)

OP OpenVibers committed 30 days ago · 647ea29238 · 5 files, +49 −4

Plan T8 / ADR-0007 M6 (client floor), first part.

**Physics in the browser comes as wasm, not base64 JavaScript**
- The client build aliases `@dimforge/rapier3d-deterministic-compat` to `@dimforge/rapier3d-deterministic`: the same version (0.21.0), the same engine, so the same deterministic results.
- With `vite-plugin-wasm`, the wasm is a file of its own: 3.1 MB, 1.18 MB gzip. It used to be a 4.37 MB JS chunk of base64 (1.65 MB gzip) that the browser had to parse before decoding.
- Node (the server, the tests) keeps the compat build.
- `loadRapier` calls `init()` only when the module has one: the non-compat module instantiates as it loads.
- The game server already serves `.wasm` as `application/wasm`.

**Checked**
- `pnpm build` is clean. Bundle budget: initial JS 2.35 MB of 2.5 (unchanged), and the Rapier chunk is gone from the over-limit list.
- A headless Chrome smoke loads the built Rapier chunk from a static server and steps a world: a ball falls from 10 to 5.075 in 60 steps.
- Full vitest: 120 files, 1,257 tests.

**Still over 400 KB:** Babylon's core chunk (2.09 MB). A split by Babylon folder was tried and refused (circular chunks; it pulled lazy shaders into the initial graph, 3.97 MB). This is recorded in ADR-0007 with what M6 still has.

Changes

ddiff --git a/apps/client/package.json b/apps/client/package.json
iindex dd56540..7495e4b 100644
---- a/apps/client/package.json
++++ b/apps/client/package.json
@@@ -14,6 +14,7 @@
"@babylonjs/loaders": "^9.20.0",
"@babylonjs/materials": "^9.20.0",
"@babylonjs/procedural-textures": "^9.20.0",
++ "@dimforge/rapier3d-deterministic": "0.21.0",
"@openvibe/casual": "workspace:*",
"@openvibe/content": "workspace:*",
"@openvibe/gameplay": "workspace:*",
@@@ -23,6 +24,7 @@
},
"devDependencies": {
"babylonjs-gltf2interface": "^9.20.0",
-- "vite": "^6.2.5"
++ "vite": "^6.2.5",
++ "vite-plugin-wasm": "3.6.0"
}
}
ddiff --git a/apps/client/vite.config.ts b/apps/client/vite.config.ts
iindex 8a5d495..5fd28e8 100644
---- a/apps/client/vite.config.ts
++++ b/apps/client/vite.config.ts
@@@ -1,7 +1,20 @@
import { defineConfig } from 'vite'
++import wasm from 'vite-plugin-wasm'
++import { fileURLToPath } from 'node:url'
export default defineConfig({
++ // Physics (plan M6): the deterministic Rapier build whose wasm is a file of its own, compiled by the browser as it
++ // streams in, instead of the compat build's 4.4 MB of base64 inside a JavaScript chunk. Same engine, same results.
++ // An absolute path: packages/physics imports the compat name and does not depend on this build itself.
++ resolve: {
++ alias: {
++ '@dimforge/rapier3d-deterministic-compat': fileURLToPath(
++ new URL('./node_modules/@dimforge/rapier3d-deterministic/rapier.js', import.meta.url),
++ ),
++ },
++ },
plugins: [
++ wasm(),
{
// Mirror the production server's pretty-URL aliases so the OAuth
// callback's redirect to /play works in dev too.
ddiff --git a/docs/adr/0007-rewrite-the-moddable-base.md b/docs/adr/0007-rewrite-the-moddable-base.md
iindex 831ac5f..0c495d6 100644
---- a/docs/adr/0007-rewrite-the-moddable-base.md
++++ b/docs/adr/0007-rewrite-the-moddable-base.md
@@@ -140,5 +140,12 @@ The Live legacy import (`platform/liveLegacyImport.ts`, `scripts/importLiveLegac
to Play. This local schema is the seed for Play's per-title results and unlocks;
the title-scoped principal/grant design in decision 10 remains the target.
- **M6: client floor.** Quality tiers, KTX2 and atlases, baked icons, bundle budget, editor E2E in CI.
++ *2026-10-14:* quality tiers and hardware scaling are in (`render/quality.ts`, the HUD's Graphics quality); initial
++ JS is 2.35 MB of 2.5 (579 KB gzip). The browser build now loads Rapier from the deterministic non-compat package
++ (Vite alias + `vite-plugin-wasm`): the same engine and results, its wasm a 3.1 MB file the browser compiles as it
++ streams (1.18 MB gzip) instead of a 4.37 MB JavaScript chunk of base64 (1.65 MB gzip). Node keeps the compat build.
++ Still over the 400 KB per-chunk line: Babylon's core (one 2.09 MB chunk: engines, materials, meshes, maths). A split
++ by Babylon folder was tried and refused: circular chunks, and it pulled the lazily loaded shaders into the initial
++ graph (3.97 MB). Left for M6: that chunk, KTX2 and atlases, baked icons, editor E2E in CI.
Each milestone is playable end to end and keeps the slice harness green.
ddiff --git a/packages/physics/src/rapier/load.ts b/packages/physics/src/rapier/load.ts
iindex 25517a9..305b80a 100644
---- a/packages/physics/src/rapier/load.ts
++++ b/packages/physics/src/rapier/load.ts
@@@ -3,14 +3,17 @@ import type { RapierModule } from './rapierWorld.js'
/**
* Loads and initializes the pinned deterministic Rapier build (ADR-0007
* decision 2): `@dimforge/rapier3d-deterministic-compat`, whose wasm is inlined
-- * as base64 so a single entry works in Node and the browser with no asset
-- * plumbing and no loader.
++ * as base64 so a single entry works in Node with no asset plumbing. The
++ * browser build aliases it to `@dimforge/rapier3d-deterministic` (plan M6: the
++ * wasm becomes its own file, so the 4.4 MB base64 chunk is gone).
*
* The import is dynamic on purpose: physics is loaded while the WebSocket
* ticket is fetched, so it is never part of the initial JavaScript.
*/
export async function loadRapier(): Promise<RapierModule> {
const R = (await import('@dimforge/rapier3d-deterministic-compat')) as unknown as RapierModule
-- await R.init()
++ // The browser build resolves this import to `@dimforge/rapier3d-deterministic` (apps/client/vite.config.ts): the same
++ // engine and wasm as a separate file, instantiated as the module loads, so it has no init() to call.
++ if (typeof R.init === 'function') await R.init()
return R
}
ddiff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
iindex 0d1de0d..76418c9 100644
---- a/pnpm-lock.yaml
++++ b/pnpm-lock.yaml
@@@ -56,6 +56,9 @@ importers:
'@babylonjs/procedural-textures':
specifier: ^9.20.0
version: 9.20.0(@babylonjs/[email protected])
++ '@dimforge/rapier3d-deterministic':
++ specifier: 0.21.0
++ version: 0.21.0
'@openvibe/casual':
specifier: workspace:*
version: link:../../packages/casual
@@@ -81,6 +84,9 @@ importers:
vite:
specifier: ^6.2.5
version: 6.4.3(@types/[email protected])([email protected])
++ vite-plugin-wasm:
++ specifier: 3.6.0
apps/server:
dependencies:
@@@ -289,6 +295,9 @@ packages:
'@dimforge/[email protected]':
resolution: {integrity: sha512-n7JuU62Obljlgq+OeH2DHhTmRSjRzwuJPl54tS9pFIzVQyvXsO5CH+a5KkIRBtnFWDbg1Zicv9BulG0jfBAopA==}
++ '@dimforge/[email protected]':
++ resolution: {integrity: sha512-ITZuCbM1hg4B1RXisactr9vQ+GU8UeuHQYXiv20itjt8aihV/lXhdkHqFJoloZJFyuY23gurCedICdWOMGguDw==}
++
'@electric-sql/[email protected]':
resolution: {integrity: sha512-n9tsbUOhwx2epK1V0ZG9Ar4SHWUju04dhmzZXiSBXwBoleOvIfals33NAaWgagQVAL4Rbvx/Ptsu3P+pA09f6Q==}
@@@ -1619,6 +1628,11 @@ packages:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
++ resolution: {integrity: sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==}
++ peerDependencies:
++ vite: ^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8
++
resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@@ -1829,6 +1843,8 @@ snapshots:
'@dimforge/[email protected]': {}
++ '@dimforge/[email protected]': {}
++
'@electric-sql/[email protected]': {}
'@esbuild/[email protected]':
@@@ -3013,6 +3029,10 @@ snapshots:
dependencies:
punycode: 2.3.1
++ dependencies:
++ vite: 6.4.3(@types/[email protected])([email protected])
++
dependencies:
esbuild: 0.25.12