Skip to content

Repository files navigation

physx-web

Unofficial TypeScript/WebAssembly port of NVIDIA PhysX for browser-based game engines. WASM-ABI release (0.1.0): the repository structure, public API surface and ownership semantics of the foundation, plus a deterministic TypeScript↔native ABI boundary (src/wasm/Abi.ts), a single-ArrayBuffer scene store (SharedBodyStore) and the first real backend (MinimalIntegrationBackend: genuine gravity/damping integration, nothing else). No PhysX simulation is implemented — and nothing here pretends otherwise.

Design principles

  1. PhysX concepts preserved — PxFoundation → PxPhysics → PxScene → PxRigidDynamic/PxRigidStatic → PxShape → PxGeometry/PxMaterial, plus PxJoint*, PxController, scene queries and serialization. Same names, same ownership direction, same descriptor-driven flow as PhysX 5.x.
  2. Public API separate from implementation — engine code imports only from src/index.ts. Solver/broadphase/narrowphase live behind IPhysicsBackend (src/wasm/PxBackend.ts). Swapping in the WASM core later changes zero call sites.
  3. Honest boundary, no fake physics — every solver/query method without an implementation throws PhysXNotImplementedError with a feature key ("PxScene.simulate", "PxScene.raycast", …). Validation errors throw PhysXValidationError; use-after-release throws PhysXReleasedObjectError.
  4. Typed-array data paths — API boundaries use plain structs (PxVec3, PxQuat, PxTransform); hot paths use packed Float32Array + explicit offsets/strides, never per-body objects. The ABI is specified byte-exactly in src/wasm/Abi.ts and owned as one ArrayBuffer per scene.
  5. Stable ABI boundary — TypeScript API → IPhysicsBackend → WASM ABI → native core. The ABI (magic, version, header, SoA sections, handles, status machine) is versioned and negotiated; a compiled PhysX core replaces MinimalIntegrationBackend behind IAbiSceneBackend with zero public-API change.

Repository structure

src/
  index.ts            # public barrel (only import surface engines need)
  core/               # Errors, Version, Ownership (PxRefCounted), PxMath
  foundation/         # PxFoundation, PxErrorCallback
  physics/            # PxPhysics factory hub
  scene/              # PxScene, PxSceneDesc, broadphase/scene flags
  actors/             # PxActor base, PxRigidStatic, PxRigidDynamic
  shapes/             # PxGeometry union + factories, PxShape
  materials/          # PxMaterial (friction/restitution)
  queries/            # PxRaycastHit/Sweep/Overlap, filter data
  constraints/        # PxJoint + joint types
  controllers/        # PxController (character controllers)
  serialization/      # descriptor snapshots (binary RepX explicitly unimplemented)
  wasm/               # PxBackend, Abi (stable boundary), SharedBodyStore,
                      # MinimalIntegrationBackend, MemoryLayout strides
examples/basic-simulation.ts            # intended API flow (default backend)
examples/minimal-backend-simulation.ts  # real integration via the ABI backend
tests/                # vitest: api-flow, ownership, validation-math,
                      # wasm-abi (raw-buffer level), minimal-backend (API level)

Ownership / lifetime

PxFoundation ─owns──▶ PxPhysics ─owns──▶ PxScene ─owns──▶ PxActor ─refs──▶ PxShape ─refs──▶ PxMaterial
  • Factories transfer ownership; callers release() (or use using, via Symbol.dispose).
  • release() is safe to call twice; any other use after release throws.
  • Releasing a parent does not cascade-release children (matches PhysX release ordering).
  • Shapes/materials are ref-counted: attachShape retains, detachShape/actor-release drops.

WASM-side ownership (authoritative rules, see src/wasm/Abi.ts)

Owner Holds Lifetime
JS-owned Descriptors, TS wrappers, actor→handle map, allocator free-list Created/released with the TS objects
Shared One ArrayBuffer per scene (64-byte header + SoA body sections) Bound at createScene, dead at destroyScene; TS writes descriptors pre-simulate, the core writes results pre-fetchResults — never both at once
Core-owned (opaque) Solver internals, broadphase, cooked meshes, constraints Referenced from TS only as u32 ids; the minimal backend owns none
Temporary transfer Small scratch views for one-record marshal Never retained across calls
  • Body handles are (generation:u16 << 16) | index:u16. destroyScene invalidates every handle; stale/double-free use throws, never aliases.
  • ABI major mismatch (or bad magic/length on attach) throws with key WasmAbi.version — a foreign core can never silently misread memory.

