Physics data and SIMD execution
A seam, not a specification #K2F6WD
This page thinks about a Catto-inspired physics experiment for the little block world, using three studies as material: the Box3D field notes (Box3D's architecture), the sb-simd field notes (sb-simd), and the domain/bundle model of #D5M8BZ. It tries to identify the seam: where CLOS objects, generic functions, and live inspection belong, and where dense columnar state and closed loops belong, such that neither side has to apologize to the other.
The first narrow decision is now executable. The block-world sequence #S2K5HN has a scalar fixed-step player AABB with voxel collision, grounding, gravity, and jumping. It is intentionally a behavioral oracle, not the general engine described below; this page keeps that small playable step from accidentally foreclosing the later one.
The general engine now exists as well, at the size #G7Q3XR asked for: #V090DQ records what was built and #IDVK7G through #NZCNHC what it turned out to be. The proposals below stand as the reasoning that led there.
Mentioned in: The step as built
The first executable seam #H7P4CX
The current block-world-player is separate from both terrain and camera. It
owns one position, velocity, shape, and grounded reading. At 120 Hz a closed
scalar step accelerates horizontal velocity from input, applies gravity, and
resolves its AABB axis by axis against candidate voxel cells. The camera is
then synchronized to the player's eye height. Space is an edge-triggered
jump request rather than upward flight. Grounded movement also jumps
automatically when a horizontal contact has a clear body-sized space one
block above it; a two-block wall remains a wall.
This is deliberately less machinery than a general rigid-body engine, but it establishes several contracts that survive the next representations:
- the terrain lattice is queried directly as static geometry;
- missing horizontal terrain is a solid simulation boundary, while space above the current vertical materialization remains traversable;
- simulation uses a fixed step distinct from render cadence;
- the body and view have distinct ownership; and
- tests state the reference behavior for falling, grounding, wall contact, jumping, and camera attachment.
The next physics step should not elaborate this controller into a framework. It should introduce the first dense awake-body columns and a terrain-contact domain beside it, run the same scalar behavior through those columns, and retain this controller as the small oracle used to compare later four-wide kernels.
Bodies, contacts, and constraints are domains of their own #Y5H8KC
The domain/bundle vocabulary was developed for terrain, but its real claim is broader: a finite domain identifies sites, and fields have values over those sites (#B8R3KF). Box3D, read in this vocabulary, is a physics engine built from exactly such domains — with the twist that domain membership is dynamic:
- The awake body domain is Box3D's awake solver set: a dense domain whose sites are currently simulating bodies, with columns for velocity and accumulated position delta. Membership changes constantly — bodies sleep, wake, and are created — and the storage compacts by swap-removal while stable ids stay valid through a forwarding address (#W2M9FJ).
- Each sleeping island is a retired materialization of the same schema minus the hot columns: evicted from every array the step iterates, resident as a contiguous copy, wakeable by memcpy (#S5K3WM).
- The contact domain has pair-shaped sites: a contact exists from AABB overlap onward, carrying persisted manifold and cache fields, and is promoted into the solver only while touching (#C6J9RW).
- The constraint coloring is a partition of the touching-contact domain under one invariant — no two constraints in a color share a body — that buys threads and lanes at once (#G3W7KD).
The important structural observation: these domains have different sites, different lifecycles, and different legal operations, even though they all describe one world. A body is not a block; a contact is not a body; none of them wants to live inside the terrain bundle. This is the same conclusion #A6X2RT reached for terrain products — ownership and invalidation, not layout, decide what is stored together — now applied to simulation state.
For luv the immediate consequence is pleasantly concrete: "add physics" does not mean adding fields to chunks. It means introducing a body domain (with a bundle of kinematic columns), a contact domain derived jointly from bodies and terrain, and keeping the block-content bundle as what it already is — the static environment those domains query.
Mentioned in: The sequence that stays fun, Columnar layout, materialization, and policy are separate, Field kernels bridge domains to semantic arithmetic
The sparse–dense split is the row/column distinction with a forwarding address #B5N8JT
Box3D splits every simulated thing into a stable sparse struct (identity, topology, user data) and moving dense rows (hot numerical state), with the sparse side holding a forwarding address that swap-removal must patch (#W2M9FJ). In the vocabulary of #C9L2WA this is precisely the distinction between a persistent object and a row projected from columns — except that Box3D, lacking a garbage collector, also had to invent checkable identity: ids carry generations so staleness is detectable data (#N7Q4XS).
A luv body can therefore be an honest CLOS object — the sparse side — owning
its name, its shape references, its joints, its userland meaning, and a
set-index=/=local-index forwarding address into whichever materialization
currently holds its numbers. The columns never contain object references;
the object knows where its row is. Inspection composes naturally: describe
on the body can present its row as if it were slots, the way Box3D's natvis
visualizers reunite what the layout separated.
What Lisp does not get for free is the rest of the id story:
- Staleness: a destroyed body's CLOS object still exists; like the HAL's destroyed GPU wrappers (#G6C2TX), it needs an explicit invalid state, and the step must never discover mid-loop that a row's owner died. Structural mutations belong between steps, as Box3D confines them to the serial phases between parallel ones.
- Value handles: the moment simulation state is shipped somewhere non-Lisp — a GPU buffer, a replay log, a foreign solver — object references stop working and something like the index-generation-world triple reappears. It costs little to give bodies a small integer id from an id pool from the start, with the CLOS object as its resolution.
The forwarding-address invariant — every relocation immediately patches the
owner — is a five-line discipline in Lisp, but it is the invariant that
makes compaction, sleep-as-relocation, and dense iteration compatible with
stable identity. It deserves a validation walker from day one, in the
spirit of Box3D's b3ValidateSolverSets, because a live image will violate
it in creative ways no C program can.
Mentioned in: The step as built
Select the phase once, then run closed loops #R9M4PV
The hot path of a physics step must not dispatch per site. The arrangement #R3F8YC anticipates — a generic operation selects a representation or phase once, borrows specialized arrays, and runs an ordinary optimized loop — maps directly onto how Box3D structures a step: prepare (gather from domains into per-step constraint arrays), then substep iterations of closed loops over those arrays, then store (scatter results back) (#R7F2QH).
Generic functions own the boundaries: which integrator, which solver flavor per constraint kind, which manifold function per shape pair — Box3D's own narrow phase is a type-pair function table, that is, a manual generic function. Inside a phase, the loop is closed: locally declared, non-consing, over specialized arrays it borrowed from bundles. Bodies never become tiny dispatching objects because bodies never appear in the loop at all — only columns do.
The first sb-simd study forced honesty (#L2W6KT): the archived library and
the machine's old SBCL 2.5.10 had no arm64 Lisp-level SIMD. That is now
history, not a deployment constraint. Luv pins and supplies SBCL 2.6.7;
sb-simd is part of the project environment on both arm64 and x86-64, and
the live arm64 image has executed f32.4 arithmetic as a NEON FADD
(#W9K4DN). The implementation order can therefore be simpler:
- A declared scalar reference kernel remains the executable specification, tail implementation, and debugging path. It is not a substitute forced on one of luv's supported CPU architectures.
- A common native four-float path uses 128-bit
f32.4: NEON on arm64 and SSE-family instructions on x86-64. This is exactly Box3D's chosen width (#T3C8FV), so four-wide constraint batches can be a real design target without making the physics layout architecture-specific. - Optional wider x86-64 paths may use AVX or AVX-512 when runtime instruction-set availability and measurement justify them. They are specializations of the same phase, not a reason to make a separate kind of image or change the logical bundle schema.
- The GPU, which luv already programs through its own SPIR-V assembler, remains a possible home for sufficiently wide regular work, at the price of crossing the completion frontier (#T9K4RC).
- A foreign kernel is now an escape hatch only for a measured limitation in SBCL or sb-simd, not the expected route to NEON. The Lisp definition remains its oracle in sb-simd's scalar-mirror style (#E8M3JC).
The design consequence is the same at every tier, and this is the point:
what a kernel needs is not merely an instruction set but a layout contract —
struct-of-arrays columns and, if constraints are ever packed four-wide, the
coloring disjointness invariant that makes gather/scatter safe (#T3C8FV).
The shared 128-bit width makes the first wide layout portable across luv's
CPU targets even though the emitted instructions differ. Generic functions
can select the phase once; instruction-set-case can select the best native
kernel for the current CPU without exposing that choice to bodies, contacts,
or bundle schemas (#C3F7XM).
Mentioned in: The first Lisp atelier should stay ordinary, Sleep is residency, and it prices determinism, Bundles keep meaning outside the lanes, One arithmetic medium, many clients, Before SBCL 2.6.7, arm64 was absent in three instructive ways
The smallest solver worth building #G7Q3XR
A first slice that exercises the seam without importing an engine:
- Shapes: spheres only, against the block world. Sphere–voxel contact generation is simple enough to write in an afternoon and rich enough to produce piles, which are the test that matters.
- Step: the Soft Step skeleton at its minimum — substeps of integrate, warm start, solve with bias, integrate positions, relax without bias; restitution afterwards, filtered by accumulated impulse (#R7F2QH). One iteration per phase, exactly as Box3D ships. The soft-constraint recipe is three numbers per constraint class; the delta-position representation comes along for free and keeps statics stateless.
- Contacts as a state machine: born at (fat) AABB overlap with terrain or another body, promoted while manifolds exist, demoted after, with warm-start impulses persisted on the contact (#C6J9RW). Begin and end become buffered events polled after the step — the same step-as-transaction shape as the HAL's command buffers, applied to outputs (#F8H3PW).
- No threads, but the invariant anyway: single-threaded, but constraints colored from the start. Coloring is cheap, it is the precondition for both lanes and threads later, and — Box3D's quiet lesson — solving static contacts last within an iteration is a solver quality decision (#G3W7KD), so ordering must be explicit data rather than loop accident.
Everything here stays live: the solver phases are generic functions one can
redefine mid-pile, the contact domain is inspectable, and a describe of a
body shows its row. The CLOS block world already proved this style renders;
the question this slice answers is whether it simulates.
Mentioned in: A seam, not a specification, The smallest solver was built: spheres, coloured contacts, four-wide lanes, Open after the first pile
DONE The smallest solver was built: spheres, coloured contacts, four-wide lanes #V090DQ
Intent. Build the slice #G7Q3XR proposed, no wider: spheres against the voxel lattice and each other, the Soft Step skeleton, persistent contacts, colouring from the first day, one scalar reference kernel and one four-wide kernel per instruction family, and enough game around it that the block world gains a ball to throw, a fountain, and a lava spring.
Evidence. luvcraft/physics.lisp is the engine, luvcraft/physics-simd.lisp
the wide kernels, luvcraft/balls.lisp the game's side, and
luvcraft/physics-tests.lisp the claims. A dropped ball rests a slop under
its radius and sleeps; a bouncy ball rises again and a dead one does not; a
ball rolling across cell seams neither hops nor slows; a pile of six sleeps
and a thrown ball wakes it; a walking box pushes a ball ahead of it; a
mortal body's contacts begin and end and it expires; a terrain edit wakes
what stood on it; and the NEON family reaches the same state hash as the
scalar family after 120 steps of a 200-ball pile (#7PAQ3M). In the game,
the ball hand item throws and scatters, the fountain and the lava spring
throw drops and gobbets, and a turtle struck by a ball turns away.
Done when. The eight tests above pass and the game runs them live. They
do, at commit f39a687 and after. What remains is listed in #NZCNHC.
Mentioned in: The sequence that stays fun, A seam, not a specification
The step as built #IDVK7G
One step-physics-world runs, in this order and with these owners:
- Reset. The event buffer empties and the terrain probe forgets its chunks: nothing borrowed survives a step.
- Grid. Every body, awake or asleep, is dropped into a uniform hash grid keyed by its centre, cell size a little over the largest diameter.
- Pairs. Each awake body walks the twenty-seven cells around it. A sleeping neighbour within reach is woken if the awake body is moving faster than the wake speed and is otherwise leaned on as if it were terrain; each awake pair is generated once, from the lower row. Woken bodies are appended to the awake set and this same pass reaches them.
- Terrain. Each awake body asks the probe about the solid cells within its radius plus a margin and offers one contact per exposed face, edge, or corner that passes the overshoot rule of #MYWH16.
- Boxes. Kinematic boxes the client posted -- the player, the animals -- first wake the sleepers they have walked into, then contact the awake set as moving terrain with a velocity.
- Prune. Contact rows whose pair was not seen this step are swap-removed and their hash entries patched; a touching one emits an end event.
- Prepare. Contacts are coloured (#SSMPYW) and the constraint buffer is written in colour order: anchors, tangent frame, masses, softness, the pre-solve approach speed, and the impulses carried from last step.
- Substeps. Four times: integrate velocities; warm start; solve with bias; integrate positions into the delta lanes; relax without bias, where friction and rolling resistance also act. Static contacts sit in the highest colours and are therefore solved last in each pass.
- Restitution, once, only for contacts that pushed and arrived above the threshold; then the impulses go back to their contact rows, touching flips emit begin and end events, and hard arrivals on boxes or on bodies that asked emit hit events.
- Finalize. Deltas fold into positions, the mortal age (the sleepers too), the slow accumulate sleep time and relocate to the sleeping set.
Bodies are rows of one generated columnar layout in two buffers -- awake and sleeping -- with a handle table as forwarding address (#B5N8JT); the awake buffer keeps one row past its end as the static dummy every one-sided contact reads. Contacts are rows of another layout, persistent, keyed by a packed pair; the constraint buffer is a third layout rebuilt each step. Every column is a specialized array; every kernel borrows them and runs a closed loop. The seam #K2F6WD asked for holds: the world, its parameters, its events, and its kernel family are CLOS and specials; nothing inside a substep dispatches.
Mentioned in: A seam, not a specification
Referenced from code: Advance The Soft Step loop.defun step-physics-world physics.lisp:2257 ↗
world by dt seconds (its step by default) and return it.
Events from the step are then readable until the next step begins. #IDVK7G
The voxel grid is the static tree, and the overshoot rule keeps seams smooth #MYWH16
A sphere near the lattice does not query a tree (#D9W4CH): it enumerates the cells within its reach through a probe that keeps up to eight chunks' storage borrowed for the step, so a neighbourhood that straddles a chunk edge costs a lookup only the first time. For each solid cell the closest point of the cell's box to the centre gives the contact: on a face, an edge, or a corner, or -- when the centre is inside the box -- through the face of least depth.
The rule that makes rolling across a floor of cells silent is about which of these contacts to keep. Let the overshoot of an axis be the direction in which the centre lies outside the box on that axis. A contact is offered only if no solid cell lies in any non-empty combination of the overshoot directions. A ball resting on cell A and reaching over the seam to cell B finds its closest point on B's top edge; but the centre overshoots B in the direction of A, A is solid, and so B offers nothing -- A's own top face, which is coplanar and nearer, is the only contact made. Convex edges and corners, where the overshoot cells are air, still offer their contacts, which is what lets a ball roll smoothly off a ledge. The rule is uniform: one neighbour to check for a face, three for an edge, seven for a corner; it needs the reach to stay under one cell, which bounds the radii this engine serves.
The same closest-point machinery serves kinematic boxes with every face exposed and no neighbour rule. Missing terrain is a wall, as it is for the walking bodies: a body near ground that has not streamed in leans on the boundary rather than falling through the world (#H2W9DJ).
Mentioned in: The step as built
Referenced from code: Give every awake body a contact with each exposed terrain face it is near. The voxel grid is the static tree (#D9W4CH). A cell's face, edge, or corner
is only offered when no solid cell lies in the direction the sphere's centre
overshoots it, which is what keeps a ball rolling across a floor of cells
from catching on the seams between them: the neighbouring cell's own face
is always the nearer, truer contact, and it is the only one made. #MYWH16 The neighbour rule needs the centre within one cell of the
cells it looks at.defun generate-physics-terrain-contacts physics.lisp:1292 ↗
Colours are the lanes: the constraint buffer is written in colour order #SSMPYW
Each step every live contact is given a colour by greedy bitset probing over awake rows (#G3W7KD): a body–body contact takes the lowest colour in which neither body is marked, from twenty colours; a contact with terrain or a box takes the highest free colour, marking only its body; what fits nowhere goes to the overflow colour. A counting sort then writes the constraint buffer so that each colour's constraints are contiguous, and a small array of colour starts is all a kernel needs.
Two things follow. Because static contacts hold the top colours, they are
solved last within every pass, the ordering Box3D found reduces
push-through. And because no two constraints of a colour touch the same
awake body, four consecutive constraints of a colour can be gathered into
f32.4 lanes -- twelve gathers of body state per side, the constraint
columns loaded straight -- solved together, and scattered back with no
lane's write landing on another lane's read. A colour's tail of fewer than
four goes to the scalar kernel. Kernels are generic functions on the
world's kernel family, dispatched once per phase, never per contact.
The measured colouring of a 3000-ball pile uses ten to twelve colours, so the twenty-four available are not close to overflow.
Mentioned in: The step as built
Referenced from code: Colour the live contacts and fill the constraint buffer in colour order.
Return the number of colours in use, the overflow colour counted. #SSMPYW Pass one: colour. Static contacts colour from the top down: solved last. Prefix sums: STARTS[c] is where colour C begins. Pass two: fill, with a moving cursor per colour. Anchors: from each centre to the contact point. A's
runs along the normal; B's against it. A tangent frame from the normal. The normal is near vertical: cross with X. Cross with Y. Approach speed now, for restitution later. Warm start from what the pair carried. How many colours actually got contacts.defun prepare-physics-constraints physics.lisp:1472 ↗
Bitwise agreement is the wide kernel's contract #7PAQ3M
The claim #E8M3JC recommends -- the scalar definition is the oracle of the fast one -- is made strictly here: after 120 steps of a 200-ball pile, or 200 steps of a 1000-ball one, the state hash of every position and velocity is identical between the scalar and the NEON families. What buys it:
- the wide arithmetic mirrors the scalar operation for operation, with the same associativity, and neither uses a fused multiply-add or an approximate reciprocal (#D2V7MK);
- where the scalar kernel branches -- speculative against soft bias, the friction cone, rolling resistance, whether a contact bounces -- the wide kernel computes both arms and blends, and pushes any divisor it will discard away from zero first;
- within a colour the order of constraints does not matter, since they share no body, so four-at-a-time is not a reordering.
One sharp edge was met and routed around: sb-simd 2.6.7's NEON
f32.4-sqrt is mis-encoded and raises SIGILL (#4UVLSQ), so the two square
roots a lane group needs -- the friction and rolling clamps -- go through
the scalar unit lane by lane, which is bit-identical to what the vector
instruction would give.
Mentioned in: The smallest solver was built: spheres, coloured contacts, four-wide lanes, Two sharp edges met in 2.6.7's NEON port
Sleep is relocation, and boxes and edits wake #QKS4GZ
A body slower than the sleep speed for half a second -- speed measured as
Box3D does, the larger of its velocity and half its position correction
rate (#P8N4TC) -- is copied to the sleeping buffer and swap-removed from
the awake one, with both moved rows' handles patched (#S5K3WM). Its
velocity is zeroed on the way; the eye draws both buffers. There are no
islands: a sleeping body is woken by any awake body faster than the wake
speed whose reach touches it, by any moving kinematic box that overlaps it,
by a terrain edit within a few cells (the client calls
wake-physics-bodies-near), or by having its velocity set. A slow awake
body resting on a sleeper leans on it as if it were terrain. Sleepers
still age: a mortal drop that comes to rest expires on time.
The consequence for a pile is Box3D's, one step at a time rather than by
island: a thrown ball wakes what it hits, those wake their neighbours next
step, and the whole heap sleeps again half a second after it stops.
validate-physics-world walks every forwarding address and every contact
key, the way b3ValidateSolverSets does, and the tests call it after each
relocation-heavy scenario.
Referenced from code: Move A sleeper is still: its velocity is not merely unread.defun sleep-physics-body physics.lisp:675 ↗
handle's body out of the awake set; return T if it was awake. #QKS4GZ
What a step costs #YYJGYO
Measured on the arm64 development machine, four substeps, no game running, each figure the mean over a run in which every body stays awake:
| bodies | contacts | scalar ms | NEON ms | colours |
|---|---|---|---|---|
| 1000 | 2342 | 1.50 | 1.15 | 9 |
| 3000 | 6687 | 4.56 | 3.96 | 10 |
The same 1000 balls settle into a sleeping pile in about two seconds, after which a step costs 0.1 ms; the wide kernels are worth about a fifth of the whole step because the solve is not the whole step. A statistical profile of the awake 3000-body case put the pair pass first, the NEON solve second, and preparation third; the pair pass is the twenty-seven-cell walk and its hash lookups, the natural next target if larger populations matter.
The game runs a fountain (two drops a step, life 2.6 s), a lava spring, and a few dozen balls at three to four hundred awake bodies for about 0.25 ms a step, and draws every body as a cube through the block pipeline in another 0.2 ms of vertex building.
Allocation: what the profile taught, and how not to be misled by it #QFMBFO
The step allocates only with change: a settled pile costs about a kilobyte a step, a violently churning 3000-body pile up to a hundred kilobytes. Three findings on the way there are worth keeping:
- The generated columnar
-pushstored every lane through a slot typedvector, so each single-float lane was boxed on the way in -- some four hundred bytes per push, paid by every columnar client. The lane slots now carry their precise specialized array type; the physics' own row-moving helpers (define-columnar-remove-swap,define-columnar-copy-row) were written typed from the start. This bears on #34G2K8: the swap-remove vocabulary the physics needed is order-free and lives beside it; the stable compaction that mark asks for is still the particle system's. - Generic-function calls with single-float arguments box those arguments, so a kernel called once per colour per phase per substep cost hundreds of boxes a step. Kernels are now called once per phase and loop over colours inside.
sb-ext:get-bytes-consedadvances only when an allocation region closes, so short measurements read zero, and it is process-wide, so a measurement taken while the game renders in the same image is the render thread's frame. Every allocation figure here was taken over hundreds of steps with no game playing. The statistical profiler's:allocmode charges the function running when a region overflows, which is usually the busiest one, not the allocating one; a heap breakdown before and after underwithout-gcingnames the object types instead.
What still allocates under churn was not run to ground; it is proportional to contacts made and dropped and to events, and small enough to leave for now (#NZCNHC).
Mentioned in: Open after the first pile
Things with weight, as the player meets them #JAU0EF
The engine knows spheres; the game knows kinds (*body-kinds* in
luvcraft/balls.lisp): a ball, a marble, a water drop, a lava gobbet, each
a tile of the atlas, a radius, a mass, a bounce, a friction, a life. Every
body is drawn as a cube of its tile turned by its orientation, written by
one closed loop into the block pipeline's vertex format, so a rolling ball
visibly rolls and a gobbet glows through the same bloom as a crystal.
The ball is a hand item: B takes it out, left click throws one from the eye
along the view with the player's own velocity added, right click scatters
a handful of marbles. The fountain and the lava spring are placeable
blocks of a spring-block-kind; the session finds them in the world's
authored edits, learns of new ones through luvcraft-block-placed, and each
step lets each one throw what it throws, deterministically from its
coordinate and a count. Water drops meet only terrain and boxes, not each
other, which is what lets a fountain be hundreds of bodies for a fraction
of a millisecond. The player and every animal are posted into the step as
kinematic boxes: a walking player kicks what is underfoot, and a ball
arriving hard on a turtle's box startles it (critter-struck), which is a
step event read back into an animal's mind.
Mentioned in: The sequence that stays fun
Open after the first pile #NZCNHC
- Islands. Waking spreads a step at a time and a jittering body keeps only its neighbours awake; a persistent island graph (#P8N4TC) would wake and sleep piles whole. Not needed at these populations.
- The pair pass. Twenty-seven cells per body and a hash lookup per candidate is honest but is now the top of the profile; a sort-based sweep or a two-by-two-by-two cell walk would halve it.
- Allocation under churn: named in #QFMBFO, not yet run to ground.
- Drawing. A cube per body through the CPU vertex path is the animals' path and is fine to a couple of thousand bodies; an instanced pipeline with a per-body record is the next step if the fountain should be a river.
- Sound and persistence: a ball landing makes no sound and is not saved.
- Shapes beyond spheres, and joints, remain what #G7Q3XR left out.
Mentioned in: A seam, not a specification, The smallest solver was built: spheres, coloured contacts, four-wide lanes, Allocation: what the profile taught, and how not to be misled by it
The block world is the static tree #D9W4CH
Box3D queries static geometry through an AABB tree because its static world is a soup of arbitrary shapes. A voxel world is better than a tree for this purpose: the grid is the acceleration structure, and a swept AABB enumerates candidate cells directly. The broad phase against terrain degenerates to arithmetic, and only body–body pairs need anything like Box3D's proxy machinery — at little-block-world scale, brute force over a handful of fat AABBs is honest and inspectable.
Two Box3D ideas still transfer:
- Fat bounds and speculative margin: contacts should exist slightly before touching, both to stabilize the solver (#R7F2QH) and to make begin-touch events lead visual contact. The margin constants are tunable data, not folklore.
- Derived products carry provenance: a chunk collider — even if it is just "the chunk itself plus a query function" — should record the source generations it observed, like meshes do, so a terrain edit invalidates exactly the artifacts whose proof included the old state (#H2W9DJ).
The chunk-boundary environment question returns with force here: a body standing on an absent neighbor chunk must not fall through the world. Missing is not air (#H2W9DJ); for physics the safe boundary policy is closer to "missing is temporarily solid" or "bodies near non-resident terrain do not simulate" — which is, pleasingly, exactly a sleep mechanism: put bodies whose support is absent into a not-yet-simulable set and wake them when residency arrives.
Mentioned in: The voxel grid is the static tree, and the overshoot rule keeps seams smooth
Referenced from code: Give every awake body a contact with each exposed terrain face it is near. The voxel grid is the static tree (#D9W4CH). A cell's face, edge, or corner
is only offered when no solid cell lies in the direction the sphere's centre
overshoots it, which is what keeps a ball rolling across a floor of cells
from catching on the seams between them: the neighbouring cell's own face
is always the nearer, truer contact, and it is the only one made. #MYWH16 The neighbour rule needs the centre within one cell of the
cells it looks at.defun generate-physics-terrain-contacts physics.lisp:1292 ↗
Sleep is residency, and it prices determinism #S6T2MV
Box3D's deepest structural rhyme with the block-world design: sleeping is relocation out of the iterated arrays (#S5K3WM), and chunk eviction is relocation out of the resident world (#W8D2MT). Both say that "inactive" should mean absent from the hot path, not flagged within it. A luv simulation adopting the domain model gets the shape of sleep almost for free: a sleeping island is one more materialization with a smaller schema.
Determinism is the decision this page must flag rather than make. Box3D shows it is a whole-design property — reduction shapes, ordering funnels, even the math library (#D2V7MK) — and that it is cheap to police once a state hash exists, but expensive to retrofit. Luv's situation is genuinely different: a live image where the user redefines a solver method mid-pile is gloriously, intentionally non-replayable. The honest options seem to be:
- declare determinism a non-goal and enjoy the freedom (Box3D's no-FMA, no-libm-trig diet stops applying);
- aim for same-image, fixed-code reproducibility — deterministic ordering and reductions, a per-step state hash as a regression tool — without cross-platform ambitions; or
- full Box3D-style determinism, priced into every kernel and every future backend tier of #R9M4PV (a foreign shim or GPU pass would have to match the Lisp oracle bitwise, which Accelerate and GPUs will not promise).
Option 2 looks like the workshop's natural stance: the state hash is a debugging instrument and a test oracle, not a shipped contract. But it should be chosen, not drifted into — the first parallel or foreign kernel sets it.
Referenced from code: A hash of every body's position and velocity, in set and row order, so
two runs of the same code can be compared. Same-image reproducibility is
the claim (#S6T2MV); this is how it is policed.defun physics-world-state-hash physics.lisp:2332 ↗
Questions to keep open #X3K8FP
- Is the body id a CLOS object reference with an id pool behind it, or an id struct with a CLOS object behind it — and which one do events carry?
- Where exactly do quantities (#L7C4GM, and the boundary-not-lanes stance of #B5L7VG) meet the kinematic columns: typed columns, typed accessors, or only typed boundaries?
- Does the contact domain own its manifolds as columns, or as per-contact structs while counts are small? Box3D needed bucketed pools only at mesh-contact scale.
- When terrain edits move blocks under a sleeping pile, who wakes it — the edit, the collider invalidation, or a broad-phase re-query?
- Which parts of a step could become GPU passes without changing the domain model — broad phase? integration? — and does the completion frontier (#T9K4RC) then own part of the step's transaction?
- What does the inspector show for one contact: its pair, its manifold, its color, its impulses, its events — and can that view stay cheap enough to keep open while a pile settles?
(world &optional dt)Advance WORLD by DT seconds (its step by default) and return it. Events from the step are then readable until the next step begins. #IDVK7G
Logical disjunction of tests and raw truth values.
The maximum of compatible quantities.
Substeps per step: the Soft Step loop runs this many times.
Division of two represented quantities.
The most a soft contact will push overlapping bodies apart, cells/s.
Approach speed below which nothing bounces, cells/s.
(probe world)(world)(world)Find every awake body's neighbours in the grid and give each near pair a contact. A moving awake body wakes a sleeping neighbour first; a slow one leans on it as on terrain. Woken bodies join the awake set at its end and are visited by this same pass.
(world)Give every awake body a contact with each exposed terrain face it is near. The voxel grid is the static tree (#D9W4CH). A cell's face, edge, or corner is only offered when no solid cell lies in the direction the sphere's centre overshoots it, which is what keeps a ball rolling across a floor of cells from catching…
(world)Drop every contact whose pair was not within reach this step, ending those that were touching.
(world h)Colour the live contacts and fill the constraint buffer in colour order. Return the number of colours in use, the overflow colour counted. #SSMPYW
Multiplication and scalar scaling.
(kernels awake h)Apply gravity and damping to the awake bodies over H.
(kernels constraints awake starts)Apply the carried impulses of every constraint, colour by colour. STARTS gives each colour's first row, with one more entry than colours.
(kernels constraints awake starts inv-h use-bias-p elapsed push-max)One Gauss-Seidel iteration over every colour in turn: normal impulses with soft or speculative bias when USE-BIAS-P, and without bias plus friction and rolling resistance when not. ELAPSED is how far into the step the substep is, for a kinematic other side's motion.
(kernels awake h)Advance the awake bodies' deltas and orientations by H.
Addition over compatible quantities.
(kernels constraints awake starts threshold)Bounce the constraints that hit hard enough, colour by colour.
(world)(world dt)Subtraction or unary negation.
How far past its radius a body looks for terrain and boxes, in cells. Wide enough that a fast body's contacts exist a step before it lands.
((bindings buffer buffer-type) &body body)Borrow BUFFER-TYPE's active extent, row declaration, and raw lane arrays. BINDINGS is (LENGTH ROW-DECLARATION (ARRAY LANE-NAME) ...). The buffer is evaluated once, and every array receives its precise specialized array type. This is the checked aggregate boundary for closed scalar or SIMD kernels; the kernel…
(handle)The minimum of compatible quantities.
(probe x y z)Convert one scalar float or unsigned value to a 32-bit float.
((nx ny nz px py pz separation)
(cx cy cz radius min-x min-y min-z max-x max-y max-z reach
&key (accept-p t))
&body body)Run BODY when the sphere at CX,CY,CZ is within REACH of the box, binding its contact normal, point, and separation. ACCEPT-P is evaluated with OUTSIDE-X OUTSIDE-Y OUTSIDE-Z bound to -1, 0, or 1 for the sphere centre's position against each axis of the box, and may refuse the contact.
Logical negation of one test or raw truth value.
Logical conjunction of tests and raw truth values.
(world key kind handle-a handle-b owner)Return the row of the contact KEY, making it if the pair is new. The row's LAST-STEP is stamped with the current step.
(kind index-a other)(x y z)The local index that means 'no body': the static side of a contact.
(world row nx ny nz px py pz kvx kvy kvz separation)(&optional (count "12"))Print COUNT fresh figure IDs that no page uses (default 12); never six hex digits, which the reader takes for a colour.
How many disjoint constraint colours to try before the overflow colour.
(world awake-count contact-count)Headings, paragraphs, figures and their IDs, mentions, marks.
Test whether two compatible scalars are equal.
(buffer count)(hertz zeta h)Contact stiffness as a frequency; zero would be rigid.
Contact softness damping ratio.
How much overlap a contact tolerates before it pushes, in cells.
(buffer-type buffer lane)BUFFER's LANE array with its precise specialized type, for the loops that borrow one lane at a time rather than a whole row schema.
Test whether one compatible scalar is greater than another.
The componentwise absolute value of a raw value.
The componentwise square root of a raw value.
Test whether one compatible scalar is less than another.
(world handle)(world handle)(world handle)(world handle from-set to-set)Move HANDLE's row from FROM-SET to TO-SET, patching every forwarding address the move disturbs.
(world)A hash of every body's position and velocity, in set and row order, so two runs of the same code can be compared. Same-image reproducibility is the claim (#S6T2MV); this is how it is policed.
Interpolate compatible quantities by a scalar amount.
The little block world began with CLOS because generic functions, inspection, class redefinition, and multiple dispatch are unusually pleasant tools for a live engine. A later interest in structure-of-arrays storage can sound like a retreat from that object-oriented view: if data belongs in columns, were the objects…
The order below is meant to keep each architectural proof attached to a new experience: – Look around a little world. Done: the camera inhabits generated terrain. – Touch it. Done as a first correctness pass: ray traversal selects, removes, and places blocks, and revisions make the edit visible to the renderer. –…
A first slice that exercises the seam without importing an engine: – Shapes: spheres only, against the block world. Sphere–voxel contact generation is simple enough to write in an afternoon and rich enough to produce piles, which are the test that matters. – Step: the Soft Step skeleton at its minimum — substeps of…
Intent. Build the slice #G7Q3XR proposed, no wider: spheres against the voxel lattice and each other, the Soft Step skeleton, persistent contacts, colouring from the first day, one scalar reference kernel and one four-wide kernel per instruction family, and enough game around it that the block world gains a ball to…
– Islands. Waking spreads a step at a time and a jittering body keeps only its neighbours awake; a persistent island graph (#P8N4TC) would wake and sleep piles whole. Not needed at these populations. – The pair pass. Twenty-seven cells per body and a hash lookup per candidate is honest but is now the top of the…
At minimum a finite domain answers: – how many sites exist; – which index type names one site; – how an index maps to a dense storage offset; and – how a dense offset maps back to an index. Additional capabilities should be explicit rather than presumed: – a neighborhood relation; – metric spacing or cell extent; –…
The id indexes a sparse array of small organizational structs that never move. b3Body (src/body.h) holds user data, intrusive linked lists of contacts and joints, sleep bookkeeping, mass properties — and, crucially, a forwarding address: setIndex (which solver set the body currently lives in) and localIndex (its…
The organizing structure of a world is the solver set (src/solver_set.h): set 0 holds static bodies, set 1 disabled ones, set 2 is the awake set, and every sleeping island gets a set of its own, numbered 3 and up. A set owns dense arrays of body sims, joint sims, and contact data. Only the awake set carries…
A contact object exists from the moment two fat AABBs overlap, long before touching. Non-touching contacts are cheap: an id in an index list, a persisted narrow-phase cache, no constraint. When the narrow phase first produces manifold points, the contact is promoted — linked into body contact lists and islands…
Constraints are assigned to one of 24 colors (src/constraint_graph.c) such that no two constraints in a color share a body. Assignment is greedy bitset probing over body ids; what doesn't fit lands in a designated overflow color, solved single-threaded. Two refinements carry design weight: – Constraints touching a…
One universal voxel bundle would combine values with very different causes and invalidation rules. A more legible arrangement might eventually include: – a mandatory block-content bundle; – a derived or incrementally maintained light bundle; – an optional fluid bundle; – ecological or geological readings over their…
A cell can be addressed without being an individual in the engine's object model. It normally has no independent allocation, ownership, or lifecycle; its identity is the site's identity inside a domain. Its block content, light, moisture, or temperature are values borne at that site. This suggests three useful…
Every public handle (include/box3d/id.h) is a small value struct: Three decisions are packed in here. The index is one-based so that a zero-initialized id is already the null id — C's cheapest initialization convention is recruited as API safety. The world index means any id is globally resolvable without a world…
Every backend wrapper has an explicit destroyed state. That gives a live Lisp object an invalid state which later operations can reject; continued CLOS reachability does not make a destroyed GPU object usable. Vulkan wrappers also install finalizers. Explicit destruction cancels the finalizer. If the collector…
A small experiment should attach ordinary inspectable field definitions to the block-content and light columns which already exist, without committing the game to a general framework or changing their storage. The concrete spatial extraction and field/kernel bridge are developed in #18MPLA. A later metaclass…
The advertised "Soft Step" solver is, structurally, the TGS-soft substepping scheme of Box2D v3 (comments cite Macklin's small-steps paper and Catto's soft-constraint talks). One step at timestep dt with n substeps of h = dt/n runs (b3SolverTask, src/solver.c): Softness is a three-number recipe (b3MakeSoft): a bias…
Empirical results from this machine (Homebrew SBCL 2.5.10, macOS arm64): – The build ships twenty contribs; sb-simd is not among them, and (require :sb-simd) fails. SBCL builds the contrib only for x86-64. – The host has no substrate: no sb-ext:simd-pack, no :sb-simd-pack feature, zero SIMD-related symbols in SB-EXT…
A hunch that "someone has surely done arm64 SIMD by now" turned out to be right, and recently. Checked against the SBCL repository and NEWS on 2026-08-11: – SBCL 2.6.7 (released 2026-07-28) announces: "the SB-SIMD contrib now supports ARM64. (Thanks to Sylvia Harrington)" — alongside AVX-512 support on x86-64 and…
Worth recording precisely, since luv can now experiment with SIMD in Lisp (see the sb-simd field notes): Box3D's SIMD is one abstraction header (src/simd.h) defining b3FloatW as SSE2, NEON, or a scalar struct of four floats, always width four. On top of it sit wide vector and symmetric-matrix types and exactly one…
Both live backends now represent completed work as a monotonically valued queue frontier, but the records attached to that frontier are different. submit-vulkan-command-buffers signals the queue timeline, installs a vulkan-gpu-submission record, and returns its index. Queue maintenance reads the semaphore counter and…
Ideas worth carrying into luv independent of instruction sets: – The X.Y naming discipline — element type and width as one lexical token, operations suffixed onto it — makes wide code readable and makes the scalar/wide relationship mechanical. A luv math layer can use that relationship directly for paired scalar and…
A subtlety easy to get wrong secondhand: the generic SB-SIMD package is not a portable SIMD API with scalar fallback. It defines only scalars — typed arithmetic, comparisons returning masks, typed aref over specialized arrays, an index type, and the instruction-set-case dispatch macro. It has no availability test…
There are no user callbacks during a step, with three narrow documented exceptions (pre-solve, custom filter, material mixers). Everything else is a buffer the user polls afterwards: body move events (one per awake body, with a fell-asleep flag patched retroactively by CCD and sleep), contact begin and end, hit…
The claim #E8M3JC recommends -- the scalar definition is the oracle of the fast one -- is made strictly here: after 120 steps of a 200-ball pile, or 200 steps of a 1000-ball one, the state hash of every position and velocity is identical between the scalar and the NEON families. What buys it: – the wide arithmetic…
Box3D splits every simulated thing into a stable sparse struct (identity, topology, user data) and moving dense rows (hot numerical state), with the sparse side holding a forwarding address that swap-removal must patch (#W2M9FJ). In the vocabulary of #C9L2WA this is precisely the distinction between a persistent…
This page thinks about a Catto-inspired physics experiment for the little block world, using three studies as material: the Box3D field notes (Box3D's architecture), the sb-simd field notes (sb-simd), and the domain/bundle model of #D5M8BZ. It tries to identify the seam: where CLOS objects, generic functions, and…
Meshing, ambient occlusion, lighting, fluids, and collision all inspect neighbors. At a chunk edge, the required value belongs to an environment larger than the chunk's finite storage. Possible explicit contexts include: – a block-world that resolves world coordinates through resident chunks, procedural sources, and…
The documentation claims bit-identical results across thread counts and platforms. The code backs this with converging mechanisms, none of which is a "determinism flag": – Work stealing changes only which worker computes a block, never the data or its internal order. Colored blocks write disjoint bodies;…
Building the physics kernels (#7PAQ3M) on the new arm64 backend found two defects, recorded here so the next kernel does not rediscover them: – sb-simd-neon:f32.4-sqrt raises SIGILL whenever it executes, on any operand: the vector square root's encoding is wrong. Every other operation the kernels use -- arithmetic,…
Islands (src/island.c) are connectivity components over awake bodies, maintained persistently rather than rebuilt each step. The asymmetry is the interesting decision: – Merging is eager: linking a contact or joint between islands appends the smaller island's members to the larger, immediately. – Splitting is lazy:…
The block-particle population is the next honest client. Its block reference, position, velocity, size, age, and lifetime naturally form quantity-aware SoA lanes, but its update removes arbitrary expired rows while preserving survivor order, and overflow discards an oldest prefix. The current generated…
The step allocates only with change: a settled pile costs about a kilobyte a step, a violently churning 3000-body pile up to a hundred kilobytes. Three findings on the way there are worth keeping: – The generated columnar -push stored every lane through a slot typed vector, so each single-float lane was boxed on the…
It is tempting to say that a block world is a three-dimensional array of block objects. That description becomes strained as soon as the world is larger than memory, generated on demand, edited sparsely, or represented at more than one resolution. A more general reading is: At full resolution, one site may have a…
The hot path of a physics step must not dispatch per site. The arrangement #R3F8YC anticipates — a generic operation selects a representation or phase once, borrows specialized arrays, and runs an ordinary optimized loop — maps directly onto how Box3D structures a step: prepare (gather from domains into per-step…
A semantic field and a storage lane are not the same thing. A field specification can name: – its meaning; – its logical value category; – default or missing-value semantics; – legal reconstruction or interpolation; – a physical storage representation; and – persistence and presentation conversions. Several…
Moppe stores quantities in typed Bundle columns. Étalon's Bundle(Domain, Row) experiment used Zig's MultiArrayList to prove a related point: a row schema can retain semantic quantity specifications while the physical data is columnar. It rejected a raw f64 field in the bundle schema even though the underlying…
One step-physics-world runs, in this order and with these owners: – Reset. The event buffer empties and the terrain probe forgets its chunks: nothing borrowed survives a step. – Grid. Every body, awake or asleep, is dropped into a uniform hash grid keyed by its centre, cell size a little over the largest diameter. –…
Each step every live contact is given a colour by greedy bitset probing over awake rows (#G3W7KD): a body–body contact takes the lowest colour in which neither body is marked, from twenty colours; a contact with terrain or a box takes the highest free colour, marking only its body; what fits nowhere goes to the…
A body slower than the sleep speed for half a second -- speed measured as Box3D does, the larger of its velocity and half its position correction rate (#P8N4TC) -- is copied to the sleeping buffer and swap-removed from the awake one, with both moved rows' handles patched (#S5K3WM). Its velocity is zeroed on the way;…
Box3D queries static geometry through an AABB tree because its static world is a soup of arbitrary shapes. A voxel world is better than a tree for this purpose: the grid is the acceleration structure, and a swept AABB enumerates candidate cells directly. The broad phase against terrain degenerates to arithmetic, and…
A sphere near the lattice does not query a tree (#D9W4CH): it enumerates the cells within its reach through a probe that keeps up to eight chunks' storage borrowed for the step, so a neighbourhood that straddles a chunk edge costs a lookup only the first time. For each solid cell the closest point of the cell's box…
Box3D's deepest structural rhyme with the block-world design: sleeping is relocation out of the iterated arrays (#S5K3WM), and chunk eviction is relocation out of the resident world (#W8D2MT). Both say that "inactive" should mean absent from the hot path, not flagged within it. A luv simulation adopting the domain…
Contacts.