↑ Vocaro Guide to MPM

Implementation — GPU Optimization & Reference Codebases

Assumes the algorithm from Theory §4.

This document is the practical engineering layer: how to make MPM fast on a GPU and the reference implementations worth studying or building on. The product questions — is MPM shipping in commercial games, how novel would an MPM-native game be, how to talk about that — live in their own doc, Games.

1. The central performance problem: the P2G scatter

Recall the four-phase step (Theory §4): reset → particle-to-grid (P2G) → grid update → grid-to-particle (G2P). Profiling almost always lands on P2G as the bottleneck, for one structural reason:

Many particles write to the same grid node at the same time. A node’s mass and momentum are sums over every particle in its B-spline stencil. On a GPU, thousands of particle-threads racing to accumulate into shared node memory means atomic write contention — the dominant cost.

Many particle threads atomically add into the same grid node; those writes serialize, which is the central bottleneck

G2P, by contrast, is a gather (each particle reads from its stencil and writes only to itself) — embarrassingly parallel, no contention. So optimization effort concentrates on P2G.

2. GPU optimization techniques

The authoritative reference is Gao et al., GPU Optimization of Material Point Methods, SIGGRAPH Asia 2018 (PDF, code: kuiwuchn/GPUMPM). The techniques below come from that paper and the surrounding GPU-MPM literature.

2.1 Tame the atomic contention

The worked shape of “sort + tile + stage in shared memory.” The three bullets above are usually given abstractly. EA’s PB-MPM main branch is the most legible complete instance of all of them at once, so it is worth reading as the reference implementation of this subsection rather than as a PB-MPM curiosity (§3.3 covers the algorithm; this is only the memory strategy).

It bins particles into 6×6-cell tiles (“bukkits”) each substep via three small kernels (bukkitCountbukkitAllocatebukkitInsert) that build an index table — particles themselves never move or reorder, which is what keeps §2.1’s determinism and index-stability intact. The entire iteration body is then ONE kernel (g2p2g.wgsl), one workgroup per 64-particle slice of a bukkit:

  1. Threads cooperatively load the tile’s grid nodes + a 1-node halo into workgroup shared memory, applying the grid update during the load — mass-normalize, collide, boundary — so the read is the grid pass.
  2. Barrier. Each thread gathers its particle from shared memory, does its per-particle work, and scatters back into shared-memory atomics.
  3. Barrier. The tile flushes to the global grid — one atomicAdd per node per channel per tile (halo overlap is why it must still be an add).

Global atomic traffic per particle drops from 27 nodes × channels to a per-tile flush — one-to-two orders of magnitude less contention, and global atomics are exactly what is slow on Apple GPUs. Three properties are worth extracting:

2.2 Data layout & memory coalescing

2.3 Sparse grids

Most of the background grid is empty (material occupies a fraction of the domain). Sparse grid structures — notably SPGrid (Sparse Paged Grid, Setaluri et al.) — store only active blocks, slashing memory and letting high-resolution simulations fit on-GPU. Active-block bookkeeping is updated as particles move.

2.4 Reported results

Gao et al. report ~20–50× speedups over an optimized multicore CPU implementation, scaling to millions of particles — bringing offline-quality MPM toward interactive rates and making large-scale visual-effects (VFX) and engineering work practical.

Four optimization branches: particle-to-grid contention, memory layout, grid sparsity, and the integration scheme

Real-time vs. throughput. The Gao-style optimizations maximize throughput (particles/second). For real-time games, the harder wall is the timestep: explicit MPM’s CFL (Courant–Friedrichs–Lewy) stability limit forces many small steps per frame. Two complementary attacks: MLS-MPM (Moving Least Squares MPM — do each step ~2× cheaper) and PB-MPM (position-based MPM — take larger, stable steps). See §3.

3. Reference implementations

A curated, build-on-able set — ordered roughly from most readable to most production-optimized. Always open the actual LICENSE in each repo before relying on it; terms can change.

