Repository navigation
All open issues: wide SAT and lane transposes, run-time SIMD width, joint accessors, integration hoists - #183
Merged
Merged
Conversation
The 79 one-line getters in joint_solver dereferenced sim_of's result unchecked, so asking a parallel joint's maximum torque of a motor joint (ae3d's balance assist while a ragdoll got up) read a null sim and took the process down, where the setters did nothing. Each getter now checks the sim as the setters do and gives its type's zero value (false, 0, the zero vector, the identity rotation), as joint_get_constraint_force answers a stale id. spherical_joint_set_cone_ellipse had the same unchecked read and does nothing now too. test_joint_solver asks every getter of every kind of a motor joint and of a destroyed joint's id (96 checks); on the old code it segfaults. Closes #181
…step integrate_velocities_range ran two pieces of per-step work on every sub-step for every awake body. The damping factor 1 / (1 + h c) is 1.0 exactly when c is 0, as for most bodies, so the divide is skipped then. The local inertia, inverted for the gyroscopic step (Box3D's own "todo wasteful computation"), is inverted on sub-step 0 and its six doubles kept in a per-step scratch array on the world's stack (StepContext.body_inertias), read back on the later sub-steps; a caller without the array computes it each time as before. Same doubles, same bits: the golden determinism trace matches bit for bit, 4 workers included. Large pyramid (-mavx2, best of 5 alternated runs): integrate_v 0.635 to 0.581 ms per step, -8.5%, the height sum unchanged. Closes #182
WideConstraint kept tangent2, cross(tangent1, normal), three f32x8 read by every warm start, solve and relax pass and by the store. It is now computed again where it is used: once a constraint in the warm start and the friction solve, once a slot of eight in the store. That is Box3D's own note (contact_solver_wide.inl: "todo test computing the tangents on the fly, at least tangent2"). The constraint is 96 bytes smaller, and the cross product is the one prepare computed, on the same single-precision lanes, so the bits are the same: the golden determinism trace matches bit for bit. Large pyramid (-mavx2, 8 alternated pairs on a loaded machine): step best 7.47 to 7.36 ms, median 7.69 to 7.56 (-1.7%); warm start, solve and store each 3-4% lower. For #121.
Each wide constraint gathered its eight bodies' states one field at a time, 104 scalar loads and inserts per side, and scattered them back the same way, on every warm start, solve, relax and restitution pass. Box3D loads a body as two rows of eight floats and transposes them in registers (b3GatherBodies, b3ScatterBodies, b3TransposeW8); with std.lanes' moves (Aether 0.835, aether#2550) it does the same here: a body state is the rows [v, pad, w, pad] and [dp, flags, dq], two transposes give the lanes, one transposes the velocities back and each dynamic body takes its row with one store (the pads written 0, which nothing reads). Pure data movement, so the bits are the same: the golden determinism trace matches bit for bit in the default and -mavx2 builds. Large pyramid (-mavx2, 6 alternated pairs): step best 6.97 to 6.57 ms, median 7.62 to 6.80; warm start median 0.91 to 0.72, solve 1.36 to 1.15, relax 1.49 to 1.25. For #121.
0.835.0 carries the lane moves, bit casts and reductions and the @target("avx2") import (aether#2550, #2603, #2607) that the wide gather already uses and the run-time width and the wide separating axis test need. Two things it changed are taken up here, no behaviour changed: - string.aether_string_raw_ptr is private now; the public view of a string's bytes is string.bytes(s), whose `as ptr` is the same pointer (null for an empty string, where every caller passes a length of 0). - ae check warns (W1006) on a call that names its module twice, hull.hull_vertices where hull.vertices reaches the same function (aether#2976): the 27 such calls across the modules, tests and benches take the short form. Every module passes ae check on 0.835.0 with no warning.
The width of the wide contact stages was the build's: -mavx2 made them eight lanes to a register and the binary faulted on a CPU without AVX2, so one executable for every player had to take the narrow build. The solver now imports contact_solver_wide twice, plain and at @target("avx2") (Aether 0.835.0, aether#2607), and runs each wide stage through the copy the world's width names, as Box3D's wide4.c / wide8.c and b3SetSIMDWidth do: - basics.set_simd_width(0 | 4 | 8) and get_simd_width: 0 asks the CPU (lanes.cpu_has_avx2), 8 without AVX2 is 4, another number is ignored. - A world takes the width when it is created (World.simd_width), and physics_world.enable_sse2_fallback (b3World_EnableSSE2Fallback, which the API audit had missed) forces it to 4. - physics_world.is_avx2_available reads lanes.cpu_has_avx2, and the C helper that did the same check is gone. - Each copy keeps its own zero record, made on one thread by the copy that runs the step. Both widths give the same bits. test_determinism runs every scene at 4 and at the CPU's width in one process (Box3D's SIMDWidthTest) and checks the AVX2 copy holds eight lanes a register; the golden trace matches bit for bit at the default width. test_host_api checks the width rules and the fallback. The bench takes AEPHYSICS_SIMD_WIDTH. In a default (non -mavx2) build, large pyramid, best of 4: width 4 8.18 ms a step, width 8 6.77 ms (solve 1.57 to 1.15, relax 1.79 to 1.23): the one binary runs at the AVX2 build's speed where it can. Closes #156
The hull SAT is now aephysics.sat, Box3D's b3ComputeSeparatingAxis (convex_manifold_wide.inl, e77352c) line for line, in single precision from the hulls' rows: - The support search keeps the vertex index in the low seven mantissa bits of its value (b3EmbedIndexW / b3MinIndexW), so one horizontal minimum gives the vertex, for every hull size (Aether 0.835.0's bit casts and reduce_min8, aether#2603). - The face dots and B's normals and vertices in A's frame are taken in lanes, the edge pairs eight of A's a pass, and the expensive part of a pass runs only when a lane passes all four arc tests. The improving lanes are reduced in order. - The Gauss-map tolerance is the reference's -LINEAR_SLOP^2. manifold.compute_separating_axis imports the module plain and at @target("avx2") and calls the copy basics.get_simd_width() names, as Box3D calls b3ComputeSeparatingAxisW4 or W8. Each copy keeps its own scratch, reserved with the manifold's; the edge contact has a small block of its own now that the old edge rows are gone. Results change on purpose: the double recompute of every winning edge pair is gone, so the golden trace is regenerated. Only the wave pile moved (it sleeps at step 320 where it slept at 326; Box3D's own sleeps at 314). The capsule, query, mesh and rolling scenes are unchanged. The new trace holds bit for bit in the default, -mavx2 and -mavx2 -mfma builds, and test_determinism steps every scene at both widths in one process. The brute-force edge test in test_manifold takes the reference's tolerance too; the 7,000-pair oracle holds (worst shortfall 4.0e-4, worst excess 3.7e-7). Measured on the reference's SAT runs, microseconds a query, ours before / after / Box3D in the same run (AVX2): complex/complex 1.83 / 1.32 / 0.95, cylinder/cylinder 2.98 / 2.02 / 1.41; without early returns complex/complex 15.6 / 7.43 / 6.15. Convex pile, best of three alternated pairs: collide 7.65 to 7.07 ms, step 11.32 to 10.74, and 1.31x to 1.20x Box3D's collide pass in the same period. Closes #150
nicolas-maman
marked this pull request as ready for review
October 20, 2026 15:00
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every open aephysics issue in one PR, on Aether 0.835.0 (which brought the lane features they waited on: aether#2550, #2603, #2607).
#150: the separating axis test as Box3D's wide one, at both widths. The new
aephysics.satmodule isb3ComputeSeparatingAxis(convex_manifold_wide.inl) line for line, in single precision from the hulls' rows:SAT runs against Box3D in the same run: 1.11-1.47x a query, from up to 2x. Convex pile collide: 1.31x to 1.20x Box3D's.
#121: the wide contact stages. Bodies are gathered and scattered through 8x8 register transposes (b3GatherBodies / b3ScatterBodies). The second tangent is computed where it is used instead of stored (Box3D's own todo). Against Box3D e77352c, same session, best of four:
Collide and store are now level or ahead on the large pyramid. What is left is about 1.15-1.2x in prepare, warm start, solve and relax, and integrate_p, which is still in double.
#156: run-time SIMD width. The solver and the SAT import their lane modules twice, plain and at
@target("avx2"). A world runs the copy its width names:basics.set_simd_width(0|4|8), Box3D's b3SetSIMDWidth;physics_world.enable_sse2_fallback, b3World_EnableSSE2Fallback, which the API audit had missed.A default build runs at the AVX2 build's speed where the CPU has AVX2 (large pyramid 8.18 to 6.77 ms) and runs anywhere else. test_determinism steps every scene at both widths in one process, bit for bit (Box3D's SIMDWidthTest).
#181: joint accessors of another kind or a stale id. The 79 one-line getters (and
spherical_joint_set_cone_ellipse) dereferenced a null sim and crashed. They now give their type's zero value, as the setters already did nothing. 96 new checks segfault on the old code.#182: velocity integration. The damping divide is skipped for undamped bodies, and the local inertia is inverted once a step instead of every sub-step (Box3D's own "todo wasteful computation"). integrate_v -8.5%, bit for bit.
Aether 0.835.0 and aeb v0.328.
string.bytesreplaces the now private raw pointer, and 27 calls take their short qualified form (W1006).The golden trace changed once, on purpose, in the SAT commit. The old SAT measured every winning edge pair again in double; the reference's does not. Only the wave pile moved: it sleeps at step 320, was 326, and Box3D's sleeps at 314. The new trace holds bit for bit in the default, -mavx2 and -mavx2 -mfma builds. The local gate passes (every module ae check clean on 0.835.0). Numbers are in bench/RESULTS.md, "The reference's wide SAT, the lanes transposed, the width at run time".
Closes #181
Closes #182
Closes #156
Closes #121
Closes #150
🤖 Generated with Claude Code