Quick start

import { createFoundation, createPhysics, pxBoxGeometry } from 'physx-web';

using foundation = createFoundation();
using physics = createPhysics({ foundation });
using scene = physics.createScene({ gravity: { x: 0, y: -9.81, z: 0 } });
using material = physics.createMaterial(0.5, 0.5, 0.1);
using body = physics.createRigidDynamic(
  { p: { x: 0, y: 5, z: 0 }, q: { x: 0, y: 0, z: 0, w: 1 } },
  1,
);
using shape = physics.createShape(pxBoxGeometry(0.5, 0.5, 0.5), [material]);
body.attachShape(shape);
scene.addActor(body);

scene.simulate(1 / 60); // default backend: throws PhysXNotImplementedError;
scene.fetchResults(true); // with MinimalIntegrationBackend: really integrates

Run it: npm run example (flow + boundary report) or npm run example:backend (real falling-body run through the ABI).

WASM ABI (stable boundary)

  • src/wasm/Abi.ts — magic 'PXWB', ABI version 1.0, 64-byte header (capacity, active count, sequence, Idle→Running→Complete→Destroyed status, last dt, shared gravity), SoA body sections (flags, generations, positions, quaternions, linear/angular velocity, inverse mass, diagonal inverse inertia, linear/angular damping), u16/u16 handles, version negotiation.
  • Total buffer: 64 + 84 × capacity bytes, one contiguous ArrayBuffer.
  • The minimal backend touches bodies only via this layout (SharedBodyStore cold paths, raw section views + i*3/i*4 strides hot). SharedBodyStore.attach() validates foreign buffers — the future wasm.memory path.

Native PhysX backend (phase 3 — structure complete, binary not yet built here)

  • native/include/physx_bridge.h + native/src/physx_bridge.cpp — a real C++ bridge over core PhysX (foundation/physics/scene/material/box actors, gravity, simulate/fetchResults, transform+velocity readback). Opaque u32 ids only, cascade destruction (actors → scene → materials → physics → foundation), last-error diagnostics. No extensions/cooking/PVD, no custom collision code — collision is performed by PhysX itself.
  • src/wasm/NativePhysXBackend.ts — IAbiSceneBackend over the bridge: same scene-state protocol as the minimal backend (register/push/step/pull through SharedBodyStore), explicit PhysXNativeError on any failure, never a silent fallback. Creation-time box extents/materials ride the WasmBodyState.shape hint (buffer layout unchanged); anything beyond boxes is rejected with a feature key.
  • Build: npm run build:native (Emscripten + CMake + PhysX SDK; fails with exit 2 naming the missing piece). Full toolchain/steps: native/README.md.
  • Tests: npm run test:native — mock adapter-contract suite (always runs; echo semantics, proves plumbing only) + real-binary suite (fall → collide → rest on real ground, handle lifecycle), which skips with a diagnostic when the artifact is absent.

Scripts

Script Command Purpose
npm run build tsc -p tsconfig.build.json Emit ESM + .d.ts to dist/
npm run typecheck tsc --noEmit Strict type check (incl. tests)
npm test vitest run Unit tests
npm run example tsx examples/basic-simulation.ts Intended API flow demo
npm run example:backend tsx examples/minimal-backend-simulation.ts Real ABI integration demo
npm run build:native node scripts/build-native.mjs Isolated native-bridge build (fails explicitly when toolchain/SDK missing)
npm run test:native vitest run tests/native-physx.test.ts Mock adapter suite + real-binary suite (skips honestly without artifact)

Strictness: strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride/noImplicitReturns — all on.

Trion Engine integration

  • Depend on the physx-web package root only; never deep-import modules.
  • Inject a backend once: createPhysics({ foundation, backend }) where backend is MinimalIntegrationBackend today and the compiled core tomorrow — call sites don't change (both implement IPhysicsBackend; ABI cores additionally implement IAbiSceneBackend).
  • Hot loops: use readGlobalPoseInto / readVelocitiesInto / BodySoABuffers instead of allocating structs per frame.
  • Catch PhysXNotImplementedError to detect unavailable features at runtime (e.g. disable CCD UI until the backend supports it).