Project Lang / Platform License Best for
nialltl/incremental_mpm Unity / High-Performance C# MIT Learning the algorithm. Single-file, incremental, paired with a written guide.
yuanming-hu/taichi_mpm C++ / CUDA (+ Taichi) MIT Canonical 88-line MLS-MPM; high-performance reference.
electronicarts/pbmpm WebGPU (JS + WGSL shaders) BSD-3-Clause Real-time / stable-at-any-timestep research demo.
kuiwuchn/GPUMPM CUDA (check repo) The Gao et al. SIGGRAPH Asia 2018 GPU optimizations.

3.1 nialltl’s incremental_mpm — the best starting point

A readable MLS-MPM implementation in Unity using data-oriented High-Performance C#, with a detailed companion guide (nialltl.neocities.org/articles/mpm_guide). The headline demo is a real-time ~40,000-particle fluid sim. The guide builds up in three parts:

  1. Part 1 — basic MLS-MPM movement using APIC (affine particle-in-cell) transfers, no deformation model. Establishes the four-phase loop, the 3×3 quadratic-B-spline stencil, and the affine momentum matrix C.

  2. Part 2 — elastic solids via a Neo-Hookean constitutive model, introducing the deformation gradient F and its update. The coded stress (from the guide), with elastic_mu as μ and elastic_lambda as λ, is:

    J=det𝑭𝑭T=(𝑭T)1𝑷=μ(𝑭𝑭T)+λlog(J)𝑭T𝝈=1J𝑷𝑭T\begin{aligned} J &= \det \mathbf{F} \\ \mathbf{F}^{-T} &= \left(\mathbf{F}^{T}\right)^{-1} \\ \mathbf{P} &= \mu\left(\mathbf{F} - \mathbf{F}^{-T}\right) + \lambda \log(J)\,\mathbf{F}^{-T} \\ \boldsymbol{\sigma} &= \frac{1}{J}\,\mathbf{P}\,\mathbf{F}^{T} \end{aligned}

  3. Part 3 — real-time fluids (credited partly to Grant Kot), via three tweaks that buy a much larger timestep:

    • Recompute particle volume every frame from a density estimate in the style of Smoothed Particle Hydrodynamics (SPH) (scatter mass to grid, gather back) instead of integrating it — more memory reads, but far larger stable Δt.
    • A Newtonian-fluid constitutive model with a Tait equation of state for weakly-compressible fluids (as in weakly-compressible SPH, WCSPH).
    • Softened, predictive boundary conditions acting on particles directly, to curb high-speed tunneling through the domain walls.

    The credit is explicit in the guide — “credits to Grant Kot for the altered volume calculations and boundary conditions tricks” — and the first of those traces back to Kot’s own 2010 applet note: “Instead of integrating the density over time (which is what most of the MPM papers do), I do a density summation every frame. Because this is not dependent on previously calculated values of density, there is no accumulated error.” Origin and the rest of his work: References §3.

3.2 taichi_mpm — the 88-line MLS-MPM

The canonical compact MLS-MPM reference from Hu et al. (the MLS-MPM authors), written in the Taichi language for high performance with CPU/CUDA backends. The famous 88-line version is the clearest end-to-end MLS-MPM in existence (a more-readable variant by David Medina also circulates). MIT-licensed and explicitly commercial-OK. Cross- reference it against the SIGGRAPH 2016 course notes for the theory.

3.3 EA SEED’s PB-MPM — Position-Based MPM (real-time)

Presented at SIGGRAPH 2024 by Chris Lewin (EA SEED). PB-MPM is a semi-implicit compliant-constraint (Position-Based-Dynamics-style) reformulation of MPM that is stable at essentially any timestep while staying “as easy to implement as an explicit integrator.” This directly attacks MPM’s single biggest real-time barrier — the small-timestep stability requirement — and is explicitly framed for real-time applications where the hardest requirement is stability under autonomous, potentially violent player input.

Algorithmically, PB-MPM differs from explicit MPM by only a small number of changes: it wraps the ParticleToGrid → GridUpdate → GridToParticle cycle in an iteration loop with an added SolveConstraints step (the lines marked in red in the paper’s Algorithm 1).

Repository: github.com/electronicarts/pbmpm

The rest of §3.3 is a close reading of both branches.

Branch Size Character
siggraph2024 ~1,070 lines of WGSL + a thin JS driver The algorithm laid bare: five kernels, one per pipeline stage. Read this one first.
main ~1,570 lines The optimized rewrite: everything fused into one g2p2g kernel over a tile system, triple-buffered grid, indirect dispatch (§2.1).

