Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,15 @@ jobs:
- name: Checkout aephysics
uses: actions/checkout@v4

# Pin: 0.830.0, the latest release: its narrowed E0902 (a kept ptr
# parameter errors only when the struct or closure holding it escapes,
# aether#2981) is what lets the 20 @keep annotations that held a ptr
# only to pass it on go (f32x8 arrived in 0.788.0, aether#2428, which the
# wide contact solver is written in). A reproducible bootstrap
# Pin: 0.835.0, the latest release: its lane moves (aether#2550), bit
# casts and reductions (#2603) and `@target("avx2") import` (#2607) are
# what the wide gather, the wide separating axis test and the run-time
# width are written with. A reproducible bootstrap
# version: the floor this code needs, verified on Linux by this job.
- name: Install the released Aether toolchain and aeb
run: |
curl -fsSL https://raw.githubusercontent.com/aether-lang-dev/aeb/main/get.sh \
| AE_PIN=0.830.0 AEB_REF=v0.327 sh
| AE_PIN=0.835.0 AEB_REF=v0.328 sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Run the tests with ae
Expand Down Expand Up @@ -57,7 +56,7 @@ jobs:
- name: Install the released Aether toolchain and aeb
run: |
curl -fsSL https://raw.githubusercontent.com/aether-lang-dev/aeb/main/get.sh \
| AE_PIN=0.830.0 AEB_REF=v0.327 sh
| AE_PIN=0.835.0 AEB_REF=v0.328 sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Run the tests with ae
Expand Down
30 changes: 17 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ so a test written against the reference reads the same here.
| `aephysics.dynamic_tree` | the bounding volume hierarchy under the broad phase: SAH insertion, rotations, enlarge, sweep refit, partial rebuild in depth-first order, box / closest / ray / swept-box queries, the tree saved and loaded as bytes (b3DynamicTree_Save and _Load; every corrupt input refused); its boxes in single precision rounded outward from double, as the reference's large-world build keeps them (32-byte nodes; the costs and rays measured on the widened boxes) | done, `test_dynamic_tree.ae` (15k checks: TreeSaveLoadRoundtrip among them, the loaded tree's queries against a scan, a scale, ten corruptions refused, and centimetre boxes at 1e7 each held by its leaf and found by every query that touches it); [same tree as the reference; inserts 0.4x its time, queries 1.1x, rays 0.9x](bench/RESULTS.md#dynamic_tree) |
| `aephysics.hull` | quickhull with face merging, the half-edge hull with its mass properties, box / cylinder / cone / rock hulls and PEEL's convex (the reference's complex hull, the same 32 points), clone-and-transform with mirroring, support functions, ray cast, the 2D hull; every hull carries single-precision rows of its vertices and normals and its edges' cosines, padded to eight lanes, for the wide separating axis test | done, `test_hull.ae` (445 checks); [same hulls as the reference; sphere hulls at parity, merge-heavy cubes 2x, box hulls 3.6x (a heap block against its stack value)](bench/RESULTS.md#hull) |
| `aephysics.distance` | GJK with the warm-started simplex cache, the shape cast by conservative advancement, the time of impact by separating-axis root finding | done, `test_distance.ae` (1.1k checks); [same results as the reference; 1.1-1.2x its time, the time of impact faster](bench/RESULTS.md#distance) |
| `aephysics.manifold` | contact manifolds for sphere, capsule and hull in every pairing: the separating axis test with its cache (support searches eight vertices a pass over the hulls' rows, edge candidates culled by the inscribed spheres and a probe point, edge pairs eight a pass, every winner measured in double), reference-face clipping, the feature pairs, reduction to four points | done, `test_manifold.ae` (43k checks, 7,000 pairs against a brute-force oracle); [same manifolds as the reference, warm cache at parity](bench/RESULTS.md#manifold) |
| `aephysics.manifold` | contact manifolds for sphere, capsule and hull in every pairing: the separating axis test (aephysics.sat) with its cache, reference-face clipping, the feature pairs, reduction to four points | done, `test_manifold.ae` (43k checks, 7,000 pairs against a brute-force oracle); [same manifolds as the reference, warm cache at parity](bench/RESULTS.md#manifold) |
| `aephysics.sat` | the separating axis test between two hulls as the reference's wide one (convex_manifold_wide.inl): single precision from the hulls' rows, a support search whose vertex index rides in the low mantissa bits so one horizontal minimum finds it, edge candidates culled by the inscribed spheres and a probe point, B taken into A's frame and the edge pairs eight a pass, the lanes reduced in order; a second copy compiled for AVX2, chosen by the SIMD width | done, in `test_manifold.ae` (the 7,000-pair oracle) and `test_determinism.ae` (both widths, bit for bit); [1.11-1.47x the reference a query, from up to 2x](bench/RESULTS.md#the-references-wide-sat-the-lanes-transposed-the-width-at-run-time-2026-10-20-150-121-156) |
| `aephysics.triangle_manifold` | one mesh triangle against a sphere, capsule or hull: back-side cull with hysteresis, GJK shallow, the separating axis test deep with the triangle's edges as zero-area faces, the feature recorded for the mesh contact's ghost-collision reduction | done, `test_triangle_manifold.ae` (1.5k checks); [same manifolds as the reference, within 10% on hulls](bench/RESULTS.md#triangle_manifold) |
| `aephysics.mesh` | the triangle mesh: a BVH by binned SAH or median split with the triangles in depth-first order, vertex welding, edge flags, any scale including mirrored; overlap, ray cast, shape cast, the mover's planes, a box query | done, `test_mesh.ae` (1.6k checks); [same trees as the reference; the SAH build at parity, the median build 1.2-1.4x, rays and casts faster, the box query 1.5x](bench/RESULTS.md#mesh) |
| `aephysics.height_field` | the height field: quantised heights on a fixed diagonal, materials and holes per cell, edge flags per triangle, either winding; overlap, ray and shape casts by a walk along the grid, the mover's planes, a box query | done, `test_height_field.ae` (158 checks, casts against a brute force over a wave, the file roundtrip through `aephysics.files`: the definition back byte for byte, the reference's own text with CRLF lines, and every bad file refused); [same results and the same 1.3 MB as the reference; rays at parity, queries and casts faster, builds 1.1x](bench/RESULTS.md#height_field) |
Expand All @@ -47,7 +48,7 @@ so a test written against the reference reads the same here.
| `aephysics.mesh_contact` | a convex shape against a mesh or height field: the triangle cache with per-triangle warm starts, the manifolds per triangle, the seam rules against ghost collisions, clusters by normal, the four-point cull | done, `test_mesh_contact.ae` (64 checks); [a box on a wave at 3.2 us a step](bench/RESULTS.md#mesh_contact) |
| `aephysics.dynamics` | the world's state and bookkeeping: worlds, bodies (creation, mass from shapes or by hand, transforms, velocities, extents, locks, sleep and enable flags, the type changed, disabled and enabled) in solver sets, shapes of every kind on bodies with their broad-phase proxies, the hull database (a hull shape holds the world's copy, equal hulls share one, transformed hulls baked into their own), islands linked by contacts and joints and split by union-find, sleeping sets, waking and merging, the constraint graph's colouring, contacts from the broad phase's pairs with the narrow phase (convex, mesh, height field, compound children) and the state changes with their events, contact recycling, joints of every kind (definitions, creation with the edges, sets and islands, collide-connected in the pair filter, destruction, separations, reactions), sensors with their begin and end events | `test_dynamics.ae` (641 checks: test_body.c, the step-free test_world.c and test_joint.c, test_shape.c's names and flags, contacts, islands, sleep, joints, sensors); [creation 0.9x, the first collide at parity, recycling 1.1-1.2x, joints 0.6x, sensors 0.75-1.0x](bench/RESULTS.md#dynamics-bookkeeping-contacts-joints-sensors) |
| `aephysics.contact_solver` | the contact constraints of a step, scalar: prepared from the manifolds (anchors, base separations, normal masses, the friction centre and its tangent mass, twist and rolling masses), warm started, solved per sub-step (soft normal constraints with speculative bias, then central friction, twist friction and rolling resistance in the relax pass), the restitution pass, the impulses stored with the hit events; the step context | `test_contact_solver.ae` (50 checks: the passes by hand on a cube on a slab) | [ours alone](bench/RESULTS.md#contact_solver) |
| `aephysics.contact_solver_wide` | a colour's convex contacts eight at a time: the constraint of eight contacts as one single-precision record in lanes (`f32x8`, std.lanes -- the reference's SIMD path; one AVX2 register with `AETHER_AEPHYSICS_CFLAGS=-mavx2`, two four-lane halves without), the same prepare, warm start, solve, restitution and store, the bodies gathered into lanes and scattered back | `test_contact_solver_wide.ae` (one scene two ways -- the stack, the bounce with hit events, rolling resistance, a conveyor, a slide -- the lanes within a millimetre of the scalar solve, and the same lanes twice bit for bit) | [the falling grid: scalar 241 ms, lanes 142, the reference 155](bench/RESULTS.md#solver) |
| `aephysics.contact_solver_wide` | a colour's convex contacts eight at a time: the constraint of eight contacts as one single-precision record in lanes (`f32x8`, std.lanes -- the reference's SIMD path; one AVX2 register in its AVX2 copy or with `AETHER_AEPHYSICS_CFLAGS=-mavx2`, two four-lane halves otherwise), the same prepare, warm start, solve, restitution and store, the bodies gathered into lanes and scattered back through 8x8 register transposes; a second copy compiled for AVX2 that the solver runs where the CPU has it (basics.set_simd_width) | `test_contact_solver_wide.ae` (one scene two ways -- the stack, the bounce with hit events, rolling resistance, a conveyor, a slide -- the lanes within a millimetre of the scalar solve, and the same lanes twice bit for bit) | [the falling grid: scalar 241 ms, lanes 142, the reference 155](bench/RESULTS.md#solver) |
| `aephysics.joint_solver` | the seven joints solved: each kind's prepare (frames relative to the centres of mass, effective masses, spring softness), warm start and solve (rigid or soft constraints, speculative limits, springs, motors), the kinds' accessors (limits, springs, motors, current angles and translations, forces and torques), joint.c's dispatch with the constraint hertz clamp; beyond the reference, a spherical joint's cone may be centred off frame A's z and may be an ellipse of two half-angles (the circle stays the default, bit for bit) | `test_joint_solver.ae` (181 checks: test_joint.c's accessors on every kind, each kind solved by hand) | [ours alone](bench/RESULTS.md#joint_solver) |
| `aephysics.solver` | the Soft Step over the workers, the reference's stages: the work in blocks (the bodies', each colour's joints, wide and scalar constraints) that workers claim by compare-and-swap, the stages published as sync bits by worker 0 -- the constraints prepared, per sub-step the velocities integrated (gravity, damping, the gyroscopic torque), warm start, solve, positions, relax, colour by colour with the overflow first on worker 0; restitution, the impulses stored; the bodies finalised in parallel (sleep velocities, move events, fast bodies swept for the time of impact, bounds and proxies), the joint and hit events, the trees refit, bullets, the sensors' hits, islands put to sleep | `test_solver.ae` (45 checks: free fall against the closed form, resting and sleeping, a stack, a bounce and its hit event, a joint event, a pendulum, a fast sphere stopped by a thin wall, a bullet, a sensor swept, two worlds bit for bit alike) | [1.2-1.3x the reference's step, stage by stage](bench/RESULTS.md#stage-by-stage) |
| `aephysics.physics_world` | the world's face: the step (pairs, narrow phase, solve, sensors, events), the events read back, the settings and counters (the reference's whole `b3Counters`: byte count, tasks, manifold counts, time of impact iterations), the maximum capacity kept at each step's start, kinematic speculation (off by default, #167: a fast kinematic body's contacts reach out over its travel, so what it strikes is not found inside it), and travel speculation (off by default, #173: every contact does, and a moving dynamic body's bounds cover its next step, so a fast dynamic body such as a car does not find what it strikes inside it either), the memory part by part to the host's log, the world counts, version and CPU queries, queries over every shape (overlap of a box or a proxy, the mover's planes, ray, shape and mover casts, the closest ray hit) and against one body at a transform of the caller's, explosions | `test_physics_world.ae` (259 checks: test_world.c whole, among it the hull database (equal hulls one world-owned copy, a caller's hull freed at once, transformed hulls baked once, every holder's release), empty and recycled worlds, every setting, the worker count set between steps, contact, hit, move and sensor events, a compound child's hit events and materials, the overflow colour pile, the explosion near and far, the bullet and sleep flags, the compound count, an enlarged proxy destroyed; test_body_query.c whole: casts, overlaps of hull proxies and turned compounds, mover planes, and the mover's time of impact against 2,000 random sweeps; test_broad_phase.c's static rebuild and its stress test, a swarm through every proxy change with the pair set checked by brute force every step; test_id.c; the world queries; a wave pile stepping alike twice) | [the reference's own benchmark scenes with the same checksums: 1.2x its AVX2 build on the large pyramid, 1.15x on the many pyramids, 1.05x on the joint grid, ours at eight lanes in AVX2 with float records (#121)](bench/RESULTS.md#the-collide-pass-as-the-reference-keeps-it-2026-10-09-121); [the rest of its suite, nine scenes and the SAT runs, at 1.04-1.21x](bench/RESULTS.md#suite-the-references-benchmark-suite) |
Expand All @@ -64,11 +65,13 @@ Deliberate choices:
one `Vec3` and one `Transform`.
- **Determinism kept.** The approximate atan2, cosine and sine exist for
cross-platform replay and are kept, folded with the engine's own pi.
- **SIMD in the language.** The wide contact solver is the reference's
eight-lane path in Aether's own lanes (`f32x8`, std.lanes): one AVX2
register a lane group when built with `AETHER_AEPHYSICS_CFLAGS=-mavx2`,
two four-lane halves on any other x86-64 or arm64, the same bits either
way, and it agrees with the scalar solve.
- **SIMD in the language.** The wide contact solver and the separating axis
test are the reference's eight-lane paths in Aether's own lanes (`f32x8`,
std.lanes). Each is imported twice, plain and at `@target("avx2")`, and a
world runs the AVX2 copy where the CPU has it (`basics.set_simd_width`,
the reference's b3SetSIMDWidth): one binary runs eight lanes to a register
on AVX2 and two four-lane halves on any other x86-64 or arm64, the same
bits either way.
- **Threads.** The reference's task scheduler is ported as it is, on
threads Aether's std.worker makes, with the little C the threads need
(aephysics.native); the single-threaded path came first and the
Expand All @@ -81,14 +84,15 @@ Install the released toolchain with the shared, version-pinned installer

```sh
curl -fsSL https://raw.githubusercontent.com/aether-lang-dev/aeb/main/get.sh \
| AE_PIN=0.830.0 AEB_REF=v0.326 sh
| AE_PIN=0.835.0 AEB_REF=v0.328 sh
export PATH="$HOME/.local/bin:$PATH"
```

This installs `ae` 0.830.0, the release CI pins, and `aeb` v0.326 under
This installs `ae` 0.835.0, the release CI pins, and `aeb` v0.328 under
`~/.local` (`PREFIX` overrides it), using release binaries where available.
CI verifies only the pinned release; older ones are not tested (the
eight-wide lanes, `f32x8`, arrived in 0.788.0).
CI verifies only the pinned release; older ones are not tested (the lane
moves and `@target("avx2")` imports this code is written with arrived in
0.835.0).

```
scripts/test.sh # builds and runs every tests/test_*.ae with ae; what CI runs
Expand Down Expand Up @@ -165,8 +169,8 @@ falling ragdolls, each run to sleep twice and over four workers and
compared bit for bit; the query spawn sleeps a step after the
reference's with its 59 query hits, the mesh drop two; and its rolling
mix of spheres, capsules and boxes with rolling resistance, whose trace
folds the manifolds' impulses in, the scene of its SIMD width test: CI
runs the golden trace on the four-lane and the AVX2 builds; 22 checks) and `test_large_world.ae` (a stack, a bullet and the
folds the manifolds' impulses in, the scene of its SIMD width test, which here steps every scene at width
4 and at the AVX2 copy's 8 in one process, bit for bit; 30 checks) and `test_large_world.ae` (a stack, a bullet and the
origin-relative queries at x = 0 and at x = 1e7 agree, the whole
engine being in doubles; 43 checks), `test_collision.ae` (the reference's
test_collision.c: boxes valid and not, a ray against a box in its twelve
Expand Down
22 changes: 21 additions & 1 deletion aephysics/basics/module.ae
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
// hold. The allocation counters the original keeps (b3Alloc, b3Free with
// sizes) are kept so a leak is a number.
import std.string
import std.lanes
import aephysics.native

exports (
set_simd_width, get_simd_width,
BitSet, IdPool, HashSet, Ints, Buffer, Stack, Arena, BlockAllocator, BLOCK_SIZE,
block_allocator_create, block_allocator_destroy, block_allocate, block_free,
alloc, free_bytes, alloc_count, bytes_in_use, sort_ints, set_allocator, clear_allocator, set_log_fn, set_assert_fn, log,
Expand Down Expand Up @@ -132,6 +134,24 @@ set_log_fn(log: fn(string)) { hooks().log = log }
// of asserting; the one such failure is the scheduler's tasks overflowing.
set_assert_fn(failed: fn(string, string, int) -> int) { hooks().failed = failed }

// The lane width the wide stages run at, Box3D's b3SetSIMDWidth: 8 the copy of
// the wide contact solver and separating axis test compiled for AVX2
// (`@target("avx2") import`), 4 the build's own, 0 whatever this CPU runs
// (8 where it has AVX2). 8 on a CPU without AVX2 is 4. Another number leaves
// the width as it was. Both widths compute the same bits. A world takes the
// width when it is created (physics_world.enable_sse2_fallback forces it to
// 4); the separating axis test reads it on each call.
var g_simd_width = 0
set_simd_width(width: int) {
if width == 8 && lanes.cpu_has_avx2() == false { g_simd_width = 4; return }
if width == 0 || width == 4 || width == 8 { g_simd_width = width }
}
get_simd_width() -> int {
if g_simd_width != 0 { return g_simd_width }
if lanes.cpu_has_avx2() { return 8 }
return 4
}

// A message for the host.
log(text: string) {
if g_hooks == null {
Expand Down Expand Up @@ -594,7 +614,7 @@ name_cache_add(c: *NameCache, name: string) -> int {
if name == null { return NULL_NAME }
length = string.length(name)
if length == 0 { return NULL_NAME }
return name_cache_add_bytes(c, string.aether_string_raw_ptr(name), length)
return name_cache_add_bytes(c, (string.bytes(name) as ptr), length)
}

// The id of the `length` bytes at `text`, interned when they are new.
Expand Down
Loading
Loading