Implementation boundary (what throws today)

Genuinely implemented and tested:

API Status
Foundation/Physics/Material/Shape/Actor creation, poses, velocity seeds, damping, mass, gravity storage, attach/detach, descriptors, serialization snapshots ✅ implemented
WASM ABI v1.0: header/sections/handles, version negotiation, foreign-buffer attach, status-machine sync ✅ implemented
MinimalIntegrationBackend: semi-implicit Euler (v += g·dt, v *= 1/(1+d·dt), p += v·dt in f32), static/kinematic skip, per-body gravity disable, scene stepping + result pull-back through the public API ✅ implemented (tiny subset — no interaction between bodies)
NativePhysXBackend over real PhysX SDK 5.1.3 compiled to wasm32: box actors, gravity, PxScene::simulate/fetchResults, transform+velocity readback through SharedBodyStore ✅ implemented + executed (see status report)
Mass/inverse-mass sync; solid-box inverse inertia for single-box dynamics (textbook formula), zeroed otherwise ✅ implemented

Still unsupported — throws PhysXNotImplementedError, never faked:

API Status
PxScene.simulate / fetchResults on the default backend; raycast/sweep/overlap on all backends (no queries in any backend yet) ❌ PhysXNotImplementedError
addForce/addTorque/setKinematicTarget/wakeUp/putToSleep, PxController.move, orientation integration, joints solving, CCD, sleeping ❌ PhysXNotImplementedError
Kinematic actors, non-box or multi-shape actors on the native bridge ❌ PhysXNotImplementedError (explicit phase scope)
Convex/triangle-mesh cooking, heightfields, binary RepX ❌ PhysXNotImplementedError

Status report (phase 4 — real execution)

IMPLEMENTED (built, executed, all green 2026-09-20):

  • Native C++ bridge (native/) compiled to WebAssembly and executed: native/build/physx_web_bridge.{js,wasm} (2.34 MB wasm).
  • NativePhysXBackend + loadEmscriptenBridgeModule + PhysXNativeError (mock suite 9/9; real suite 3/3 — see below).
  • Full suite: 70/70 tests pass, 0 skipped (was 67 + 3 skipped).
  • Reproducible native path: npm run build:physx (pinned SDK + reviewed 3-guard wasm32 patch) → npm run build:native → npm run test:native.

VERIFIED BY REAL EXECUTION (tests/native-physx.test.ts, real binary):

  • Foundation, physics, scene, material, static ground box, dynamic box: all initialize through the bridge.
  • Gravity transfer, PxScene::simulate, fetchResults, ABI readback of transform + velocities: all execute for real.
  • Collision: the box falls from y=5, never tunnels (min y > 0.9 over the trajectory), rests at y≈1.0 (one half-extent above the ground top face) with near-zero velocity; the static ground never moves.
  • Lifetime: release order, scene-death invalidation, double-free rejection, stale-generation rejection: all pass against the native backend.

UNSUPPORTED: queries on all backends; forces/torques/kinematic targets/ sleep API; joints, controllers, CCD, cooking, heightfields, binary RepX; non-box/multi-shape/kinematic actors on the native bridge.

REMAINING TOOLCHAIN LIMITATIONS:

  • The native build was provisioned on Windows (CMake 4.4.3, Ninja 1.13.2, Emscripten 6.0.9, Python 3.12.10, PhysX 5.1.3 @ 93b6c25). Rebuilding needs the same: PHYSX_ROOT, PHYSX_LIB_DIR, activated emsdk (see native/README.md).
  • Browser deployment of the threaded build additionally requires COOP/COEP headers for SharedArrayBuffer.

Roadmap

  1. WASM ABI + first real backend ✅ done.
  2. Native bridge + real PhysX execution ✅ done (70/70, collision verified).
  3. Broadphase (SAP) → narrowphase (box/sphere/capsule/plane) → queries.
  4. Constraints/joints solver, character controllers, sleeping/filtering.
  5. Mesh cooking, serialization parity, benchmarks vs native PhysX.

About

An unofficial WebAssembly port of NVIDIA PhysX for JavaScript and TypeScript.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages