Skip to content

Latest commit

 

History

66 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Box3D Metal

An opt-in Apple Silicon Metal compute backend for Box3D, distributed as a reproducible overlay rather than a fork containing the entire upstream source tree.

The backend keeps body state GPU-resident across supported solver phases and accelerates fused integration, colored convex and mesh contacts, distance joints, parallel joints, restitution, and deterministic graph overflow. The original CPU implementation remains the behavioral reference and fallback. An experimental, separately opt-in finalization kernel also computes final rotation, origin offset, sleep-motion metrics, world-space inverse inertia, and awake-shape AABBs. A second experimental opt-in traverses Box3D's existing dynamic trees and performs deterministic candidate compaction on Metal, preserving upstream candidate order. Resident moved-proxy marks and shape metadata reject self/duplicate, same-body, sensor, and built-in filter pairs; a revisioned mirror of Box3D's pair set suppresses existing non-compound contacts. Compounds, joints, custom filters, and contact creation retain the CPU path. Unchanged tree snapshots remain in persistent Metal storage, while supported shape motion now updates leaves and refits internal bounds on-device. Enlarged proxy bookkeeping consumes a stable GPU-compacted subset rather than rescanning every result or walking body shape lists again. Topology changes and unsupported CPU mutations invalidate the snapshot. Double-precision worlds use VF64 exact software binary64 translation and directed float narrowing for conservative far-world AABBs. On unchanged resident steps, the prior Metal fat bounds—not the CPU mirror—are the next dispatch's containment input; tree revisions fail closed to a CPU reseed. The full 64-byte shape result now stays in private Metal storage. A bounded collision-free, non-CCD route avoids its full blit and CPU apply; public AABB queries stage individual records, while route changes synchronize explicitly. Revision-stable steps also reuse persistent 72-byte shape-input records after an exact awake-body id check, skipping shape counting, body shape-list traversal, local AABB computation, and record writes. Both stages remain off by default; CPU topology and cold/topology registry rebuilds are still in the path. The shape-specialized narrow-phase route now batches sphere-sphere, capsule-sphere, capsule-capsule, and bounded compact hull-sphere local manifold geometry in one Metal command buffer, including ordered two-point capsule manifolds and feature ids. Sphere/capsule endpoints and radii plus compact hull geometry/material descriptors live in persistent Metal buffers; identical hull point, plane, and boundary-triangle streams are content-deduplicated. A body-id registry retains static and awake rotations, local centers, and VF64-capable world translations for collision. Revision-stable dispatches reuse both registries. Each 32-byte contact input contains eligibility, shape/contact identity, and contact generation. A revisioned contact input/order registry retains those records across unchanged pair-set, constraint-graph, and eligibility revisions, so stable steps do not gather CPU contact IDs or rewrite the input buffer. Current body indices and transient fast flags come from the per-step body registry. A zero-exception stable dispatch skips capacity-linear contact-state bitset clears, worker unions, and the serial state-change traversal; any later CPU exception or fallback clears before collision workers write. A complete cold ordinary batch now uses an 8-byte contact-ID-indexed topology table and commits once in canonical ID order, bypassing the same bitsets and second serial scan. For a strict virgin plan, narrow phase also expands the broad phase's retained 8-byte pair seeds directly into the private contact-input table. This removes the separate CPU-written 16-byte identity stream; recycled IDs, event/callback hazards, and revision mismatches retain the checked legacy route. A cold, event-free, all dynamic-vs-static batch then solves from a device-private one-color schedule with zero transition bytes and zero direct commits, while CPU island/graph topology materializes once at the next observation, step, mutation, or fallback boundary. The solver likewise defers its contact-capacity hit-event bitset clears when the current resident compact event list is empty, restoring them before an event-enabled path or Metal fallback. Full 160-byte outputs stay in private Metal storage; a deterministic scan/prefix/scatter pass returns only ordered CPU exceptions in the same command buffer. Stable resident contacts finalize directly into the private contact-ID table, emit zero shared manifold bytes, and schedule no CPU collision task. The scatter also produces both COM-relative anchors, feature-matches prior resident warm starts, and resolves default friction/restitution/rolling parameters plus tangent velocity. First-touch, callback, event, topology, and unsupported exceptions retain CPU graph/island state, while ordinary cold first touches defer CPU manifold allocation entirely. High-aspect and speculative hull-sphere contacts explicitly retain CPU GJK; other shape pairs remain on the CPU. Double worlds use the vendored VF64 exact subtraction before narrowing relative translations to float. The same scatter writes active finalized records into a private table indexed by Box3D contact id. This adds no steady-path readback or dispatch; explicit table staging exists only for validation and fallback diagnostics. Transient per-contact ownership now carries that authority through persistence, and topology into solver setup. Pre-solve callbacks remain CPU-owned. When every colored convex contact is resident-authoritative and no convex overflow exists, a Metal kernel now prepares Erin's SIMD-wide contact constraints in the existing solver command buffer. CPU workers seed a generation-tagged 152-byte contact-ID record with body indices and manifold identity. On later generation-stable steps, the manifold scatter refreshes the record directly with current indices, finalized anchors and materials, prior contact-scope impulses, persistence, and normal warm starts. Recycling, pre-solve, and custom material callbacks remain CPU-written exceptions. Once a stable touching contact has a generation-current device-refreshed preparation record, no collision worker runs and the finalized private contact-ID table remains authoritative. A world generation makes CPU manifolds lazy mirrors without stable-contact flag writes. Public/debug/snapshot consumers, sleep transitions, Metal disable, and CPU solver fallback materialize them by contact ID and generation. Fast/CCD, hit-event, recording, callback, non-ordinary first-touch, and topology-changing contacts remain ordered CPU exceptions. Solver submission bulk-copies only a deterministic four-byte ID schedule per SIMD lane and no longer dereferences contacts to repack those records. Mixed, recycled, callback, overflow, and unsupported routes fail closed, including explicit CPU prepare-on-fallback recovery. After restitution, Metal extracts one 80-byte impulse record per active contact into a generation-tagged contact-ID table. Successful resident steps bypass the all-contact CPU impulse-store traversal. Hit-enabled contact IDs are compacted during the existing narrow-phase input pack and only those exceptions synchronize before ordered event construction. Contact, body, and shape queries synchronize requested manifolds on demand; force debug drawing and snapshots are explicit boundaries. Invalid and unsupported routes retain the original store path, and CPU fallback invalidates prior GPU result authority. The constraint graph carries a monotonic topology/order revision. The four-byte contact-ID lane schedule remains in its Metal buffer while that revision and its exact wide/contact counts are unchanged; contact or joint insertion/removal invalidates it before the next solver submission. The post-solve record also retains contact-slot generation and each point's feature ID without growing beyond 80 bytes. On the next fresh supported collision pass, the scatter kernel claims matching features in upstream order and emits GPU-authored point persistence plus normal warm-start impulses; friction, twist, and rolling terms remain resident through staging. Reused contact-ID slots cannot consume stale solver state.