3.3.1 Conventions decoder

Their code and standard velocity-form MLS-MPM describe the same discretization in different clothes. Translation table, for reading their shaders — the right-hand column is the convention of a typical explicit velocity-form 3D MLS-MPM (normalized unit-cube domain, fixed-point scatter):

Concept pbmpm Velocity-form MLS-MPM (3D)
Domain grid-cell units, [0,gridSize]2[0,\; \mathrm{gridSize}]^{2}, y up normalized [0,1]3[0,1]^{3}, y up
Grid state displacement 𝒅=𝒗Δt\mathbf{d} = \mathbf{v}\,\Delta t (dt folded in everywhere) velocity v
Affine state deformationDisplacement 𝑫=Δt𝑪\mathbf{D} = \Delta t\,\mathbf{C} APIC C
F update 𝑭(𝑰+𝑫)𝑭\mathbf{F} \leftarrow (\mathbf{I} + \mathbf{D})\mathbf{F} 𝑭(𝑰+Δt𝑪)𝑭\mathbf{F} \leftarrow (\mathbf{I} + \Delta t\,\mathbf{C})\mathbf{F}identical
Weights quadratic B-spline, 3×3, cellIndex=p1\mathrm{cellIndex} = \lfloor p \rfloor - 1, offset=frac0.5\mathrm{offset} = \mathrm{frac} - 0.5 same spline, base=xN0.5\mathrm{base} = \lfloor x\cdot N - 0.5 \rfloor parameterization
Node offset nodep+0.5\mathrm{node} - p + 0.5 (cell units) (i,j,k)fx(i,j,k) - \mathrm{fx} (cell units, G2P side) — same sign convention
Scatter fixed-point i32 atomics, multiplier 10710^{7} as a runtime uniform, i32(x·mult) truncation fixed-point int atomics, 65536 compile-time, round()
Gravity particle-level, dy=gridSizeygdt2d_y \mathrel{-\!=} \mathrm{gridSize}_y \cdot g \cdot \mathrm{dt}^{2} (normalized to window height) grid-level, vy=dtgv_y \mathrel{-\!=} \mathrm{dt} \cdot g
Boundary “guardian” band, fixed 3 cells + particle clamp domain BC band + clamp(p.x, 0, 1) backstop
Stress none in P2G — see 3.3.2 Kirchhoff τ folded into the P2G affine (MLS-MPM)

Their fixed-point discipline is the same as the right-hand column’s, for the same reason (WebGPU, like Metal, has no float atomics; integer sums commute → deterministic scatter — §2.1). Two small deltas: their multiplier is runtime-tunable (a precision dial worth remembering), and their encode truncates where the right-hand column rounds — rounding is the better half of that trade, since truncation carries a toward-zero bias.

3.3.2 The algorithm as implemented

The loop. The particle carries displacement and deformationDisplacement (D) instead of velocity and C. Per substep (sim.js, both branches; defaults dt = 1/240, iterationCount = 5):

emit / recycle particles
for iteration in 0..iterationCount:            # default 5 (UI: 2..100)
    particleUpdate     # constraint projection: nudge D toward the material's target
    gridZero
    particleToGrid     # scatter mass + momentum(d + D·offset) — NO stress term
    gridUpdate         # mass-normalize, collide, guardian — all in displacement form
    gridToParticle     # gather d and D (= B·4)
particleIntegrate      # x += d;  F ← (I+D)F;  plasticity; gravity → next d;  once

The entire transfer cycle runs inside the constraint loop. That is the whole trick: particleUpdate projects each particle’s D toward a constitutive target shape, and the P2G→grid→G2P pass in between is what makes neighboring particles negotiate — Jacobi-style position-based dynamics where the MPM grid is the constraint coupling. Nothing anywhere applies a force scaled by stiffness·dt², so nothing can explode; stiffness is expressed as a convergence rate (elasticRelaxation, liquidRelaxation, default 1.5 — over-relaxed). Stability is unconditional; what varies with iteration count is how stiff materials manage to look.

Warm start, for free: d and D persist across substeps as the next loop’s initial guess, and the integrate step adds gravity into the next substep’s initial displacement. Preserve this in any port — zeroing D between substeps silently costs iterations.

Materials as constraint projections (particleUpdatePBMPM.wgsl). Each material answers “what shape does F want to be, and how fast do we relax toward it”:

Material Target / projection
Liquid Viscosity: remove the symmetric (deviatoric) part of D, scaled by liquidViscosity (default 0.01). Volume: α=12(1/ρtr(𝑫)1)\alpha = \tfrac{1}{2}\left(1/\rho - \mathrm{tr}(\mathbf{D}) - 1\right) drives 𝑫+=liquidRelaxationα𝑰\mathbf{D} \mathrel{+\!=} \mathrm{liquidRelaxation}\cdot\alpha\cdot\mathbf{I} so that the end-of-step volume identity lands on det = 1.
Elastic 𝑭trial=(𝑰+𝑫)𝑭\mathbf{F}_\mathrm{trial} = (\mathbf{I}+\mathbf{D})\mathbf{F}; target tgt=α(𝑼𝑽T)+(1α)𝑭/|det𝑭|\mathrm{tgt} = \alpha\,(\mathbf{U}\mathbf{V}^{T}) + (1-\alpha)\,\mathbf{F}/\sqrt{|\det \mathbf{F}|} — a blend of the rotation (rigid target, from SVD) and the closest det-1 matrix (volume target). elasticityRatio α is the character dial: 1 = shape-remembering, 0 = volume-only (fluid-like). 𝑫+=elasticRelaxation((tgt𝑭1𝑰)𝑫)\mathbf{D} \mathrel{+\!=} \mathrm{elasticRelaxation}\cdot\bigl((\mathrm{tgt}\cdot\mathbf{F}^{-1} - \mathbf{I}) - \mathbf{D}\bigr).
Sand Elastic projection with singular values clamped to 1\ge 1 while logJp == 0 — a unilateral constraint: sand resists compression but never pulls back when stretched. Plus the liquid viscosity term.
Visco Same projection as elastic; the difference is all in the integrate-time plasticity (below).

Their 2×2 SVD is closed-form (Gimeno’s rotation-angle construction, matrix.inc.wgsl) so they can afford one per particle per iteration. That luxury does not survive 3D: the elastic projection needs only 𝑼𝑽T\mathbf{U}\mathbf{V}^{T}, which is the polar rotation R, so a Newton polar iteration replaces it in the loop and the full SVD is paid once per substep at integrate time. Sand’s in-loop unilateral Σ1\Sigma \ge 1 clamp genuinely needs singular values and has no such escape.

Integration and plasticity (particleIntegrate.wgsl) runs once per substep. For liquids, F is never stored as a matrix at all — only a scalar liquidDensity, integrated by the first-order identity det((𝑰+𝑫)𝑭)(1+tr𝑫)det𝑭\det((\mathbf{I}+\mathbf{D})\mathbf{F}) \approx (1 + \mathrm{tr}\,\mathbf{D})\det \mathbf{F}. Solids do 𝑭(𝑰+𝑫)𝑭\mathbf{F} \leftarrow (\mathbf{I}+\mathbf{D})\mathbf{F}, then SVD, then:

Displacement-form boundaries. gridUpdate works on predicted position: it asks “would this node’s displaced position penetrate?” — colliders subtract only the radial component of displacement past contact (with a gap term so approach up to the surface is free), and the guardian projects the displaced node position back into bounds. Position-predictive BCs are naturally tunnel-resistant; the velocity-form equivalent is a sign test, which only knows “moving inward,” not “will cross this substep.” Worth remembering when next fighting a fast-material leak. (The main branch adds borderFriction mixing at the guardian; the sig branch zeroes.)

Grid-recovered volume. gridToParticle.wgsl carries a comment worth quoting in paraphrase: standard MPM volume integration slowly loses liquid volume under shear, so they recover an objective volume measure from the grid — scatter particle rest volume (their 4th grid channel; emitted as 1/ppc1/\mathrm{ppc} per particle so a rest-packed node sums to ~1), gather it back, reciprocate, and blend into the tracked density only where the grid reads compressed (at a free surface the estimate reads spuriously expanded — fewer neighbors, not more volume — so tension is never trusted). It is an SPH-flavored density estimate used as a drift corrector, and it transplants cleanly into an explicit solver.

3.3.3 GPU-resident particle lifecycle

particleCount lives in a GPU buffer; a one-thread setIndirectArgs kernel refreshes indirect dispatch args; drains push freed indices onto an atomic-cursor free list; emitters pop it. Emission is grid-dispatched — one thread per cell decides whether its cell spawns this frame, so rate is per-area and collision-free by construction. Tombstone schemes (dead particles kept in place) cost dispatch threads forever and never reuse the slot; when a scene needs hoses, waterfalls, or blast-debris cleanup, this is the pattern, and it composes with the tiling in §2.1 because dead particles simply never get binned.

3.3.4 Read and rejected

Recorded so nobody re-litigates them from scratch:

3.3.5 Not in their scope (don’t look for it there)

3D. Multi-body contact (single velocity field, so two elastic bodies weld on contact — they simply do not have separable bodies). Cutting/CPIC. Two-way rigid coupling and articulation. Snow. Determinism as a verified property — their scheme is deterministic by the fixed-point argument of §2.1, but nothing tests it. Any renderer beyond a point splat.

3.3.6 File map (siggraph2024 branch)

File What it is
shaders/particleUpdatePBMPM.wgsl The constraint projections — the heart, 87 lines
shaders/particleToGrid.wgsl / gridToParticle.wgsl Stress-free transfers in displacement form
shaders/gridUpdate.wgsl Displacement-form BCs (collider gap term, guardian)
shaders/particleIntegrate.wgsl Once-per-substep: advect, F update, plasticity, gravity, drains
shaders/particle.inc.wgsl Particle struct, guardian helpers, quadratic + cubic weights
shaders/matrix.inc.wgsl Closed-form 2×2 SVD (Gimeno)
src/sim.js The substep/iteration loop, uniform plumbing
main:shaders/g2p2g.wgsl The fused kernel (§2.1)
main:shaders/bukkit*.wgsl Count/allocate/insert binning

3.3.7 License & origin

The repo is BSD-3-Clause (Electronic Arts, 2024 — EA’s stock variant, no added terms). Redistribution of their code, including transliterated shaders, requires retaining the copyright notice + conditions + disclaimer, a notices entry in binary distributions, and no use of EA’s name for promotion. Algorithms and ideas are not expression: a technique re-derived and re-written against a different architecture carries no notice obligation — but if anyone ever ports their WGSL wholesale, add the LICENSE.md text to a third-party notices file that ships with builds. Requested citation either way:

Chris Lewin. A Position Based Material Point Method. ACM SIGGRAPH 2024 Talks. ea.com/seed · paper PDF

See References for the full citation block.

What PB-MPM buys, and what it does not. Unconditional stability, not accuracy. Stiffness is a function of iteration count and dt — materials are “relaxation rates,” not Young’s moduli, and cannot be calibrated to physical units; the relaxation is inherently dissipative. It is the right solver for game-feel materials at display-rate substeps on mobile; an explicit MLS-MPM remains the right one for calibrated or violent dynamics.

Four stages: learn the algorithm, deepen the theory, scale on GPU, then move to a position-based solver if you hit the timestep wall

4. Suggested path forward (for building with MPM)

  1. Learn the algorithm from nialltl’s incremental examples (most readable, MIT, Unity-native).
  2. Cross-reference Yuanming Hu’s 88-line MLS-MPM and the SIGGRAPH 2016 course (mpm.graphics) for deeper theory.
  3. Scale to GPU with the Gao et al. techniques (spatial sort, sparse grid, fixed-point atomics, AoSoA) when particle counts grow.
  4. If you hit the timestep/stability wall in a shipping context, study EA’s PB-MPM (siggraph2024 branch first), purpose-built to solve it.

Building this for a game specifically? The product-side questions — where MPM ships today, whether any commercial game runs it as core physics, and what makes an MPM-native game newly viable — are covered in Games.

5. Engineering rules that hold for any GPU MPM

Every GPU MPM implementation ends up rediscovering these. They follow from the structure of the method and of GPU execution, not from any one codebase, and each is stated with its reason, because the reason is what tells you when it applies.

  1. Fixed-point integer atomics are what make the scatter deterministic. Floating-point addition is not associative and GPU thread order is not reproducible, so a float-atomic P2G produces a different grid on every run. Accumulate into scaled integers with InterlockedAdd, which is associative, and convert once in the grid update. Metal has no float atomics at all, so this is also the portable choice.
  2. Particle arrays must stay index-stable. Sorting particles for memory locality, or compacting out dead ones, reorders the accumulation and destroys the determinism just bought. Kill a particle by relabeling and parking it — a dead group id, a position outside the domain — never by compaction. If spatial sorting is needed for performance, sort an index buffer, never the particle buffer itself.
  3. Determinism is same-platform only. Same binary, same device, same seed → byte-identical replay. A different GPU architecture, shader compiler, or FMA-contraction policy → statistically equivalent at best. Golden-file regression tests work; cross-platform golden files do not. Corollary: any edit inside a hot kernel recompiles it, and the compiler may contract the same arithmetic differently, so a replay digest moves for a change that altered no logic. A digest is a same-code invariant, not a “nothing happened” check.
  4. Consume-and-zero in place is free — only where each accumulator is read exactly once. The grid clear can be folded into the grid update when every node accumulator has one reader. A pass that reads neighboring nodes’ accumulators in the same dispatch — a multi-field contact pass, for instance — must keep its explicit clear.
  5. dt is not a free performance knob. The timestep is entangled with oscillation, plastic yield rates, and boundary friction; raising it to buy frames changes the result, not just the cost. Treat (dt, gravity, substeps per frame) as one tuned set, and re-tune the others when one moves.
  6. There are two timestep limits, and the second depends on the running state. The acoustic limit, ΔtCFLΔx/cp\Delta t \le \mathrm{CFL} \cdot \Delta x / c_p, is a property of the material and can be read off a preset before it runs. The advective limit — material must not cross a cell in a step, ΔtΔx/maxp(𝒗p+1.5Δx𝑪pF)\Delta t \le \Delta x / \max_p\!\left(\lVert \mathbf{v}_p \rVert + 1.5\,\Delta x\,\lVert \mathbf{C}_p \rVert_F\right) for an APIC/MLS transfer, where the affine term counts because it advects too — is a property of the current state and has to be monitored, not precomputed. The smaller one sets the step.
  7. Gravity and damping are environmental, not material. A material is the same on Earth and on the Moon. Keep gravity and damping on the solver, set per scene, and keep the material record to the constitutive law alone (E, ν, hardening, yield, cohesion, friction angle). Give each quantity one owner, or the two drift apart and the bug reads as “the material changed” when nothing did. A fixed per-substep velocity blend is a damper whose rate is (1β)/Δt(1-\beta)/\Delta t; expose a rate in 1/s and derive the per-step factor from it.
  8. Keep synchronous readback off the per-frame path. A synchronous buffer read costs however long the GPU happens to be backed up, not the size of the copy, because it blocks until the queued work drains — “a few floats, once a second” can freeze a frame for seconds. Read back asynchronously, and keep any per-tick state readback O(groups), never O(particles).
  9. Fixed-size GPU buffers truncate silently. An append buffer, collider array, or query result that overflows its capacity caps with no error and produces physics that is subtly, deterministically wrong. Size for the densest scene you can construct, and carry an overflow counter you actually check.
  10. A dispatch has a per-axis group limit, and exceeding it fails silently and partially. A 256³ field cleared as 262,144 one-dimensional groups exceeds the 65,535-groups-per-axis limit of Direct3D and of the engines built on it. The clear partly does not happen, and with a freshly allocated buffer the first frame still looks correct, because zeroed memory and a successful clear are indistinguishable. Dispatch 3D fields with 3D groups.
  11. A fixed-point readback has a quantum, and its error scales with particle count, not time. Summing a fixed-point per-particle quantity measures through a quantizer. The rounding errors cancel while the motion is lively and stop canceling when it goes quiet: thousands of particles sharing a similar small velocity round the same way, and the error becomes a bias. A conservation check that drifts only at rest is usually measuring its own quantum.

Sources for this document