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
64 changes: 36 additions & 28 deletions aephysics/dynamics/module.ae
Original file line number Diff line number Diff line change
Expand Up @@ -395,24 +395,27 @@ struct BodyState {
}

// What integration and collision need of a body.
// What every constraint prepare reads first (the pose, the masses), then
// the collide pass's extents, the integration's loads, and the time of
// impact's start pose.
struct BodySim {
transform: Transform // of the body's origin
center: Vec3 // of mass, in world space
rotation0: Quat // for the time of impact
center0: Vec3
local_center: Vec3
force: Vec3
torque: Vec3
inv_mass: float
inv_inertia_local: Matrix3
inv_inertia_world: Matrix3
min_extent: float
body_id: int
flags: int
max_extent: Vec3
min_extent: float
force: Vec3
torque: Vec3
linear_damping: float
angular_damping: float
gravity_scale: float
body_id: int
flags: int
inv_inertia_local: Matrix3
rotation0: Quat // for the time of impact
center0: Vec3
}

// A shape in the world: its geometry (the shape module's Shape, with
Expand Down Expand Up @@ -833,13 +836,26 @@ struct RevoluteJoint {
enable_limit: bool
}

// The point constraint's fields first (every solve reads them), then the
// impulses and axes the warm start reads, the settings after.
struct SphericalJoint {
enable_spring: bool
enable_motor: bool
enable_cone_limit: bool
enable_twist_limit: bool
index_a: int
index_b: int
linear_impulse: Vec3
frame_a: Transform
frame_b: Transform
delta_center: Vec3
spring_impulse: Vec3
motor_impulse: Vec3
lower_twist_impulse: float
upper_twist_impulse: float
swing_impulse: float
swing_axis: Vec3
twist_jacobian: Vec3
hertz: float
damping_ratio: float
max_motor_torque: float
Expand All @@ -849,21 +865,10 @@ struct SphericalJoint {
cone_angle: float
cone_axis: Vec3 // unit, in frame A
target_rotation: Quat
index_a: int
index_b: int
frame_a: Transform
frame_b: Transform
delta_center: Vec3
swing_axis: Vec3
twist_jacobian: Vec3
rotation_mass: Matrix3
swing_mass: float
twist_mass: float
spring_softness: Softness
enable_spring: bool
enable_motor: bool
enable_cone_limit: bool
enable_twist_limit: bool
}

struct WeldJoint {
Expand Down Expand Up @@ -927,24 +932,27 @@ struct WheelJoint {
// (filled by the solver's prepare), the constraint's softness, the
// thresholds, and the kind's data in the block at the end (the
// reference's union: read through distance_joint_of and its siblings).
// What the warm start and the solve read comes first and the kind's data
// right after it, so a joint's pass touches as few lines as it can; the
// frames and settings only the prepare and the API read go last.
struct JointSim {
joint_id: int
kind: int
body_id_a: int
body_id_b: int
local_frame_a: Transform
local_frame_b: Transform
fixed_rotation: bool
inv_mass_a: float
inv_mass_b: float
inv_inertia_a: Matrix3
inv_inertia_b: Matrix3
constraint_hertz: float
constraint_damping_ratio: float
constraint_softness: Softness
force_threshold: float
force_threshold: float // the solve's joint event test
torque_threshold: float
fixed_rotation: bool
data: long[68] // JOINT_DATA_BYTES
joint_id: int
body_id_a: int
body_id_b: int
local_frame_a: Transform
local_frame_b: Transform
constraint_hertz: float
constraint_damping_ratio: float
}

struct JointPair {
Expand Down
7 changes: 6 additions & 1 deletion aephysics/solver/module.ae
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,18 @@ stage_clock() -> float[] {
return totals
}

// The averages, in the reference's own order and words.
// The averages since the last report, in the reference's own order and
// words; the clocks then start over, so a benchmark's next scene reports
// its own steps rather than an average over every scene so far.
stage_report() {
if stages_timed() == false || g_stage_steps == 0 || g_stage_ms == null { return }
clock = stage_clock()
n = g_stage_steps as float
println("aephysics stages ms/step: prepare_joints ${clock[STAGE_PREPARE_JOINTS] / n} prepare_wide ${clock[STAGE_PREPARE_WIDE] / n} prepare_contacts ${clock[STAGE_PREPARE_CONTACTS] / n} integrate_v ${clock[STAGE_INTEGRATE_VELOCITIES] / n} warm ${clock[STAGE_WARM_START] / n} solve ${clock[STAGE_SOLVE] / n} integrate_p ${clock[STAGE_INTEGRATE_POSITIONS] / n} relax ${clock[STAGE_RELAX] / n} restitution ${clock[STAGE_RESTITUTION] / n} store ${clock[STAGE_STORE_WIDE] / n}")
basics.free_bytes(clock as ptr, STAGE_COUNT * 8)
basics.free_bytes(g_stage_ms, native.MAX_WORKERS * STAGE_COUNT * 8)
g_stage_ms = null
g_stage_steps = 0
}

const NULL_INDEX = 0 - 1
Expand Down
45 changes: 44 additions & 1 deletion bench/RESULTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,50 @@ the eight-wide (AVX2) contact stages:
| store | 0.48 | 0.38 |

Matching that needs eight float lanes. Aether's `std.lanes` has `f32x4`
and no `f32x8`.
and no `f32x8` (aether-lang-dev/aether#2428).

### The joint grid, stage by stage (2026-10-05)

The joint grid has no contacts, so the eight-wide lanes don't explain its
gap. It is the scalar spherical joint, the same code as the reference's.
Each scene run alone (`AEPHYSICS_BENCH_SCENE=grid`), one thread:

| stage | aephysics before | aephysics after | Box3D `e77352c` |
|---|---|---|---|
| prepare joints | 1.21-1.42 ms | 1.27-1.49 | 0.61 |
| integrate velocities | 1.25 | 1.25 | 1.15 |
| warm start | 2.09-2.95 | 2.04-2.39 | 1.31 |
| solve | 4.36-4.98 | 3.90-4.14 | 3.33 |
| relax | 4.21-4.88 | 3.88-4.14 | 3.22 |
| step | 14.4-17.0 | 13.6-14.7 | 10.4-10.6 |

"After" is #107's layout. The solve and relax were reading about eleven
cache lines per joint, because what they need was spread through the
records. `JointSim` kept the local frames (read only by the prepare)
between its header and the masses, and `SphericalJoint` kept the enable
flags, tested first, 536 bytes in. `JointSim` now puts the solve's fields
first: kind, masses, inertias, softness and the joint event thresholds.
The kind's data comes next, and the frames and settings go last.
`SphericalJoint` puts its flags, body indices, impulse, frames and centre
delta first, then what the warm start reads. The arithmetic is unchanged
and the height sum is the same.

`BodySim` now starts with what every constraint prepare reads: the
transform, centres, inverse mass and world inertia, in three lines
instead of five. That took the joint prepare from 1.43-1.49 to 1.26-1.37 ms
and the many pyramids' wide prepare from 3.53-3.60 to 3.38-3.49. The
large pyramid was about even.

What's left of the gap is the records' size in double precision. Our
`JointSim` is about 900 bytes, against the reference's float union. A
`BodyState` is 112 bytes against 64.

Before #107, `AEPHYSICS_STAGES` never reset its clocks between scenes. A
benchmark's second and third scenes reported averages over every step
since the start, so the joint grid showed a wide prepare and a store
although it has no contacts. Stage figures for the many pyramids and the
joint grid from before then (in this file and on the issues) carry the
earlier scenes' stages. The large pyramid, which runs first, was right.

### Stage by stage

Expand Down
Loading