Quick start

Requirements: Apple Silicon, macOS, Xcode command-line tools, CMake, Ninja, and Git.

git clone https://github.com/Lulzx/box3d-metal.git
cd box3d-metal
./scripts/bootstrap.sh ../box3d-metal-worktree
../box3d-metal-worktree/build/metal-release/bin/test MetalTest
../box3d-metal-worktree/build/metal-release/bin/metal_demo

The bootstrap script clones the pinned upstream revision, verifies the patch, applies it, configures a Release build, and builds tests, demos, and benchmarks. Nothing from the upstream Box3D checkout is stored in this repository.

Documentation

Start with the documentation index. Compact printable guides are under docs/pdf.

Measured M4 Pro results

Whole-world or end-to-end measurements, including synchronization and relevant CPU work:

Workload Largest demonstrated result
Fused four-substep integration primitive 10.584x at 524,288 bodies
Unconstrained whole world 1.146x at 524,288 bodies
Scalar multi-manifold mesh contacts 1.646x at 131,072 bodies
Convex contacts 1.098x at 262,144 bodies
Distance joints 1.158x at 524,288 bodies
Parallel joints No stable whole-world crossover demonstrated
Experimental GPU finalization Correct, but 27% slower at the 524,288-body paired median
GPU shape finalization Correct, but 17.9% slower at 524,288 shapes
Experimental GPU tree traversal 1.068x at 524,288 shapes; small worlds regress
Indexed cold-contact topology 2.7-3.6% less GPU time than deferred-manifold checkpoint at 131,072-262,144 contacts

