diff --git a/aephysics/dynamics/module.ae b/aephysics/dynamics/module.ae index 7df1328..01004b8 100644 --- a/aephysics/dynamics/module.ae +++ b/aephysics/dynamics/module.ae @@ -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 @@ -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 @@ -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 { @@ -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 { diff --git a/aephysics/solver/module.ae b/aephysics/solver/module.ae index 8b849e7..a98f500 100644 --- a/aephysics/solver/module.ae +++ b/aephysics/solver/module.ae @@ -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 diff --git a/bench/RESULTS.md b/bench/RESULTS.md index 28a1749..af600d8 100644 --- a/bench/RESULTS.md +++ b/bench/RESULTS.md @@ -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