The tree-traversal speedup is historical evidence for the earlier CPU-prefix implementation. The current on-device scan has exact-order validation, but no new whole-world timing is published from the loaded development machine. The resident-refit and VF64 checkpoint likewise publishes correctness evidence only. The private-result/selective-sync checkpoint also publishes correctness evidence only because the development host was loaded. The persistent-input checkpoint likewise publishes route/correctness evidence only because the host remained loaded. The resident pair-filter checkpoint publishes exact-order correctness and metadata-residency evidence only; the host was still loaded. The existing-pair checkpoint likewise publishes lifecycle and residency evidence only, not timing from the loaded host. The bounded hull-sphere checkpoint likewise publishes CPU-oracle, deterministic replay, mixed-fallback, two-point ordering, and VF64 far-world evidence only. It does not publish loaded-host timing. The resident hull-geometry checkpoint publishes exact upload/reuse/rebuild and deduplication evidence only; it does not convert loaded-host kernel timings into a whole-world performance claim. The resident shape-geometry checkpoint adds primitive-mutation invalidation and 120-byte input evidence under the same no-loaded-host-timing boundary. The resident body-transform checkpoint reduces that input to 16 bytes and adds step/teleport/replay invalidation evidence, again without a loaded-host timing claim. The private manifold-result checkpoint adds active-only deterministic readback and range-linear CPU consumption under the same no-loaded-host-timing boundary. The manifold-finalization checkpoint fuses world-axis orientation into that scatter, again publishing correctness rather than loaded-host timing. The resident manifold-table checkpoint establishes stable contact-id addressing under the same correctness-only timing boundary. The solver-ownership checkpoint establishes the fail-closed preparation gate; the resident contact-preparation checkpoint now uses it to skip CPU preparation arithmetic for complete supported sets. The metadata-residency checkpoint also removes the dedicated solver-time contact traversal and 152-byte lane stream; CPU persistence/table writes and graph scheduling remain. Compact post-solve extraction reduces the 81-contact CPU impulse-input surface from 35,616 to 6,480 bytes while retaining public manifolds and matching hit events. It adds no loaded-host speedup claim. The schedule-residency checkpoint performs one pack and three reuses across four stable steps, then exactly one repack after graph topology changes. The warm-start-carry checkpoint then proves that a deliberately stale CPU manifold is repaired from the resident result by contact generation and feature ID, ending one-step CPU/GPU comparison at 4.47e-08 velocity error. The lazy-sync checkpoint then removes the all-contact CPU store: four stable 81-contact steps perform four bypasses and zero manifold syncs, while the hit event fixture synchronizes exactly one exception contact. The resident contact-finalization checkpoint moves feature persistence, COM-relative anchors, and default material/tangent finalization into the manifold scatter while preserving custom callback exceptions. The device-refresh checkpoint then updates all 81 stable preparation records on each of three post-seed steps (243 refreshes), while custom material callbacks report zero. It removes steady-state CPU table writes, not the compact shared result stream or CPU topology traversal; the 512-contact smoke remains a documented 0.118x regression. The collision-bypass checkpoint then skips all 243 stable CPU manifold applications with zero geometry synchronizations. Explicit public, fallback, sleep, and disable boundaries materialize the lazy mirror safely. Its exception-compaction follow-on removes the stable shared stream and flat collision task: 512 and 8,192-contact runs report only their seed contacts as cumulative CPU collision work, then zero latest exceptions and zero shared manifold bytes. Timing was withheld there because the host load exceeded 80. The input/order follow-on then removes the stable graph-ID gather, 32-byte input rewrite, and per-contact solver coverage walk. At 512 and 8,192 contacts it reports two cold/topology packs followed by eleven reuses, zero latest input bytes, and complete stable coverage bypass. Timing remains withheld because a Python process occupied a full CPU core and host load stayed above 4. The contact-state follow-on then removes the remaining bitset clear/union and serial state traversal on zero-exception steps. Loaded-host 512 and 8,192 contact runs cleared state only for their seed phase and reported zero latest clear bytes; timing remains withheld at host load near 8. The empty-event solver follow-on skips all per-worker contact-sized hit-bitset clears: 512 and 8,192-contact runs bypassed all 48 and 28 resident solver phases respectively, while hit-enabled and forced-fallback tests restored the clear before CPU work. The private-first-touch checkpoint keeps each ordinary cold manifold and its prepare record on-device. Its indexed-topology follow-on halves the shared transition representation to 8 bytes per contact and removes per-worker state bitset clear/union plus the second serial scan. Against the deferred-manifold checkpoint, M4 Pro cold GPU medians improve by another 2.7% at 131,072 contacts and 3.6% at 262,144. The remaining canonical island/graph commit is measured at about 8.0 and 15.5 ms and remains the next device-residency boundary.

Small workloads remain CPU-favorable. Metal is explicitly enabled per world with a caller-selected body threshold.

License

MIT. See LICENSE and NOTICE.md.

About

Apple Silicon Metal compute backend for Box3D, distributed as a reproducible overlay with tests, benchmarks, and documentation

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages