From a65492a1ae17009db8af1eec42899492fc338304 Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Mon, 19 Oct 2026 21:43:51 -0300 Subject: [PATCH 1/7] Joint accessors of another kind or a stale id: zero value, no crash 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 --- aephysics/joint_solver/module.ae | 492 +++++++++++++++++++++++++------ tests/test_joint_solver.ae | 137 ++++++++- 2 files changed, 545 insertions(+), 84 deletions(-) diff --git a/aephysics/joint_solver/module.ae b/aephysics/joint_solver/module.ae index 1dfdc9c..6903328 100644 --- a/aephysics/joint_solver/module.ae +++ b/aephysics/joint_solver/module.ae @@ -207,7 +207,11 @@ limit_scales(base: *JointSim, ctx: *StepContext, c: float, use_bias: bool) -> Li return LimitScales { bias: 0.0, mass_scale: 1.0, impulse_scale: 0.0 } } -// A joint id's sim when it is of the kind, else null. +// A joint id's sim when it is of the kind, else null. A null sim (a stale +// id, or a joint of another kind) leaves a setter doing nothing and gives a +// getter its type's zero value (false, 0, the zero vector, the identity +// rotation), as joint_get_constraint_force answers a stale id. Box3D asserts +// the kind in b3GetJointSimCheckType; here the call is a no-op, not a crash. sim_of(id: JointId, kind: int) -> *JointSim { return dynamics.get_joint_sim_of_kind(id, kind) } // A setter's number: finite and not below zero (stiffness, damping, @@ -402,13 +406,21 @@ distance_joint_set_length(id: JointId, length: float) { j.lower_impulse = 0.0 j.upper_impulse = 0.0 } -distance_joint_get_length(id: JointId) -> float { return dynamics.distance_joint_of(sim_of(id, dynamics.JOINT_DISTANCE)).length } +distance_joint_get_length(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_DISTANCE) + if base == null { return 0.0 } + return dynamics.distance_joint_of(base).length +} distance_joint_enable_limit(id: JointId, flag: bool) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_DISTANCE_JOINT_ENABLE_LIMIT); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_DISTANCE) if base != null { dynamics.distance_joint_of(base).enable_limit = flag } } -distance_joint_is_limit_enabled(id: JointId) -> bool { return dynamics.distance_joint_of(sim_of(id, dynamics.JOINT_DISTANCE)).enable_limit } +distance_joint_is_limit_enabled(id: JointId) -> bool { + base = sim_of(id, dynamics.JOINT_DISTANCE) + if base == null { return false } + return dynamics.distance_joint_of(base).enable_limit +} distance_joint_set_length_range(id: JointId, min_length: float, max_length: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_DISTANCE_JOINT_SET_LENGTH_RANGE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, min_length); dynamics.rec_float(recorder, max_length); dynamics.rec_end(recorder) } if math.is_valid_float(min_length) == false || math.is_valid_float(max_length) == false { return } @@ -423,8 +435,16 @@ distance_joint_set_length_range(id: JointId, min_length: float, max_length: floa j.lower_impulse = 0.0 j.upper_impulse = 0.0 } -distance_joint_get_min_length(id: JointId) -> float { return dynamics.distance_joint_of(sim_of(id, dynamics.JOINT_DISTANCE)).min_length } -distance_joint_get_max_length(id: JointId) -> float { return dynamics.distance_joint_of(sim_of(id, dynamics.JOINT_DISTANCE)).max_length } +distance_joint_get_min_length(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_DISTANCE) + if base == null { return 0.0 } + return dynamics.distance_joint_of(base).min_length +} +distance_joint_get_max_length(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_DISTANCE) + if base == null { return 0.0 } + return dynamics.distance_joint_of(base).max_length +} distance_joint_get_current_length(id: JointId) -> float { base = sim_of(id, dynamics.JOINT_DISTANCE) if base == null { return 0.0 } @@ -438,7 +458,11 @@ distance_joint_enable_spring(id: JointId, flag: bool) { base = sim_of(id, dynamics.JOINT_DISTANCE) if base != null { dynamics.distance_joint_of(base).enable_spring = flag } } -distance_joint_is_spring_enabled(id: JointId) -> bool { return dynamics.distance_joint_of(sim_of(id, dynamics.JOINT_DISTANCE)).enable_spring } +distance_joint_is_spring_enabled(id: JointId) -> bool { + base = sim_of(id, dynamics.JOINT_DISTANCE) + if base == null { return false } + return dynamics.distance_joint_of(base).enable_spring +} distance_joint_set_spring_force_range(id: JointId, lower: float, upper: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_DISTANCE_JOINT_SET_SPRING_FORCE_RANGE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, lower); dynamics.rec_float(recorder, upper); dynamics.rec_end(recorder) } if math.is_valid_float(lower) == false || math.is_valid_float(upper) == false { return } @@ -448,8 +472,16 @@ distance_joint_set_spring_force_range(id: JointId, lower: float, upper: float) { j.lower_spring_force = lower j.upper_spring_force = upper } -distance_joint_get_lower_spring_force(id: JointId) -> float { return dynamics.distance_joint_of(sim_of(id, dynamics.JOINT_DISTANCE)).lower_spring_force } -distance_joint_get_upper_spring_force(id: JointId) -> float { return dynamics.distance_joint_of(sim_of(id, dynamics.JOINT_DISTANCE)).upper_spring_force } +distance_joint_get_lower_spring_force(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_DISTANCE) + if base == null { return 0.0 } + return dynamics.distance_joint_of(base).lower_spring_force +} +distance_joint_get_upper_spring_force(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_DISTANCE) + if base == null { return 0.0 } + return dynamics.distance_joint_of(base).upper_spring_force +} distance_joint_set_spring_hertz(id: JointId, hertz: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_DISTANCE_JOINT_SET_SPRING_HERTZ); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, hertz); dynamics.rec_end(recorder) } if nonnegative(hertz) == false { return } @@ -462,8 +494,16 @@ distance_joint_set_spring_damping_ratio(id: JointId, ratio: float) { base = sim_of(id, dynamics.JOINT_DISTANCE) if base != null { dynamics.distance_joint_of(base).damping_ratio = ratio } } -distance_joint_get_spring_hertz(id: JointId) -> float { return dynamics.distance_joint_of(sim_of(id, dynamics.JOINT_DISTANCE)).hertz } -distance_joint_get_spring_damping_ratio(id: JointId) -> float { return dynamics.distance_joint_of(sim_of(id, dynamics.JOINT_DISTANCE)).damping_ratio } +distance_joint_get_spring_hertz(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_DISTANCE) + if base == null { return 0.0 } + return dynamics.distance_joint_of(base).hertz +} +distance_joint_get_spring_damping_ratio(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_DISTANCE) + if base == null { return 0.0 } + return dynamics.distance_joint_of(base).damping_ratio +} distance_joint_enable_motor(id: JointId, flag: bool) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_DISTANCE_JOINT_ENABLE_MOTOR); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_DISTANCE) @@ -474,14 +514,22 @@ distance_joint_enable_motor(id: JointId, flag: bool) { j.motor_impulse = 0.0 } } -distance_joint_is_motor_enabled(id: JointId) -> bool { return dynamics.distance_joint_of(sim_of(id, dynamics.JOINT_DISTANCE)).enable_motor } +distance_joint_is_motor_enabled(id: JointId) -> bool { + base = sim_of(id, dynamics.JOINT_DISTANCE) + if base == null { return false } + return dynamics.distance_joint_of(base).enable_motor +} distance_joint_set_motor_speed(id: JointId, speed: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_DISTANCE_JOINT_SET_MOTOR_SPEED); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, speed); dynamics.rec_end(recorder) } if math.is_valid_float(speed) == false { return } base = sim_of(id, dynamics.JOINT_DISTANCE) if base != null { dynamics.distance_joint_of(base).motor_speed = speed } } -distance_joint_get_motor_speed(id: JointId) -> float { return dynamics.distance_joint_of(sim_of(id, dynamics.JOINT_DISTANCE)).motor_speed } +distance_joint_get_motor_speed(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_DISTANCE) + if base == null { return 0.0 } + return dynamics.distance_joint_of(base).motor_speed +} distance_joint_get_motor_force(id: JointId) -> float { base = sim_of(id, dynamics.JOINT_DISTANCE) if base == null { return 0.0 } @@ -493,7 +541,11 @@ distance_joint_set_max_motor_force(id: JointId, force: float) { base = sim_of(id, dynamics.JOINT_DISTANCE) if base != null { dynamics.distance_joint_of(base).max_motor_force = force } } -distance_joint_get_max_motor_force(id: JointId) -> float { return dynamics.distance_joint_of(sim_of(id, dynamics.JOINT_DISTANCE)).max_motor_force } +distance_joint_get_max_motor_force(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_DISTANCE) + if base == null { return 0.0 } + return dynamics.distance_joint_of(base).max_motor_force +} // --- motor joint -------------------------------------------------------------------------------------------------- @@ -657,70 +709,110 @@ motor_joint_set_linear_velocity(id: JointId, v: Vec3) { base = sim_of(id, dynamics.JOINT_MOTOR) if base != null { dynamics.motor_joint_of(base).linear_velocity = v } } -motor_joint_get_linear_velocity(id: JointId) -> Vec3 { return dynamics.motor_joint_of(sim_of(id, dynamics.JOINT_MOTOR)).linear_velocity } +motor_joint_get_linear_velocity(id: JointId) -> Vec3 { + base = sim_of(id, dynamics.JOINT_MOTOR) + if base == null { return math.vec3_zero() } + return dynamics.motor_joint_of(base).linear_velocity +} motor_joint_set_angular_velocity(id: JointId, v: Vec3) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_MOTOR_JOINT_SET_ANGULAR_VELOCITY); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_vec3(recorder, v); dynamics.rec_end(recorder) } if math.is_valid_vec3(v) == false { return } base = sim_of(id, dynamics.JOINT_MOTOR) if base != null { dynamics.motor_joint_of(base).angular_velocity = v } } -motor_joint_get_angular_velocity(id: JointId) -> Vec3 { return dynamics.motor_joint_of(sim_of(id, dynamics.JOINT_MOTOR)).angular_velocity } +motor_joint_get_angular_velocity(id: JointId) -> Vec3 { + base = sim_of(id, dynamics.JOINT_MOTOR) + if base == null { return math.vec3_zero() } + return dynamics.motor_joint_of(base).angular_velocity +} motor_joint_set_max_velocity_torque(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_MOTOR_JOINT_SET_MAX_VELOCITY_TORQUE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_MOTOR) if base != null { dynamics.motor_joint_of(base).max_velocity_torque = value } } -motor_joint_get_max_velocity_torque(id: JointId) -> float { return dynamics.motor_joint_of(sim_of(id, dynamics.JOINT_MOTOR)).max_velocity_torque } +motor_joint_get_max_velocity_torque(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_MOTOR) + if base == null { return 0.0 } + return dynamics.motor_joint_of(base).max_velocity_torque +} motor_joint_set_max_velocity_force(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_MOTOR_JOINT_SET_MAX_VELOCITY_FORCE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_MOTOR) if base != null { dynamics.motor_joint_of(base).max_velocity_force = value } } -motor_joint_get_max_velocity_force(id: JointId) -> float { return dynamics.motor_joint_of(sim_of(id, dynamics.JOINT_MOTOR)).max_velocity_force } +motor_joint_get_max_velocity_force(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_MOTOR) + if base == null { return 0.0 } + return dynamics.motor_joint_of(base).max_velocity_force +} motor_joint_set_linear_hertz(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_MOTOR_JOINT_SET_LINEAR_HERTZ); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_MOTOR) if base != null { dynamics.motor_joint_of(base).linear_hertz = value } } -motor_joint_get_linear_hertz(id: JointId) -> float { return dynamics.motor_joint_of(sim_of(id, dynamics.JOINT_MOTOR)).linear_hertz } +motor_joint_get_linear_hertz(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_MOTOR) + if base == null { return 0.0 } + return dynamics.motor_joint_of(base).linear_hertz +} motor_joint_set_linear_damping_ratio(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_MOTOR_JOINT_SET_LINEAR_DAMPING_RATIO); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_MOTOR) if base != null { dynamics.motor_joint_of(base).linear_damping_ratio = value } } -motor_joint_get_linear_damping_ratio(id: JointId) -> float { return dynamics.motor_joint_of(sim_of(id, dynamics.JOINT_MOTOR)).linear_damping_ratio } +motor_joint_get_linear_damping_ratio(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_MOTOR) + if base == null { return 0.0 } + return dynamics.motor_joint_of(base).linear_damping_ratio +} motor_joint_set_angular_hertz(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_MOTOR_JOINT_SET_ANGULAR_HERTZ); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_MOTOR) if base != null { dynamics.motor_joint_of(base).angular_hertz = value } } -motor_joint_get_angular_hertz(id: JointId) -> float { return dynamics.motor_joint_of(sim_of(id, dynamics.JOINT_MOTOR)).angular_hertz } +motor_joint_get_angular_hertz(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_MOTOR) + if base == null { return 0.0 } + return dynamics.motor_joint_of(base).angular_hertz +} motor_joint_set_angular_damping_ratio(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_MOTOR_JOINT_SET_ANGULAR_DAMPING_RATIO); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_MOTOR) if base != null { dynamics.motor_joint_of(base).angular_damping_ratio = value } } -motor_joint_get_angular_damping_ratio(id: JointId) -> float { return dynamics.motor_joint_of(sim_of(id, dynamics.JOINT_MOTOR)).angular_damping_ratio } +motor_joint_get_angular_damping_ratio(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_MOTOR) + if base == null { return 0.0 } + return dynamics.motor_joint_of(base).angular_damping_ratio +} motor_joint_set_max_spring_force(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_MOTOR_JOINT_SET_MAX_SPRING_FORCE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_MOTOR) if base != null { dynamics.motor_joint_of(base).max_spring_force = value } } -motor_joint_get_max_spring_force(id: JointId) -> float { return dynamics.motor_joint_of(sim_of(id, dynamics.JOINT_MOTOR)).max_spring_force } +motor_joint_get_max_spring_force(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_MOTOR) + if base == null { return 0.0 } + return dynamics.motor_joint_of(base).max_spring_force +} motor_joint_set_max_spring_torque(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_MOTOR_JOINT_SET_MAX_SPRING_TORQUE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_MOTOR) if base != null { dynamics.motor_joint_of(base).max_spring_torque = value } } -motor_joint_get_max_spring_torque(id: JointId) -> float { return dynamics.motor_joint_of(sim_of(id, dynamics.JOINT_MOTOR)).max_spring_torque } +motor_joint_get_max_spring_torque(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_MOTOR) + if base == null { return 0.0 } + return dynamics.motor_joint_of(base).max_spring_torque +} // --- parallel joint ----------------------------------------------------------------------------------------------- @@ -810,14 +902,22 @@ parallel_joint_set_spring_hertz(id: JointId, value: float) { base = sim_of(id, dynamics.JOINT_PARALLEL) if base != null { dynamics.parallel_joint_of(base).hertz = value } } -parallel_joint_get_spring_hertz(id: JointId) -> float { return dynamics.parallel_joint_of(sim_of(id, dynamics.JOINT_PARALLEL)).hertz } +parallel_joint_get_spring_hertz(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_PARALLEL) + if base == null { return 0.0 } + return dynamics.parallel_joint_of(base).hertz +} parallel_joint_set_spring_damping_ratio(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_PARALLEL_JOINT_SET_SPRING_DAMPING_RATIO); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_PARALLEL) if base != null { dynamics.parallel_joint_of(base).damping_ratio = value } } -parallel_joint_get_spring_damping_ratio(id: JointId) -> float { return dynamics.parallel_joint_of(sim_of(id, dynamics.JOINT_PARALLEL)).damping_ratio } +parallel_joint_get_spring_damping_ratio(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_PARALLEL) + if base == null { return 0.0 } + return dynamics.parallel_joint_of(base).damping_ratio +} // A cap of zero or less is no torque at all. The solve is skipped then, // as the reference skips it, but the impulse the joint last applied stays // and is warm-started every sub-step after: the reference's @@ -835,7 +935,11 @@ parallel_joint_set_max_torque(id: JointId, value: float) { if value <= 0.0 { j.perp_impulse = Vec2 { x: 0.0, y: 0.0 } } } } -parallel_joint_get_max_torque(id: JointId) -> float { return dynamics.parallel_joint_of(sim_of(id, dynamics.JOINT_PARALLEL)).max_torque } +parallel_joint_get_max_torque(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_PARALLEL) + if base == null { return 0.0 } + return dynamics.parallel_joint_of(base).max_torque +} // --- prismatic joint ----------------------------------------------------------------------------------------------- @@ -1064,28 +1168,44 @@ prismatic_joint_enable_spring(id: JointId, flag: bool) { if flag != j.enable_spring { j.spring_impulse = 0.0 } j.enable_spring = flag } -prismatic_joint_is_spring_enabled(id: JointId) -> bool { return dynamics.prismatic_joint_of(sim_of(id, dynamics.JOINT_PRISMATIC)).enable_spring } +prismatic_joint_is_spring_enabled(id: JointId) -> bool { + base = sim_of(id, dynamics.JOINT_PRISMATIC) + if base == null { return false } + return dynamics.prismatic_joint_of(base).enable_spring +} prismatic_joint_set_spring_hertz(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_PRISMATIC_JOINT_SET_SPRING_HERTZ); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_PRISMATIC) if base != null { dynamics.prismatic_joint_of(base).hertz = value } } -prismatic_joint_get_spring_hertz(id: JointId) -> float { return dynamics.prismatic_joint_of(sim_of(id, dynamics.JOINT_PRISMATIC)).hertz } +prismatic_joint_get_spring_hertz(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_PRISMATIC) + if base == null { return 0.0 } + return dynamics.prismatic_joint_of(base).hertz +} prismatic_joint_set_spring_damping_ratio(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_PRISMATIC_JOINT_SET_SPRING_DAMPING_RATIO); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_PRISMATIC) if base != null { dynamics.prismatic_joint_of(base).damping_ratio = value } } -prismatic_joint_get_spring_damping_ratio(id: JointId) -> float { return dynamics.prismatic_joint_of(sim_of(id, dynamics.JOINT_PRISMATIC)).damping_ratio } +prismatic_joint_get_spring_damping_ratio(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_PRISMATIC) + if base == null { return 0.0 } + return dynamics.prismatic_joint_of(base).damping_ratio +} prismatic_joint_set_target_translation(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_PRISMATIC_JOINT_SET_TARGET_TRANSLATION); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if math.is_valid_float(value) == false { return } base = sim_of(id, dynamics.JOINT_PRISMATIC) if base != null { dynamics.prismatic_joint_of(base).target_translation = value } } -prismatic_joint_get_target_translation(id: JointId) -> float { return dynamics.prismatic_joint_of(sim_of(id, dynamics.JOINT_PRISMATIC)).target_translation } +prismatic_joint_get_target_translation(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_PRISMATIC) + if base == null { return 0.0 } + return dynamics.prismatic_joint_of(base).target_translation +} prismatic_joint_enable_limit(id: JointId, flag: bool) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_PRISMATIC_JOINT_ENABLE_LIMIT); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_PRISMATIC) @@ -1097,9 +1217,21 @@ prismatic_joint_enable_limit(id: JointId, flag: bool) { } j.enable_limit = flag } -prismatic_joint_is_limit_enabled(id: JointId) -> bool { return dynamics.prismatic_joint_of(sim_of(id, dynamics.JOINT_PRISMATIC)).enable_limit } -prismatic_joint_get_lower_limit(id: JointId) -> float { return dynamics.prismatic_joint_of(sim_of(id, dynamics.JOINT_PRISMATIC)).lower_translation } -prismatic_joint_get_upper_limit(id: JointId) -> float { return dynamics.prismatic_joint_of(sim_of(id, dynamics.JOINT_PRISMATIC)).upper_translation } +prismatic_joint_is_limit_enabled(id: JointId) -> bool { + base = sim_of(id, dynamics.JOINT_PRISMATIC) + if base == null { return false } + return dynamics.prismatic_joint_of(base).enable_limit +} +prismatic_joint_get_lower_limit(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_PRISMATIC) + if base == null { return 0.0 } + return dynamics.prismatic_joint_of(base).lower_translation +} +prismatic_joint_get_upper_limit(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_PRISMATIC) + if base == null { return 0.0 } + return dynamics.prismatic_joint_of(base).upper_translation +} prismatic_joint_set_limits(id: JointId, lower: float, upper: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_PRISMATIC_JOINT_SET_LIMITS); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, lower); dynamics.rec_float(recorder, upper); dynamics.rec_end(recorder) } if math.is_valid_float(lower) == false || math.is_valid_float(upper) == false { return } @@ -1129,21 +1261,33 @@ prismatic_joint_enable_motor(id: JointId, flag: bool) { if flag != j.enable_motor { j.motor_impulse = 0.0 } j.enable_motor = flag } -prismatic_joint_is_motor_enabled(id: JointId) -> bool { return dynamics.prismatic_joint_of(sim_of(id, dynamics.JOINT_PRISMATIC)).enable_motor } +prismatic_joint_is_motor_enabled(id: JointId) -> bool { + base = sim_of(id, dynamics.JOINT_PRISMATIC) + if base == null { return false } + return dynamics.prismatic_joint_of(base).enable_motor +} prismatic_joint_set_motor_speed(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_PRISMATIC_JOINT_SET_MOTOR_SPEED); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if math.is_valid_float(value) == false { return } base = sim_of(id, dynamics.JOINT_PRISMATIC) if base != null { dynamics.prismatic_joint_of(base).motor_speed = value } } -prismatic_joint_get_motor_speed(id: JointId) -> float { return dynamics.prismatic_joint_of(sim_of(id, dynamics.JOINT_PRISMATIC)).motor_speed } +prismatic_joint_get_motor_speed(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_PRISMATIC) + if base == null { return 0.0 } + return dynamics.prismatic_joint_of(base).motor_speed +} prismatic_joint_set_max_motor_force(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_PRISMATIC_JOINT_SET_MAX_MOTOR_FORCE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_PRISMATIC) if base != null { dynamics.prismatic_joint_of(base).max_motor_force = value } } -prismatic_joint_get_max_motor_force(id: JointId) -> float { return dynamics.prismatic_joint_of(sim_of(id, dynamics.JOINT_PRISMATIC)).max_motor_force } +prismatic_joint_get_max_motor_force(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_PRISMATIC) + if base == null { return 0.0 } + return dynamics.prismatic_joint_of(base).max_motor_force +} prismatic_joint_get_motor_force(id: JointId) -> float { base = sim_of(id, dynamics.JOINT_PRISMATIC) if base == null { return 0.0 } @@ -1370,9 +1514,21 @@ revolute_joint_enable_limit(id: JointId, flag: bool) { } j.enable_limit = flag } -revolute_joint_is_limit_enabled(id: JointId) -> bool { return dynamics.revolute_joint_of(sim_of(id, dynamics.JOINT_REVOLUTE)).enable_limit } -revolute_joint_get_lower_limit(id: JointId) -> float { return dynamics.revolute_joint_of(sim_of(id, dynamics.JOINT_REVOLUTE)).lower_angle } -revolute_joint_get_upper_limit(id: JointId) -> float { return dynamics.revolute_joint_of(sim_of(id, dynamics.JOINT_REVOLUTE)).upper_angle } +revolute_joint_is_limit_enabled(id: JointId) -> bool { + base = sim_of(id, dynamics.JOINT_REVOLUTE) + if base == null { return false } + return dynamics.revolute_joint_of(base).enable_limit +} +revolute_joint_get_lower_limit(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_REVOLUTE) + if base == null { return 0.0 } + return dynamics.revolute_joint_of(base).lower_angle +} +revolute_joint_get_upper_limit(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_REVOLUTE) + if base == null { return 0.0 } + return dynamics.revolute_joint_of(base).upper_angle +} revolute_joint_set_limits(id: JointId, lower: float, upper: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_REVOLUTE_JOINT_SET_LIMITS); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, lower); dynamics.rec_float(recorder, upper); dynamics.rec_end(recorder) } if math.is_valid_float(lower) == false || math.is_valid_float(upper) == false { return } @@ -1403,28 +1559,44 @@ revolute_joint_enable_spring(id: JointId, flag: bool) { if flag != j.enable_spring { j.spring_impulse = 0.0 } j.enable_spring = flag } -revolute_joint_is_spring_enabled(id: JointId) -> bool { return dynamics.revolute_joint_of(sim_of(id, dynamics.JOINT_REVOLUTE)).enable_spring } +revolute_joint_is_spring_enabled(id: JointId) -> bool { + base = sim_of(id, dynamics.JOINT_REVOLUTE) + if base == null { return false } + return dynamics.revolute_joint_of(base).enable_spring +} revolute_joint_set_target_angle(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_REVOLUTE_JOINT_SET_TARGET_ANGLE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if math.is_valid_float(value) == false { return } base = sim_of(id, dynamics.JOINT_REVOLUTE) if base != null { dynamics.revolute_joint_of(base).target_angle = value } } -revolute_joint_get_target_angle(id: JointId) -> float { return dynamics.revolute_joint_of(sim_of(id, dynamics.JOINT_REVOLUTE)).target_angle } +revolute_joint_get_target_angle(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_REVOLUTE) + if base == null { return 0.0 } + return dynamics.revolute_joint_of(base).target_angle +} revolute_joint_set_spring_hertz(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_REVOLUTE_JOINT_SET_SPRING_HERTZ); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_REVOLUTE) if base != null { dynamics.revolute_joint_of(base).hertz = value } } -revolute_joint_get_spring_hertz(id: JointId) -> float { return dynamics.revolute_joint_of(sim_of(id, dynamics.JOINT_REVOLUTE)).hertz } +revolute_joint_get_spring_hertz(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_REVOLUTE) + if base == null { return 0.0 } + return dynamics.revolute_joint_of(base).hertz +} revolute_joint_set_spring_damping_ratio(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_REVOLUTE_JOINT_SET_SPRING_DAMPING_RATIO); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_REVOLUTE) if base != null { dynamics.revolute_joint_of(base).damping_ratio = value } } -revolute_joint_get_spring_damping_ratio(id: JointId) -> float { return dynamics.revolute_joint_of(sim_of(id, dynamics.JOINT_REVOLUTE)).damping_ratio } +revolute_joint_get_spring_damping_ratio(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_REVOLUTE) + if base == null { return 0.0 } + return dynamics.revolute_joint_of(base).damping_ratio +} revolute_joint_enable_motor(id: JointId, flag: bool) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_REVOLUTE_JOINT_ENABLE_MOTOR); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_REVOLUTE) @@ -1433,21 +1605,33 @@ revolute_joint_enable_motor(id: JointId, flag: bool) { if flag != j.enable_motor { j.motor_impulse = 0.0 } j.enable_motor = flag } -revolute_joint_is_motor_enabled(id: JointId) -> bool { return dynamics.revolute_joint_of(sim_of(id, dynamics.JOINT_REVOLUTE)).enable_motor } +revolute_joint_is_motor_enabled(id: JointId) -> bool { + base = sim_of(id, dynamics.JOINT_REVOLUTE) + if base == null { return false } + return dynamics.revolute_joint_of(base).enable_motor +} revolute_joint_set_motor_speed(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_REVOLUTE_JOINT_SET_MOTOR_SPEED); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if math.is_valid_float(value) == false { return } base = sim_of(id, dynamics.JOINT_REVOLUTE) if base != null { dynamics.revolute_joint_of(base).motor_speed = value } } -revolute_joint_get_motor_speed(id: JointId) -> float { return dynamics.revolute_joint_of(sim_of(id, dynamics.JOINT_REVOLUTE)).motor_speed } +revolute_joint_get_motor_speed(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_REVOLUTE) + if base == null { return 0.0 } + return dynamics.revolute_joint_of(base).motor_speed +} revolute_joint_set_max_motor_torque(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_REVOLUTE_JOINT_SET_MAX_MOTOR_TORQUE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_REVOLUTE) if base != null { dynamics.revolute_joint_of(base).max_motor_torque = value } } -revolute_joint_get_max_motor_torque(id: JointId) -> float { return dynamics.revolute_joint_of(sim_of(id, dynamics.JOINT_REVOLUTE)).max_motor_torque } +revolute_joint_get_max_motor_torque(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_REVOLUTE) + if base == null { return 0.0 } + return dynamics.revolute_joint_of(base).max_motor_torque +} revolute_joint_get_motor_torque(id: JointId) -> float { base = sim_of(id, dynamics.JOINT_REVOLUTE) if base == null { return 0.0 } @@ -1650,8 +1834,16 @@ spherical_joint_enable_cone_limit(id: JointId, flag: bool) { if flag != j.enable_cone_limit { j.swing_impulse = 0.0 } j.enable_cone_limit = flag } -spherical_joint_is_cone_limit_enabled(id: JointId) -> bool { return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).enable_cone_limit } -spherical_joint_get_cone_limit(id: JointId) -> float { return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).cone_angle } +spherical_joint_is_cone_limit_enabled(id: JointId) -> bool { + base = sim_of(id, dynamics.JOINT_SPHERICAL) + if base == null { return false } + return dynamics.spherical_joint_of(base).enable_cone_limit +} +spherical_joint_get_cone_limit(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_SPHERICAL) + if base == null { return 0.0 } + return dynamics.spherical_joint_of(base).cone_angle +} // Clamped as the joint's creation clamps it: to a right angle about frame // A's z, to 0.8 pi about a centre off it. spherical_joint_set_cone_limit(id: JointId, angle: float) { @@ -1679,7 +1871,11 @@ spherical_joint_set_cone_axis(id: JointId, axis: Vec3) { dynamics.set_spherical_cone_axis(j, axis) j.cone_angle = dynamics.clamp_spherical_cone(j, j.cone_angle) } -spherical_joint_get_cone_axis(id: JointId) -> Vec3 { return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).cone_axis } +spherical_joint_get_cone_axis(id: JointId) -> Vec3 { + base = sim_of(id, dynamics.JOINT_SPHERICAL) + if base == null { return math.vec3_zero() } + return dynamics.spherical_joint_of(base).cone_axis +} // An elliptical cone: `major_angle` toward `major_axis` (frame A, taken // across the centre; zero for frame A's x) and `minor_angle` across it, // each clamped as the circle's angle is. A circle again when the angles @@ -1687,16 +1883,22 @@ spherical_joint_get_cone_axis(id: JointId) -> Vec3 { return dynamics.spherical_j spherical_joint_set_cone_ellipse(id: JointId, major_angle: float, minor_angle: float, major_axis: Vec3) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_SET_CONE_ELLIPSE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, major_angle); dynamics.rec_float(recorder, minor_angle); dynamics.rec_vec3(recorder, major_axis); dynamics.rec_end(recorder) } if nonnegative(major_angle) == false || nonnegative(minor_angle) == false || math.is_valid_vec3(major_axis) == false { return } - j = dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)) + base = sim_of(id, dynamics.JOINT_SPHERICAL) + if base == null { return } + j = dynamics.spherical_joint_of(base) j.cone_angle = dynamics.clamp_spherical_cone(j, major_angle) j.cone_minor_angle = dynamics.spherical_cone_minor(j, minor_angle) j.cone_major_axis = math.narrow3(major_axis) } spherical_joint_get_cone_minor_angle(id: JointId) -> float { - return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).cone_minor_angle as float + base = sim_of(id, dynamics.JOINT_SPHERICAL) + if base == null { return 0.0 } + return dynamics.spherical_joint_of(base).cone_minor_angle as float } spherical_joint_get_cone_major_axis(id: JointId) -> Vec3 { - return math.widen3(dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).cone_major_axis) + base = sim_of(id, dynamics.JOINT_SPHERICAL) + if base == null { return math.vec3_zero() } + return math.widen3(dynamics.spherical_joint_of(base).cone_major_axis) } spherical_joint_enable_twist_limit(id: JointId, flag: bool) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_ENABLE_TWIST_LIMIT); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } @@ -1709,9 +1911,21 @@ spherical_joint_enable_twist_limit(id: JointId, flag: bool) { } j.enable_twist_limit = flag } -spherical_joint_is_twist_limit_enabled(id: JointId) -> bool { return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).enable_twist_limit } -spherical_joint_get_lower_twist_limit(id: JointId) -> float { return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).lower_twist_angle } -spherical_joint_get_upper_twist_limit(id: JointId) -> float { return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).upper_twist_angle } +spherical_joint_is_twist_limit_enabled(id: JointId) -> bool { + base = sim_of(id, dynamics.JOINT_SPHERICAL) + if base == null { return false } + return dynamics.spherical_joint_of(base).enable_twist_limit +} +spherical_joint_get_lower_twist_limit(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_SPHERICAL) + if base == null { return 0.0 } + return dynamics.spherical_joint_of(base).lower_twist_angle +} +spherical_joint_get_upper_twist_limit(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_SPHERICAL) + if base == null { return 0.0 } + return dynamics.spherical_joint_of(base).upper_twist_angle +} spherical_joint_set_twist_limits(id: JointId, lower: float, upper: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_SET_TWIST_LIMITS); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, lower); dynamics.rec_float(recorder, upper); dynamics.rec_end(recorder) } if math.is_valid_float(lower) == false || math.is_valid_float(upper) == false { return } @@ -1735,28 +1949,44 @@ spherical_joint_enable_spring(id: JointId, flag: bool) { if flag != j.enable_spring { j.spring_impulse = math.vec3_zero() } j.enable_spring = flag } -spherical_joint_is_spring_enabled(id: JointId) -> bool { return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).enable_spring } +spherical_joint_is_spring_enabled(id: JointId) -> bool { + base = sim_of(id, dynamics.JOINT_SPHERICAL) + if base == null { return false } + return dynamics.spherical_joint_of(base).enable_spring +} spherical_joint_set_target_rotation(id: JointId, q: Quat) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_SET_TARGET_ROTATION); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_quat(recorder, q); dynamics.rec_end(recorder) } if math.is_valid_quat(q) == false { return } base = sim_of(id, dynamics.JOINT_SPHERICAL) if base != null { dynamics.spherical_joint_of(base).target_rotation = q } } -spherical_joint_get_target_rotation(id: JointId) -> Quat { return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).target_rotation } +spherical_joint_get_target_rotation(id: JointId) -> Quat { + base = sim_of(id, dynamics.JOINT_SPHERICAL) + if base == null { return math.quat_identity() } + return dynamics.spherical_joint_of(base).target_rotation +} spherical_joint_set_spring_hertz(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_SET_SPRING_HERTZ); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_SPHERICAL) if base != null { dynamics.spherical_joint_of(base).hertz = value } } -spherical_joint_get_spring_hertz(id: JointId) -> float { return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).hertz } +spherical_joint_get_spring_hertz(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_SPHERICAL) + if base == null { return 0.0 } + return dynamics.spherical_joint_of(base).hertz +} spherical_joint_set_spring_damping_ratio(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_SET_SPRING_DAMPING_RATIO); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_SPHERICAL) if base != null { dynamics.spherical_joint_of(base).damping_ratio = value } } -spherical_joint_get_spring_damping_ratio(id: JointId) -> float { return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).damping_ratio } +spherical_joint_get_spring_damping_ratio(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_SPHERICAL) + if base == null { return 0.0 } + return dynamics.spherical_joint_of(base).damping_ratio +} spherical_joint_enable_motor(id: JointId, flag: bool) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_ENABLE_MOTOR); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_SPHERICAL) @@ -1765,21 +1995,33 @@ spherical_joint_enable_motor(id: JointId, flag: bool) { if flag != j.enable_motor { j.motor_impulse = math.vec3_zero() } j.enable_motor = flag } -spherical_joint_is_motor_enabled(id: JointId) -> bool { return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).enable_motor } +spherical_joint_is_motor_enabled(id: JointId) -> bool { + base = sim_of(id, dynamics.JOINT_SPHERICAL) + if base == null { return false } + return dynamics.spherical_joint_of(base).enable_motor +} spherical_joint_set_motor_velocity(id: JointId, v: Vec3) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_SET_MOTOR_VELOCITY); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_vec3(recorder, v); dynamics.rec_end(recorder) } if math.is_valid_vec3(v) == false { return } base = sim_of(id, dynamics.JOINT_SPHERICAL) if base != null { dynamics.spherical_joint_of(base).motor_velocity = v } } -spherical_joint_get_motor_velocity(id: JointId) -> Vec3 { return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).motor_velocity } +spherical_joint_get_motor_velocity(id: JointId) -> Vec3 { + base = sim_of(id, dynamics.JOINT_SPHERICAL) + if base == null { return math.vec3_zero() } + return dynamics.spherical_joint_of(base).motor_velocity +} spherical_joint_set_max_motor_torque(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_SPHERICAL_JOINT_SET_MAX_MOTOR_TORQUE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_SPHERICAL) if base != null { dynamics.spherical_joint_of(base).max_motor_torque = value } } -spherical_joint_get_max_motor_torque(id: JointId) -> float { return dynamics.spherical_joint_of(sim_of(id, dynamics.JOINT_SPHERICAL)).max_motor_torque } +spherical_joint_get_max_motor_torque(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_SPHERICAL) + if base == null { return 0.0 } + return dynamics.spherical_joint_of(base).max_motor_torque +} spherical_joint_get_motor_torque(id: JointId) -> Vec3 { base = sim_of(id, dynamics.JOINT_SPHERICAL) if base == null { return math.vec3_zero() } @@ -1884,28 +2126,44 @@ weld_joint_set_linear_hertz(id: JointId, value: float) { base = sim_of(id, dynamics.JOINT_WELD) if base != null { dynamics.weld_joint_of(base).linear_hertz = value } } -weld_joint_get_linear_hertz(id: JointId) -> float { return dynamics.weld_joint_of(sim_of(id, dynamics.JOINT_WELD)).linear_hertz } +weld_joint_get_linear_hertz(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_WELD) + if base == null { return 0.0 } + return dynamics.weld_joint_of(base).linear_hertz +} weld_joint_set_linear_damping_ratio(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WELD_JOINT_SET_LINEAR_DAMPING_RATIO); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_WELD) if base != null { dynamics.weld_joint_of(base).linear_damping_ratio = value } } -weld_joint_get_linear_damping_ratio(id: JointId) -> float { return dynamics.weld_joint_of(sim_of(id, dynamics.JOINT_WELD)).linear_damping_ratio } +weld_joint_get_linear_damping_ratio(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_WELD) + if base == null { return 0.0 } + return dynamics.weld_joint_of(base).linear_damping_ratio +} weld_joint_set_angular_hertz(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WELD_JOINT_SET_ANGULAR_HERTZ); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_WELD) if base != null { dynamics.weld_joint_of(base).angular_hertz = value } } -weld_joint_get_angular_hertz(id: JointId) -> float { return dynamics.weld_joint_of(sim_of(id, dynamics.JOINT_WELD)).angular_hertz } +weld_joint_get_angular_hertz(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_WELD) + if base == null { return 0.0 } + return dynamics.weld_joint_of(base).angular_hertz +} weld_joint_set_angular_damping_ratio(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WELD_JOINT_SET_ANGULAR_DAMPING_RATIO); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_WELD) if base != null { dynamics.weld_joint_of(base).angular_damping_ratio = value } } -weld_joint_get_angular_damping_ratio(id: JointId) -> float { return dynamics.weld_joint_of(sim_of(id, dynamics.JOINT_WELD)).angular_damping_ratio } +weld_joint_get_angular_damping_ratio(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_WELD) + if base == null { return 0.0 } + return dynamics.weld_joint_of(base).angular_damping_ratio +} // --- wheel joint ---------------------------------------------------------------------------------------------------- @@ -2241,21 +2499,33 @@ wheel_joint_enable_suspension(id: JointId, flag: bool) { j.suspension_spring_impulse = 0.0 } } -wheel_joint_is_suspension_enabled(id: JointId) -> bool { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).enable_suspension_spring } +wheel_joint_is_suspension_enabled(id: JointId) -> bool { + base = sim_of(id, dynamics.JOINT_WHEEL) + if base == null { return false } + return dynamics.wheel_joint_of(base).enable_suspension_spring +} wheel_joint_set_suspension_hertz(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_SET_SUSPENSION_HERTZ); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_WHEEL) if base != null { dynamics.wheel_joint_of(base).suspension_hertz = value } } -wheel_joint_get_suspension_hertz(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).suspension_hertz } +wheel_joint_get_suspension_hertz(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_WHEEL) + if base == null { return 0.0 } + return dynamics.wheel_joint_of(base).suspension_hertz +} wheel_joint_set_suspension_damping_ratio(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_SET_SUSPENSION_DAMPING_RATIO); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_WHEEL) if base != null { dynamics.wheel_joint_of(base).suspension_damping_ratio = value } } -wheel_joint_get_suspension_damping_ratio(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).suspension_damping_ratio } +wheel_joint_get_suspension_damping_ratio(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_WHEEL) + if base == null { return 0.0 } + return dynamics.wheel_joint_of(base).suspension_damping_ratio +} wheel_joint_enable_suspension_limit(id: JointId, flag: bool) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_ENABLE_SUSPENSION_LIMIT); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_WHEEL) @@ -2267,9 +2537,21 @@ wheel_joint_enable_suspension_limit(id: JointId, flag: bool) { j.enable_suspension_limit = flag } } -wheel_joint_is_suspension_limit_enabled(id: JointId) -> bool { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).enable_suspension_limit } -wheel_joint_get_lower_suspension_limit(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).lower_suspension_limit } -wheel_joint_get_upper_suspension_limit(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).upper_suspension_limit } +wheel_joint_is_suspension_limit_enabled(id: JointId) -> bool { + base = sim_of(id, dynamics.JOINT_WHEEL) + if base == null { return false } + return dynamics.wheel_joint_of(base).enable_suspension_limit +} +wheel_joint_get_lower_suspension_limit(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_WHEEL) + if base == null { return 0.0 } + return dynamics.wheel_joint_of(base).lower_suspension_limit +} +wheel_joint_get_upper_suspension_limit(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_WHEEL) + if base == null { return 0.0 } + return dynamics.wheel_joint_of(base).upper_suspension_limit +} wheel_joint_set_suspension_limits(id: JointId, lower: float, upper: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_SET_SUSPENSION_LIMITS); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, lower); dynamics.rec_float(recorder, upper); dynamics.rec_end(recorder) } if math.is_valid_float(lower) == false || math.is_valid_float(upper) == false { return } @@ -2293,21 +2575,33 @@ wheel_joint_enable_spin_motor(id: JointId, flag: bool) { j.enable_spin_motor = flag } } -wheel_joint_is_spin_motor_enabled(id: JointId) -> bool { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).enable_spin_motor } +wheel_joint_is_spin_motor_enabled(id: JointId) -> bool { + base = sim_of(id, dynamics.JOINT_WHEEL) + if base == null { return false } + return dynamics.wheel_joint_of(base).enable_spin_motor +} wheel_joint_set_spin_motor_speed(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_SET_SPIN_MOTOR_SPEED); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if math.is_valid_float(value) == false { return } base = sim_of(id, dynamics.JOINT_WHEEL) if base != null { dynamics.wheel_joint_of(base).spin_speed = value } } -wheel_joint_get_spin_motor_speed(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).spin_speed } +wheel_joint_get_spin_motor_speed(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_WHEEL) + if base == null { return 0.0 } + return dynamics.wheel_joint_of(base).spin_speed +} wheel_joint_set_max_spin_torque(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_SET_MAX_SPIN_TORQUE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_WHEEL) if base != null { dynamics.wheel_joint_of(base).max_spin_torque = value } } -wheel_joint_get_max_spin_torque(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).max_spin_torque } +wheel_joint_get_max_spin_torque(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_WHEEL) + if base == null { return 0.0 } + return dynamics.wheel_joint_of(base).max_spin_torque +} wheel_joint_enable_steering(id: JointId, flag: bool) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_ENABLE_STEERING); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_WHEEL) @@ -2323,28 +2617,44 @@ wheel_joint_enable_steering(id: JointId, flag: bool) { j.enable_steering = flag } } -wheel_joint_is_steering_enabled(id: JointId) -> bool { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).enable_steering } +wheel_joint_is_steering_enabled(id: JointId) -> bool { + base = sim_of(id, dynamics.JOINT_WHEEL) + if base == null { return false } + return dynamics.wheel_joint_of(base).enable_steering +} wheel_joint_set_steering_hertz(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_SET_STEERING_HERTZ); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_WHEEL) if base != null { dynamics.wheel_joint_of(base).steering_hertz = value } } -wheel_joint_get_steering_hertz(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).steering_hertz } +wheel_joint_get_steering_hertz(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_WHEEL) + if base == null { return 0.0 } + return dynamics.wheel_joint_of(base).steering_hertz +} wheel_joint_set_steering_damping_ratio(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_SET_STEERING_DAMPING_RATIO); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_WHEEL) if base != null { dynamics.wheel_joint_of(base).steering_damping_ratio = value } } -wheel_joint_get_steering_damping_ratio(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).steering_damping_ratio } +wheel_joint_get_steering_damping_ratio(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_WHEEL) + if base == null { return 0.0 } + return dynamics.wheel_joint_of(base).steering_damping_ratio +} wheel_joint_set_max_steering_torque(id: JointId, value: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_SET_MAX_STEERING_TORQUE); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, value); dynamics.rec_end(recorder) } if nonnegative(value) == false { return } base = sim_of(id, dynamics.JOINT_WHEEL) if base != null { dynamics.wheel_joint_of(base).max_steering_torque = value } } -wheel_joint_get_max_steering_torque(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).max_steering_torque } +wheel_joint_get_max_steering_torque(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_WHEEL) + if base == null { return 0.0 } + return dynamics.wheel_joint_of(base).max_steering_torque +} wheel_joint_enable_steering_limit(id: JointId, flag: bool) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_ENABLE_STEERING_LIMIT); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } base = sim_of(id, dynamics.JOINT_WHEEL) @@ -2356,9 +2666,21 @@ wheel_joint_enable_steering_limit(id: JointId, flag: bool) { j.enable_steering_limit = flag } } -wheel_joint_is_steering_limit_enabled(id: JointId) -> bool { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).enable_steering_limit } -wheel_joint_get_lower_steering_limit(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).lower_steering_limit } -wheel_joint_get_upper_steering_limit(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).upper_steering_limit } +wheel_joint_is_steering_limit_enabled(id: JointId) -> bool { + base = sim_of(id, dynamics.JOINT_WHEEL) + if base == null { return false } + return dynamics.wheel_joint_of(base).enable_steering_limit +} +wheel_joint_get_lower_steering_limit(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_WHEEL) + if base == null { return 0.0 } + return dynamics.wheel_joint_of(base).lower_steering_limit +} +wheel_joint_get_upper_steering_limit(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_WHEEL) + if base == null { return 0.0 } + return dynamics.wheel_joint_of(base).upper_steering_limit +} wheel_joint_set_steering_limits(id: JointId, lower: float, upper: float) { if dynamics.recording_on(id.world0) { recorder = dynamics.rec_begin(id.world0, dynamics.OP_WHEEL_JOINT_SET_STEERING_LIMITS); dynamics.rec_id(recorder, id.index1, id.generation); dynamics.rec_float(recorder, lower); dynamics.rec_float(recorder, upper); dynamics.rec_end(recorder) } if math.is_valid_float(lower) == false || math.is_valid_float(upper) == false { return } @@ -2381,7 +2703,11 @@ wheel_joint_set_target_steering_angle(id: JointId, value: float) { base = sim_of(id, dynamics.JOINT_WHEEL) if base != null { dynamics.wheel_joint_of(base).target_steering_angle = value } } -wheel_joint_get_target_steering_angle(id: JointId) -> float { return dynamics.wheel_joint_of(sim_of(id, dynamics.JOINT_WHEEL)).target_steering_angle } +wheel_joint_get_target_steering_angle(id: JointId) -> float { + base = sim_of(id, dynamics.JOINT_WHEEL) + if base == null { return 0.0 } + return dynamics.wheel_joint_of(base).target_steering_angle +} wheel_joint_get_spin_speed(id: JointId) -> float { base = sim_of(id, dynamics.JOINT_WHEEL) if base == null { return 0.0 } diff --git a/tests/test_joint_solver.ae b/tests/test_joint_solver.ae index 289a84a..e3e35e7 100644 --- a/tests/test_joint_solver.ae +++ b/tests/test_joint_solver.ae @@ -1,5 +1,6 @@ // aephysics.joint_solver: each kind's accessors as test_joint.c -// exercises them, then each kind solved by hand between a static +// exercises them, every accessor asked of another kind or of a stale +// id (a zero value, no crash), then each kind solved by hand between a static // ground and a unit cube (mass 1, inertia 1/6) with the step's numbers // (four sub-steps of 1/60, the constraint hertz clamped to 60): the // velocities a rigid joint removes and leaves, the springs' pull, the @@ -331,6 +332,139 @@ test_accessors() { dynamics.destroy_world(f.world_id) } +// --- another kind's accessors, and a stale id's (#181) ------------------------------------------------------------ + +zero_vec3(v: Vec3) -> bool { return v.x == 0.0 && v.y == 0.0 && v.z == 0.0 } +identity_quat(q: Quat) -> bool { return q.x == 0.0 && q.y == 0.0 && q.z == 0.0 && q.w == 1.0 } + +// Every getter of the kinds other than the motor joint, asked of `id`. +other_kinds_read_zero(id: JointId, what: string) { + ensure("${what}: distance_joint_get_length", joint_solver.distance_joint_get_length(id) == 0.0) + ensure("${what}: distance_joint_is_limit_enabled", joint_solver.distance_joint_is_limit_enabled(id) == false) + ensure("${what}: distance_joint_get_min_length", joint_solver.distance_joint_get_min_length(id) == 0.0) + ensure("${what}: distance_joint_get_max_length", joint_solver.distance_joint_get_max_length(id) == 0.0) + ensure("${what}: distance_joint_get_current_length", joint_solver.distance_joint_get_current_length(id) == 0.0) + ensure("${what}: distance_joint_is_spring_enabled", joint_solver.distance_joint_is_spring_enabled(id) == false) + ensure("${what}: distance_joint_get_lower_spring_force", joint_solver.distance_joint_get_lower_spring_force(id) == 0.0) + ensure("${what}: distance_joint_get_upper_spring_force", joint_solver.distance_joint_get_upper_spring_force(id) == 0.0) + ensure("${what}: distance_joint_get_spring_hertz", joint_solver.distance_joint_get_spring_hertz(id) == 0.0) + ensure("${what}: distance_joint_get_spring_damping_ratio", joint_solver.distance_joint_get_spring_damping_ratio(id) == 0.0) + ensure("${what}: distance_joint_is_motor_enabled", joint_solver.distance_joint_is_motor_enabled(id) == false) + ensure("${what}: distance_joint_get_motor_speed", joint_solver.distance_joint_get_motor_speed(id) == 0.0) + ensure("${what}: distance_joint_get_motor_force", joint_solver.distance_joint_get_motor_force(id) == 0.0) + ensure("${what}: distance_joint_get_max_motor_force", joint_solver.distance_joint_get_max_motor_force(id) == 0.0) + ensure("${what}: parallel_joint_get_spring_hertz", joint_solver.parallel_joint_get_spring_hertz(id) == 0.0) + ensure("${what}: parallel_joint_get_spring_damping_ratio", joint_solver.parallel_joint_get_spring_damping_ratio(id) == 0.0) + ensure("${what}: parallel_joint_get_max_torque", joint_solver.parallel_joint_get_max_torque(id) == 0.0) + ensure("${what}: prismatic_joint_is_spring_enabled", joint_solver.prismatic_joint_is_spring_enabled(id) == false) + ensure("${what}: prismatic_joint_get_spring_hertz", joint_solver.prismatic_joint_get_spring_hertz(id) == 0.0) + ensure("${what}: prismatic_joint_get_spring_damping_ratio", joint_solver.prismatic_joint_get_spring_damping_ratio(id) == 0.0) + ensure("${what}: prismatic_joint_get_target_translation", joint_solver.prismatic_joint_get_target_translation(id) == 0.0) + ensure("${what}: prismatic_joint_is_limit_enabled", joint_solver.prismatic_joint_is_limit_enabled(id) == false) + ensure("${what}: prismatic_joint_get_lower_limit", joint_solver.prismatic_joint_get_lower_limit(id) == 0.0) + ensure("${what}: prismatic_joint_get_upper_limit", joint_solver.prismatic_joint_get_upper_limit(id) == 0.0) + ensure("${what}: prismatic_joint_get_translation", joint_solver.prismatic_joint_get_translation(id) == 0.0) + ensure("${what}: prismatic_joint_is_motor_enabled", joint_solver.prismatic_joint_is_motor_enabled(id) == false) + ensure("${what}: prismatic_joint_get_motor_speed", joint_solver.prismatic_joint_get_motor_speed(id) == 0.0) + ensure("${what}: prismatic_joint_get_max_motor_force", joint_solver.prismatic_joint_get_max_motor_force(id) == 0.0) + ensure("${what}: prismatic_joint_get_motor_force", joint_solver.prismatic_joint_get_motor_force(id) == 0.0) + ensure("${what}: prismatic_joint_get_speed", joint_solver.prismatic_joint_get_speed(id) == 0.0) + ensure("${what}: revolute_joint_is_limit_enabled", joint_solver.revolute_joint_is_limit_enabled(id) == false) + ensure("${what}: revolute_joint_get_lower_limit", joint_solver.revolute_joint_get_lower_limit(id) == 0.0) + ensure("${what}: revolute_joint_get_upper_limit", joint_solver.revolute_joint_get_upper_limit(id) == 0.0) + ensure("${what}: revolute_joint_get_angle", joint_solver.revolute_joint_get_angle(id) == 0.0) + ensure("${what}: revolute_joint_is_spring_enabled", joint_solver.revolute_joint_is_spring_enabled(id) == false) + ensure("${what}: revolute_joint_get_target_angle", joint_solver.revolute_joint_get_target_angle(id) == 0.0) + ensure("${what}: revolute_joint_get_spring_hertz", joint_solver.revolute_joint_get_spring_hertz(id) == 0.0) + ensure("${what}: revolute_joint_get_spring_damping_ratio", joint_solver.revolute_joint_get_spring_damping_ratio(id) == 0.0) + ensure("${what}: revolute_joint_is_motor_enabled", joint_solver.revolute_joint_is_motor_enabled(id) == false) + ensure("${what}: revolute_joint_get_motor_speed", joint_solver.revolute_joint_get_motor_speed(id) == 0.0) + ensure("${what}: revolute_joint_get_max_motor_torque", joint_solver.revolute_joint_get_max_motor_torque(id) == 0.0) + ensure("${what}: revolute_joint_get_motor_torque", joint_solver.revolute_joint_get_motor_torque(id) == 0.0) + ensure("${what}: spherical_joint_is_cone_limit_enabled", joint_solver.spherical_joint_is_cone_limit_enabled(id) == false) + ensure("${what}: spherical_joint_get_cone_limit", joint_solver.spherical_joint_get_cone_limit(id) == 0.0) + ensure("${what}: spherical_joint_get_cone_angle", joint_solver.spherical_joint_get_cone_angle(id) == 0.0) + ensure("${what}: spherical_joint_get_cone_axis", zero_vec3(joint_solver.spherical_joint_get_cone_axis(id))) + ensure("${what}: spherical_joint_get_cone_minor_angle", joint_solver.spherical_joint_get_cone_minor_angle(id) == 0.0) + ensure("${what}: spherical_joint_get_cone_major_axis", zero_vec3(joint_solver.spherical_joint_get_cone_major_axis(id))) + ensure("${what}: spherical_joint_is_twist_limit_enabled", joint_solver.spherical_joint_is_twist_limit_enabled(id) == false) + ensure("${what}: spherical_joint_get_lower_twist_limit", joint_solver.spherical_joint_get_lower_twist_limit(id) == 0.0) + ensure("${what}: spherical_joint_get_upper_twist_limit", joint_solver.spherical_joint_get_upper_twist_limit(id) == 0.0) + ensure("${what}: spherical_joint_get_twist_angle", joint_solver.spherical_joint_get_twist_angle(id) == 0.0) + ensure("${what}: spherical_joint_is_spring_enabled", joint_solver.spherical_joint_is_spring_enabled(id) == false) + ensure("${what}: spherical_joint_get_target_rotation", identity_quat(joint_solver.spherical_joint_get_target_rotation(id))) + ensure("${what}: spherical_joint_get_spring_hertz", joint_solver.spherical_joint_get_spring_hertz(id) == 0.0) + ensure("${what}: spherical_joint_get_spring_damping_ratio", joint_solver.spherical_joint_get_spring_damping_ratio(id) == 0.0) + ensure("${what}: spherical_joint_is_motor_enabled", joint_solver.spherical_joint_is_motor_enabled(id) == false) + ensure("${what}: spherical_joint_get_motor_velocity", zero_vec3(joint_solver.spherical_joint_get_motor_velocity(id))) + ensure("${what}: spherical_joint_get_max_motor_torque", joint_solver.spherical_joint_get_max_motor_torque(id) == 0.0) + ensure("${what}: spherical_joint_get_motor_torque", zero_vec3(joint_solver.spherical_joint_get_motor_torque(id))) + ensure("${what}: weld_joint_get_linear_hertz", joint_solver.weld_joint_get_linear_hertz(id) == 0.0) + ensure("${what}: weld_joint_get_linear_damping_ratio", joint_solver.weld_joint_get_linear_damping_ratio(id) == 0.0) + ensure("${what}: weld_joint_get_angular_hertz", joint_solver.weld_joint_get_angular_hertz(id) == 0.0) + ensure("${what}: weld_joint_get_angular_damping_ratio", joint_solver.weld_joint_get_angular_damping_ratio(id) == 0.0) + ensure("${what}: wheel_joint_is_suspension_enabled", joint_solver.wheel_joint_is_suspension_enabled(id) == false) + ensure("${what}: wheel_joint_get_suspension_hertz", joint_solver.wheel_joint_get_suspension_hertz(id) == 0.0) + ensure("${what}: wheel_joint_get_suspension_damping_ratio", joint_solver.wheel_joint_get_suspension_damping_ratio(id) == 0.0) + ensure("${what}: wheel_joint_is_suspension_limit_enabled", joint_solver.wheel_joint_is_suspension_limit_enabled(id) == false) + ensure("${what}: wheel_joint_get_lower_suspension_limit", joint_solver.wheel_joint_get_lower_suspension_limit(id) == 0.0) + ensure("${what}: wheel_joint_get_upper_suspension_limit", joint_solver.wheel_joint_get_upper_suspension_limit(id) == 0.0) + ensure("${what}: wheel_joint_is_spin_motor_enabled", joint_solver.wheel_joint_is_spin_motor_enabled(id) == false) + ensure("${what}: wheel_joint_get_spin_motor_speed", joint_solver.wheel_joint_get_spin_motor_speed(id) == 0.0) + ensure("${what}: wheel_joint_get_max_spin_torque", joint_solver.wheel_joint_get_max_spin_torque(id) == 0.0) + ensure("${what}: wheel_joint_is_steering_enabled", joint_solver.wheel_joint_is_steering_enabled(id) == false) + ensure("${what}: wheel_joint_get_steering_hertz", joint_solver.wheel_joint_get_steering_hertz(id) == 0.0) + ensure("${what}: wheel_joint_get_steering_damping_ratio", joint_solver.wheel_joint_get_steering_damping_ratio(id) == 0.0) + ensure("${what}: wheel_joint_get_max_steering_torque", joint_solver.wheel_joint_get_max_steering_torque(id) == 0.0) + ensure("${what}: wheel_joint_is_steering_limit_enabled", joint_solver.wheel_joint_is_steering_limit_enabled(id) == false) + ensure("${what}: wheel_joint_get_lower_steering_limit", joint_solver.wheel_joint_get_lower_steering_limit(id) == 0.0) + ensure("${what}: wheel_joint_get_upper_steering_limit", joint_solver.wheel_joint_get_upper_steering_limit(id) == 0.0) + ensure("${what}: wheel_joint_get_target_steering_angle", joint_solver.wheel_joint_get_target_steering_angle(id) == 0.0) + ensure("${what}: wheel_joint_get_spin_speed", joint_solver.wheel_joint_get_spin_speed(id) == 0.0) + ensure("${what}: wheel_joint_get_spin_torque", joint_solver.wheel_joint_get_spin_torque(id) == 0.0) + ensure("${what}: wheel_joint_get_steering_angle", joint_solver.wheel_joint_get_steering_angle(id) == 0.0) + ensure("${what}: wheel_joint_get_steering_torque", joint_solver.wheel_joint_get_steering_torque(id) == 0.0) +} + +// The motor joint's getters, asked of `id`. +motor_reads_zero(id: JointId, what: string) { + ensure("${what}: motor_joint_get_linear_velocity", zero_vec3(joint_solver.motor_joint_get_linear_velocity(id))) + ensure("${what}: motor_joint_get_angular_velocity", zero_vec3(joint_solver.motor_joint_get_angular_velocity(id))) + ensure("${what}: motor_joint_get_max_velocity_torque", joint_solver.motor_joint_get_max_velocity_torque(id) == 0.0) + ensure("${what}: motor_joint_get_max_velocity_force", joint_solver.motor_joint_get_max_velocity_force(id) == 0.0) + ensure("${what}: motor_joint_get_linear_hertz", joint_solver.motor_joint_get_linear_hertz(id) == 0.0) + ensure("${what}: motor_joint_get_linear_damping_ratio", joint_solver.motor_joint_get_linear_damping_ratio(id) == 0.0) + ensure("${what}: motor_joint_get_angular_hertz", joint_solver.motor_joint_get_angular_hertz(id) == 0.0) + ensure("${what}: motor_joint_get_angular_damping_ratio", joint_solver.motor_joint_get_angular_damping_ratio(id) == 0.0) + ensure("${what}: motor_joint_get_max_spring_force", joint_solver.motor_joint_get_max_spring_force(id) == 0.0) + ensure("${what}: motor_joint_get_max_spring_torque", joint_solver.motor_joint_get_max_spring_torque(id) == 0.0) +} + +// Box3D asserts a joint's kind in its accessors (b3GetJointSimCheckType). +// Here an accessor of another kind, or of a stale id, is no crash: a getter +// gives its type's zero value and a setter does nothing. ae3d asked a +// parallel joint's maximum torque of a ragdoll's motor joint while the +// figure got up, and the read dereferenced a null sim. +test_wrong_kind_and_stale_ids() { + f = fixture() + mdef = dynamics.default_motor_joint_def() + common_frames(&f, &mdef.base) + mid = dynamics.create_motor_joint(f.world_id, &mdef) + joint_solver.motor_joint_set_max_velocity_force(mid, 500.0) + other_kinds_read_zero(mid, "another kind") + joint_solver.parallel_joint_set_max_torque(mid, 250.0) + joint_solver.spherical_joint_set_cone_ellipse(mid, 0.5, 0.25, math.vec3(1.0, 0.0, 0.0)) + joint_solver.distance_joint_set_length(mid, 3.0) + ensure("another kind's setters leave the motor joint as it was", joint_solver.motor_joint_get_max_velocity_force(mid) == 500.0) + dynamics.destroy_joint(mid, true) + other_kinds_read_zero(mid, "a stale id") + motor_reads_zero(mid, "a stale id") + joint_solver.motor_joint_set_max_velocity_force(mid, 500.0) + joint_solver.spherical_joint_set_cone_ellipse(mid, 0.5, 0.25, math.vec3(1.0, 0.0, 0.0)) + ensure("a stale id's setters create nothing", dynamics.world_joint_count(f.w) == 0 && dynamics.validate_joints(f.w)) + dynamics.destroy_world(f.world_id) +} + // --- the solves --------------------------------------------------------------------------------------------------- test_distance() { @@ -790,6 +924,7 @@ main() { dynamics.reserve_scratch() before = basics.alloc_count() test_accessors() + test_wrong_kind_and_stale_ids() test_distance() test_revolute() test_weld() From b465efaecf36afd16f7f5203bf10a433f8b7e46f Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Mon, 19 Oct 2026 23:03:45 -0300 Subject: [PATCH 2/7] Velocity integration: skip undamped divides, keep the inertia over a 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 --- aephysics/contact_solver/module.ae | 4 ++- aephysics/solver/module.ae | 54 +++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/aephysics/contact_solver/module.ae b/aephysics/contact_solver/module.ae index d97590f..b781495 100644 --- a/aephysics/contact_solver/module.ae +++ b/aephysics/contact_solver/module.ae @@ -62,6 +62,8 @@ struct StepContext { active_color_count: int enable_warm_starting: bool any_restitution: int // set by a prepare that met a contact with restitution + sub_step_index: int // the sub-step the stages are running, 0 first + body_inertias: ptr // float[6] per awake body: its local inertia, kept from sub-step 0; null to compute it each time } default_step_context(w: *World) -> StepContext { @@ -70,7 +72,7 @@ default_step_context(w: *World) -> StepContext { restitution_threshold: w.restitution_threshold, max_linear_velocity: w.max_linear_speed, world: w, states: null, sims: null, enlarged_shapes: null, enlarged_shape_count: 0, bullet_bodies: null, bullet_body_count: 0, awake_contact_indices: null, awake_contact_count: 0, active_color_count: 0, enable_warm_starting: w.enable_warm_starting, - any_restitution: 0 } + any_restitution: 0, sub_step_index: 0, body_inertias: null } } // One manifold point's constraint: the anchors relative to the centres diff --git a/aephysics/solver/module.ae b/aephysics/solver/module.ae index 80c9b1e..283f0c3 100644 --- a/aephysics/solver/module.ae +++ b/aephysics/solver/module.ae @@ -149,8 +149,12 @@ integrate_velocities_range(ctx: *StepContext, start: int, end: int) { state = state_at(ctx.states, i) v = math.widen3(state.v) wv = math.widen3(state.w) - linear_damping = 1.0 / (1.0 + h * sim.linear_damping) - angular_damping = 1.0 / (1.0 + h * sim.angular_damping) + // 1 / (1 + h c) is 1 exactly when c is 0 (or -0), as most bodies + // have it: the divide is skipped then, with the same bits. + linear_damping = 1.0 + if sim.linear_damping != 0.0 { linear_damping = 1.0 / (1.0 + h * sim.linear_damping) } + angular_damping = 1.0 + if sim.angular_damping != 0.0 { angular_damping = 1.0 / (1.0 + h * sim.angular_damping) } gravity_scale = 0.0 if sim.inv_mass > 0.0 { gravity_scale = sim.gravity_scale } linear_delta = math.blend2(h * sim.inv_mass, math.widen3(sim.force), h * gravity_scale, gravity) @@ -159,15 +163,42 @@ integrate_velocities_range(ctx: *StepContext, start: int, end: int) { wv = math.mul_add(angular_delta, angular_damping, wv) // Gyroscopic: I (w2 - w1) + h w2 x (I w2) = 0, solved for w2 in the body's frame. q = math.mul_quat(math.widen_quat(state.dq), sim.transform.q) - inertia = math.invert_matrix(math.widen_mat3(sim.inv_inertia_local)) omega1 = math.inv_rotate_vector(q, wv) omega2 = omega1 - i00 = inertia.cx.x - i01 = inertia.cy.x - i02 = inertia.cz.x - i11 = inertia.cy.y - i12 = inertia.cz.y - i22 = inertia.cz.z + // The local inertia does not change over a step: inverted on the + // first sub-step and kept for the rest, the same doubles (Box3D + // inverts it every sub-step, its own "todo wasteful computation"). + i00 = 0.0 + i01 = 0.0 + i02 = 0.0 + i11 = 0.0 + i12 = 0.0 + i22 = 0.0 + kept = ctx.body_inertias as float[] + if ctx.body_inertias != null && ctx.sub_step_index > 0 { + i00 = kept[i * 6] + i01 = kept[i * 6 + 1] + i02 = kept[i * 6 + 2] + i11 = kept[i * 6 + 3] + i12 = kept[i * 6 + 4] + i22 = kept[i * 6 + 5] + } else { + inertia = math.invert_matrix(math.widen_mat3(sim.inv_inertia_local)) + i00 = inertia.cx.x + i01 = inertia.cy.x + i02 = inertia.cz.x + i11 = inertia.cy.y + i12 = inertia.cz.y + i22 = inertia.cz.z + if ctx.body_inertias != null { + kept[i * 6] = i00 + kept[i * 6 + 1] = i01 + kept[i * 6 + 2] = i02 + kept[i * 6 + 3] = i11 + kept[i * 6 + 4] = i12 + kept[i * 6 + 5] = i22 + } + } iteration = 0 while iteration < GYROSCOPIC_ITERATIONS { w1 = omega2.x @@ -1012,6 +1043,7 @@ orchestrate(sc: *SolverContext) { sub = 0 while sub < ctx.sub_step_count { + ctx.sub_step_index = sub // before the stage starts the workers // The stage index restarts each sub-step; the sync indices keep growing. iteration_stage = stage_index execute_main_stage(stage_at(stages, iteration_stage), sc, sync_bits(body_sync, iteration_stage)) @@ -1110,6 +1142,8 @@ solve_step(w: *World, ctx: *StepContext) { stack = &w.stack ctx.bullet_body_count = 0 ctx.bullet_bodies = basics.stack_alloc(stack, awake_body_count * 4) + ctx.body_inertias = basics.stack_alloc(stack, awake_body_count * 6 * 8) + ctx.sub_step_index = 0 ctx.sims = awake.body_sims.data ctx.states = awake.body_states.data resize_move_events(w, awake_body_count) @@ -1323,6 +1357,8 @@ solve_step(w: *World, ctx: *StepContext) { basics.stack_free(stack, manifold_block) basics.stack_free(stack, contact_block) basics.stack_free(stack, wide_block) + basics.stack_free(stack, ctx.body_inertias) + ctx.body_inertias = null c = 0 while c < dynamics.GRAPH_COLOR_COUNT { color = dynamics.graph_color(w, c) From 5eb03e55fe13b5dc4c175cbd743eab5ad35e9c72 Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Mon, 19 Oct 2026 23:09:24 -0300 Subject: [PATCH 3/7] Wide contacts: the second tangent computed where it is used, not stored 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. --- aephysics/contact_solver_wide/module.ae | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/aephysics/contact_solver_wide/module.ae b/aephysics/contact_solver_wide/module.ae index 68c5b74..a7b451f 100644 --- a/aephysics/contact_solver_wide/module.ae +++ b/aephysics/contact_solver_wide/module.ae @@ -201,7 +201,6 @@ struct WideConstraint { inv_inertia_b: SymMatrix3W normal: Vec3W tangent1: Vec3W - tangent2: Vec3W center_a: Vec3W // the friction centres center_b: Vec3W twist_mass: f32x8 @@ -535,7 +534,6 @@ prepare_wide_slot(ctx: *StepContext, color: *GraphColor, slot: int, end: int) { tangent2 = cross_w(tangent1, normal) c.normal = normal c.tangent1 = tangent1 - c.tangent2 = tangent2 tangent_velocity = gather_vec3f(k0.tangent_velocity, k1.tangent_velocity, k2.tangent_velocity, k3.tangent_velocity, k4.tangent_velocity, k5.tangent_velocity, k6.tangent_velocity, k7.tangent_velocity) c.tangent_velocity1 = dot_w(tangent_velocity, tangent1) c.tangent_velocity2 = dot_w(tangent_velocity, tangent2) @@ -698,7 +696,8 @@ warm_start_wide_contacts(ctx: *StepContext, constraints: ptr, count: int) { j = j + 1 } normal = c.normal - friction = mul_add_sv_w(mul_sv_w(c.friction_impulse.x, c.tangent1), c.friction_impulse.y, c.tangent2) + tangent2 = cross_w(c.tangent1, normal) + friction = mul_add_sv_w(mul_sv_w(c.friction_impulse.x, c.tangent1), c.friction_impulse.y, tangent2) linear = mul_add_sv_w(friction, total_normal_impulse, normal) twist = mul_sv_w(c.twist_impulse, normal) angular_a = add_v_w(add_v_w(cross_w(moment_a, normal), cross_w(c.center_a, friction)), twist) @@ -814,7 +813,8 @@ solve_wide_contacts(ctx: *StepContext, constraints: ptr, count: int, use_bias: b vr_a = add_v_w(b_a.v, cross_w(b_a.w, c_a)) vr_b = add_v_w(b_b.v, cross_w(b_b.w, c_b)) vr = sub_v_w(vr_b, vr_a) - vt = Vec2W { x: sub_w(dot_w(vr, c.tangent1), c.tangent_velocity1), y: sub_w(dot_w(vr, c.tangent2), c.tangent_velocity2) } + tangent2 = cross_w(c.tangent1, c.normal) + vt = Vec2W { x: sub_w(dot_w(vr, c.tangent1), c.tangent_velocity1), y: sub_w(dot_w(vr, tangent2), c.tangent_velocity2) } tm = mul_mv2_w(c.tangent_mass, vt) new_friction = Vec2W { x: sub_w(c.friction_impulse.x, tm.x), y: sub_w(c.friction_impulse.y, tm.y) } max_friction = mul_w(c.friction, total_normal_impulse) @@ -825,7 +825,7 @@ solve_wide_contacts(ctx: *StepContext, constraints: ptr, count: int, use_bias: b new_friction = Vec2W { x: mul_w(scale, new_friction.x), y: mul_w(scale, new_friction.y) } delta_friction = Vec2W { x: sub_w(new_friction.x, c.friction_impulse.x), y: sub_w(new_friction.y, c.friction_impulse.y) } c.friction_impulse = new_friction - p = add_v_w(mul_sv_w(delta_friction.x, c.tangent1), mul_sv_w(delta_friction.y, c.tangent2)) + p = add_v_w(mul_sv_w(delta_friction.x, c.tangent1), mul_sv_w(delta_friction.y, tangent2)) b_a.w = mul_sub_mv_w(b_a.w, c.inv_inertia_a, cross_w(c_a, p)) b_a.v = mul_sub_sv_w(b_a.v, c.inv_mass_a, p) b_b.w = mul_add_mv_w(b_b.w, c.inv_inertia_b, cross_w(c_b, p)) @@ -909,10 +909,15 @@ store_wide_range(ctx: *StepContext, color: *GraphColor, start: int, end: int, hi w = ctx.world has_hits = false neg_threshold = 0.0 - w.hit_event_threshold + // The second tangent is not stored (Box3D's "todo test computing the + // tangents on the fly"): cross(tangent1, normal) again, once a slot, + // the bits prepare computed it with. + tangent2 = Vec3W { x: zero_w(), y: zero_w(), z: zero_w() } i = start while i < end { c = wide_constraint_at(color.wide_constraints, i / WIDTH) l = i % WIDTH + if l == 0 || i == start { tangent2 = cross_w(c.tangent1, c.normal) } // The next slot's manifolds, which nothing has touched since the // collide pass, fetched while this slot's are written. if l == 0 && i + WIDTH < end { @@ -929,9 +934,9 @@ store_wide_range(ctx: *StepContext, color: *GraphColor, start: int, end: int, hi m = ptr_lane(&c.manifolds, l) as *Manifold f1 = lane(c.friction_impulse.x, l) f2 = lane(c.friction_impulse.y, l) - m.friction_impulse = math.narrow3(math.vec3(f1 * lane(c.tangent1.x, l) + f2 * lane(c.tangent2.x, l), - f1 * lane(c.tangent1.y, l) + f2 * lane(c.tangent2.y, l), - f1 * lane(c.tangent1.z, l) + f2 * lane(c.tangent2.z, l))) + m.friction_impulse = math.narrow3(math.vec3(f1 * lane(c.tangent1.x, l) + f2 * lane(tangent2.x, l), + f1 * lane(c.tangent1.y, l) + f2 * lane(tangent2.y, l), + f1 * lane(c.tangent1.z, l) + f2 * lane(tangent2.z, l))) m.twist_impulse = lane(c.twist_impulse, l) m.rolling_impulse = Vec3f { x: lanes.lane8(c.rolling_impulse.x, l as long), y: lanes.lane8(c.rolling_impulse.y, l as long), z: lanes.lane8(c.rolling_impulse.z, l as long) } point_count = m.point_count From cd9bd1057d7524ea7b4da0864fe81ae1c9a27c28 Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Mon, 19 Oct 2026 23:41:35 -0300 Subject: [PATCH 4/7] Wide contacts: gather and scatter bodies through 8x8 transposes 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. --- aephysics/contact_solver_wide/module.ae | 108 ++++++++++++++++-------- 1 file changed, 72 insertions(+), 36 deletions(-) diff --git a/aephysics/contact_solver_wide/module.ae b/aephysics/contact_solver_wide/module.ae index a7b451f..c580666 100644 --- a/aephysics/contact_solver_wide/module.ae +++ b/aephysics/contact_solver_wide/module.ae @@ -265,49 +265,85 @@ state_or_identity(states: ptr, index1: int) -> *BodyState { return (states + (index1 - 1) * sizeof(BodyState)) as *BodyState } +// Eight rows of eight lanes, transposed: row k of the result is lane k of +// every input (b3TransposeW8). 24 register moves; no lane is read or +// written on its own. +struct Rows8 { + r0: f32x8 + r1: f32x8 + r2: f32x8 + r3: f32x8 + r4: f32x8 + r5: f32x8 + r6: f32x8 + r7: f32x8 +} + +transpose8(a0: f32x8, a1: f32x8, a2: f32x8, a3: f32x8, a4: f32x8, a5: f32x8, a6: f32x8, a7: f32x8) -> Rows8 { + t0 = lanes.unpacklo8(a0, a1) + t1 = lanes.unpackhi8(a0, a1) + t2 = lanes.unpacklo8(a2, a3) + t3 = lanes.unpackhi8(a2, a3) + t4 = lanes.unpacklo8(a4, a5) + t5 = lanes.unpackhi8(a4, a5) + t6 = lanes.unpacklo8(a6, a7) + t7 = lanes.unpackhi8(a6, a7) + u0 = lanes.pairs_lo8(t0, t2) + u1 = lanes.pairs_hi8(t0, t2) + u2 = lanes.pairs_lo8(t1, t3) + u3 = lanes.pairs_hi8(t1, t3) + u4 = lanes.pairs_lo8(t4, t6) + u5 = lanes.pairs_hi8(t4, t6) + u6 = lanes.pairs_lo8(t5, t7) + u7 = lanes.pairs_hi8(t5, t7) + return Rows8 { r0: lanes.halves_lo8(u0, u4), r1: lanes.halves_lo8(u1, u5), r2: lanes.halves_lo8(u2, u6), r3: lanes.halves_lo8(u3, u7), + r4: lanes.halves_hi8(u0, u4), r5: lanes.halves_hi8(u1, u5), r6: lanes.halves_hi8(u2, u6), r7: lanes.halves_hi8(u3, u7) } +} + +// Eight bodies' states in lanes, as Box3D's b3GatherBodies does it: a body +// state is two rows of eight floats, [v, pad, w, pad] and [dp, flags, dq], +// so each body is two loads and the lanes come out of two transposes. A +// static body or a spare lane reads the identity state. gather_bodies(states: ptr, indices: *IntW) -> BodyStateW { - s0 = state_or_identity(states, indices.l0) - s1 = state_or_identity(states, indices.l1) - s2 = state_or_identity(states, indices.l2) - s3 = state_or_identity(states, indices.l3) - s4 = state_or_identity(states, indices.l4) - s5 = state_or_identity(states, indices.l5) - s6 = state_or_identity(states, indices.l6) - s7 = state_or_identity(states, indices.l7) - v = Vec3W { x: lanes.f32x8(s0.v.x, s1.v.x, s2.v.x, s3.v.x, s4.v.x, s5.v.x, s6.v.x, s7.v.x), - y: lanes.f32x8(s0.v.y, s1.v.y, s2.v.y, s3.v.y, s4.v.y, s5.v.y, s6.v.y, s7.v.y), - z: lanes.f32x8(s0.v.z, s1.v.z, s2.v.z, s3.v.z, s4.v.z, s5.v.z, s6.v.z, s7.v.z) } - w = Vec3W { x: lanes.f32x8(s0.w.x, s1.w.x, s2.w.x, s3.w.x, s4.w.x, s5.w.x, s6.w.x, s7.w.x), - y: lanes.f32x8(s0.w.y, s1.w.y, s2.w.y, s3.w.y, s4.w.y, s5.w.y, s6.w.y, s7.w.y), - z: lanes.f32x8(s0.w.z, s1.w.z, s2.w.z, s3.w.z, s4.w.z, s5.w.z, s6.w.z, s7.w.z) } - dp = Vec3W { x: lanes.f32x8(s0.dp.x, s1.dp.x, s2.dp.x, s3.dp.x, s4.dp.x, s5.dp.x, s6.dp.x, s7.dp.x), - y: lanes.f32x8(s0.dp.y, s1.dp.y, s2.dp.y, s3.dp.y, s4.dp.y, s5.dp.y, s6.dp.y, s7.dp.y), - z: lanes.f32x8(s0.dp.z, s1.dp.z, s2.dp.z, s3.dp.z, s4.dp.z, s5.dp.z, s6.dp.z, s7.dp.z) } - dqv = Vec3W { x: lanes.f32x8(s0.dq.x, s1.dq.x, s2.dq.x, s3.dq.x, s4.dq.x, s5.dq.x, s6.dq.x, s7.dq.x), - y: lanes.f32x8(s0.dq.y, s1.dq.y, s2.dq.y, s3.dq.y, s4.dq.y, s5.dq.y, s6.dq.y, s7.dq.y), - z: lanes.f32x8(s0.dq.z, s1.dq.z, s2.dq.z, s3.dq.z, s4.dq.z, s5.dq.z, s6.dq.z, s7.dq.z) } - dqs = lanes.f32x8(s0.dq.w, s1.dq.w, s2.dq.w, s3.dq.w, s4.dq.w, s5.dq.w, s6.dq.w, s7.dq.w) - return BodyStateW { v: v, w: w, dp: dp, dq: QuatW { v: dqv, s: dqs } } -} - -// The velocities back to the dynamic bodies of the lanes. -scatter_lane(states: ptr, index1: int, body: *BodyStateW, l: int) { + p0 = state_or_identity(states, indices.l0) as ptr + p1 = state_or_identity(states, indices.l1) as ptr + p2 = state_or_identity(states, indices.l2) as ptr + p3 = state_or_identity(states, indices.l3) as ptr + p4 = state_or_identity(states, indices.l4) as ptr + p5 = state_or_identity(states, indices.l5) as ptr + p6 = state_or_identity(states, indices.l6) as ptr + p7 = state_or_identity(states, indices.l7) as ptr + a = transpose8(lanes.load8(p0, 0), lanes.load8(p1, 0), lanes.load8(p2, 0), lanes.load8(p3, 0), + lanes.load8(p4, 0), lanes.load8(p5, 0), lanes.load8(p6, 0), lanes.load8(p7, 0)) + b = transpose8(lanes.load8(p0, 8), lanes.load8(p1, 8), lanes.load8(p2, 8), lanes.load8(p3, 8), + lanes.load8(p4, 8), lanes.load8(p5, 8), lanes.load8(p6, 8), lanes.load8(p7, 8)) + return BodyStateW { v: Vec3W { x: a.r0, y: a.r1, z: a.r2 }, w: Vec3W { x: a.r4, y: a.r5, z: a.r6 }, + dp: Vec3W { x: b.r0, y: b.r1, z: b.r2 }, dq: QuatW { v: Vec3W { x: b.r4, y: b.r5, z: b.r6 }, s: b.r7 } } +} + +// A lane's velocities back to its body's first row, [v, 0, w, 0], when the +// body is dynamic (b3StoreBodyVelocity). The pads are written 0; nothing +// reads them. +store_velocity(states: ptr, index1: int, row: f32x8) { if index1 == 0 { return } state = (states + (index1 - 1) * sizeof(BodyState)) as *BodyState if (state.flags & dynamics.DYNAMIC_FLAG) == 0 { return } - state.v = Vec3f { x: lanes.lane8(body.v.x, l as long), y: lanes.lane8(body.v.y, l as long), z: lanes.lane8(body.v.z, l as long) } - state.w = Vec3f { x: lanes.lane8(body.w.x, l as long), y: lanes.lane8(body.w.y, l as long), z: lanes.lane8(body.w.z, l as long) } + lanes.store8(state as ptr, 0, row) } +// The velocities back to the dynamic bodies of the lanes: one transpose +// turns the lanes into the eight bodies' rows (b3ScatterBodies). scatter_bodies(states: ptr, indices: *IntW, body: *BodyStateW) { - scatter_lane(states, indices.l0, body, 0) - scatter_lane(states, indices.l1, body, 1) - scatter_lane(states, indices.l2, body, 2) - scatter_lane(states, indices.l3, body, 3) - scatter_lane(states, indices.l4, body, 4) - scatter_lane(states, indices.l5, body, 5) - scatter_lane(states, indices.l6, body, 6) - scatter_lane(states, indices.l7, body, 7) + zero = zero_w() + rows = transpose8(body.v.x, body.v.y, body.v.z, zero, body.w.x, body.w.y, body.w.z, zero) + store_velocity(states, indices.l0, rows.r0) + store_velocity(states, indices.l1, rows.r1) + store_velocity(states, indices.l2, rows.r2) + store_velocity(states, indices.l3, rows.r3) + store_velocity(states, indices.l4, rows.r4) + store_velocity(states, indices.l5, rows.r5) + store_velocity(states, indices.l6, rows.r6) + store_velocity(states, indices.l7, rows.r7) } // --- the stages ---------------------------------------------------------------------------------------------- From 9771960b45b367646e4ffe30efb7279f40fa8a76 Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Tue, 20 Oct 2026 11:16:24 -0300 Subject: [PATCH 5/7] Aether 0.835.0 and aeb v0.328, the latest releases 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. --- .github/workflows/test.yaml | 13 ++-- README.md | 9 ++- aephysics/basics/module.ae | 2 +- aephysics/compound/module.ae | 4 +- aephysics/dynamics/module.ae | 62 +++++++-------- aephysics/files/module.ae | 4 +- aephysics/manifold/module.ae | 106 +++++++++++++------------- aephysics/mesh/module.ae | 2 +- aephysics/mesh_contact/module.ae | 6 +- aephysics/physics_world/module.ae | 38 ++++----- aephysics/recording/module.ae | 8 +- aephysics/shape/module.ae | 6 +- aephysics/snapshot/module.ae | 10 +-- aephysics/solver/module.ae | 18 ++--- aephysics/triangle_manifold/module.ae | 38 ++++----- bench/broad_phase.ae | 6 +- bench/manifold.ae | 2 +- bench/mesh_contact.ae | 4 +- bench/shape.ae | 2 +- bench/suite.ae | 8 +- bench/triangle.ae | 2 +- tests/test_basics.ae | 8 +- tests/test_broad_phase.ae | 42 +++++----- tests/test_collision.ae | 6 +- tests/test_compound.ae | 8 +- tests/test_dynamics.ae | 8 +- tests/test_edge_cases.ae | 2 +- tests/test_height_field.ae | 20 ++--- tests/test_host_api.ae | 4 +- tests/test_hull.ae | 10 +-- tests/test_manifold.ae | 88 ++++++++++----------- tests/test_mesh_contact.ae | 30 ++++---- tests/test_mover.ae | 4 +- tests/test_mover_world.ae | 2 +- tests/test_physics_world.ae | 2 +- tests/test_shape.ae | 4 +- tests/test_triangle_manifold.ae | 24 +++--- tests/test_world_queries.ae | 2 +- 38 files changed, 307 insertions(+), 307 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 965a827..768f770 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -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 @@ -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 diff --git a/README.md b/README.md index 090402a..79da5ba 100644 --- a/README.md +++ b/README.md @@ -81,14 +81,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 diff --git a/aephysics/basics/module.ae b/aephysics/basics/module.ae index e199fef..ed76e41 100644 --- a/aephysics/basics/module.ae +++ b/aephysics/basics/module.ae @@ -594,7 +594,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. diff --git a/aephysics/compound/module.ae b/aephysics/compound/module.ae index 10e0ac5..59455d0 100644 --- a/aephysics/compound/module.ae +++ b/aephysics/compound/module.ae @@ -772,7 +772,7 @@ compound_sections_fit(c: *CompoundData) -> bool { offset = hulls[i].hull_offset if hulls[i].material_index < 0 || hulls[i].material_index >= c.material_count { return false } if offset < header || offset > n - sizeof(HullData) { return false } - if hull.hull_bytes_valid((block + offset) as *HullData, n - offset) == false { return false } + if hull.bytes_valid((block + offset) as *HullData, n - offset) == false { return false } i = i + 1 } meshes = (block + c.mesh_offset) as MeshInstance[] @@ -782,7 +782,7 @@ compound_sections_fit(c: *CompoundData) -> bool { if meshes[i].material_count < 1 || meshes[i].material_start < 0 { return false } if meshes[i].material_count > c.material_map_count - meshes[i].material_start { return false } if offset < header || offset > n - sizeof(MeshData) { return false } - if mesh.mesh_bytes_valid((block + offset) as *MeshData, n - offset) == false { return false } + if mesh.bytes_valid((block + offset) as *MeshData, n - offset) == false { return false } i = i + 1 } spheres = (block + c.sphere_offset) as CompoundSphere[] diff --git a/aephysics/dynamics/module.ae b/aephysics/dynamics/module.ae index 34d9b68..bbbcf53 100644 --- a/aephysics/dynamics/module.ae +++ b/aephysics/dynamics/module.ae @@ -1545,8 +1545,8 @@ get_max_world_count() -> int { return g_max_world_count } // world's def from it to grow nothing while stepping. track_capacity(w: *World) { c = &w.max_capacity - c.static_shape_count = math.max_int(c.static_shape_count, dynamic_tree.tree_get_proxy_count(broad_phase.broad_phase_tree(&w.broad_phase, BODY_STATIC))) - c.dynamic_shape_count = math.max_int(c.dynamic_shape_count, dynamic_tree.tree_get_proxy_count(broad_phase.broad_phase_tree(&w.broad_phase, BODY_DYNAMIC))) + c.static_shape_count = math.max_int(c.static_shape_count, dynamic_tree.tree_get_proxy_count(broad_phase.tree(&w.broad_phase, BODY_STATIC))) + c.dynamic_shape_count = math.max_int(c.dynamic_shape_count, dynamic_tree.tree_get_proxy_count(broad_phase.tree(&w.broad_phase, BODY_DYNAMIC))) static_bodies = solver_set_at(w, STATIC_SET).body_sims.count c.static_body_count = math.max_int(c.static_body_count, static_bodies) c.dynamic_body_count = math.max_int(c.dynamic_body_count, world_body_count(w) - static_bodies) @@ -1712,7 +1712,7 @@ free_world_contents(w: *World) { c = contact_at(w, i) if c.contact_id != NULL_INDEX { free_manifolds(w, c) - if (c.flags & SIM_MESH_CONTACT) != 0 { mesh_contact.mesh_contact_cache_destroy(mesh_cache_of(c)) } + if (c.flags & SIM_MESH_CONTACT) != 0 { mesh_contact.cache_destroy(mesh_cache_of(c)) } } i = i + 1 } @@ -2147,7 +2147,7 @@ destroy_body(id: BodyId) { release_debug_shape(w, s) if s.sensor_index != NULL_INDEX { destroy_sensor(w, s) } if s.proxy_key != NULL_INDEX { - broad_phase.broad_phase_destroy_proxy(&w.broad_phase, s.proxy_key) + broad_phase.destroy_proxy(&w.broad_phase, s.proxy_key) s.proxy_key = NULL_INDEX } if s.materials != null { @@ -2192,7 +2192,7 @@ compute_shape_margin(g: *Shape) -> float { if kind == KIND_SPHERE { margin = g.sphere.radius } else if kind == KIND_CAPSULE { margin = 0.5 * math.distance(g.capsule.center2, g.capsule.center1) + g.capsule.radius } else if kind == KIND_HULL { - points = hull.hull_points(g.hull) + points = hull.points(g.hull) max_extent_sqr = 0.0 i = 0 while i < g.hull.vertex_count { @@ -2219,7 +2219,7 @@ update_shape_aabbs(w: *World, s: *WorldShape, t: Transform, proxy_kind: int) { create_shape_proxy(w: *World, s: *WorldShape, kind: int, t: Transform, force_pair_creation: bool) { update_shape_aabbs(w, s, t, kind) - s.proxy_key = broad_phase.broad_phase_create_proxy(&w.broad_phase, kind, shape_fat_aabb(w, s.id), s.filter.category_bits, s.id, force_pair_creation) + s.proxy_key = broad_phase.create_proxy(&w.broad_phase, kind, shape_fat_aabb(w, s.id), s.filter.category_bits, s.id, force_pair_creation) } // A shape on a body: its slot, its geometry, its materials, its proxy in @@ -2687,7 +2687,7 @@ rec_string(r: *Recording, s: string) { } n = string.length(s) rec_int(r, n) - rec_raw(r, string.aether_string_raw_ptr(s), n) + rec_raw(r, (string.bytes(s) as ptr), n) } // A shape def's materials: their count, then each one, or -1 for none. @@ -2857,7 +2857,7 @@ query_tag_key(id: long, name: string) -> long { i = i + 1 } if name != null { - bytes = string.aether_string_raw_ptr(name) + bytes = (string.bytes(name) as ptr) n = string.length(name) i = 0 while i < n { @@ -2881,7 +2881,7 @@ intern_tag(r: *Recording, key: long, id: long, name: string) { if name != null && string.length(name) > 0 { tag.length = string.length(name) tag.name = basics.alloc(tag.length + 1) - memcpy(tag.name, string.aether_string_raw_ptr(name), tag.length) + memcpy(tag.name, (string.bytes(name) as ptr), tag.length) } basics.map_set(&r.tag_heads, key, index) } @@ -3455,7 +3455,7 @@ destroy_shape(id: ShapeId, update_body_mass: bool) { release_debug_shape(w, s) if s.sensor_index != NULL_INDEX { destroy_sensor(w, s) } if s.proxy_key != NULL_INDEX { - broad_phase.broad_phase_destroy_proxy(&w.broad_phase, s.proxy_key) + broad_phase.destroy_proxy(&w.broad_phase, s.proxy_key) s.proxy_key = NULL_INDEX } if s.materials != null { @@ -3521,10 +3521,10 @@ reset_shape_proxy(w: *World, s: *WorldShape, wake_bodies: bool, remake: bool) { kind = broad_phase.proxy_type(s.proxy_key) update_shape_aabbs(w, s, t, kind) if remake { - broad_phase.broad_phase_destroy_proxy(&w.broad_phase, s.proxy_key) - s.proxy_key = broad_phase.broad_phase_create_proxy(&w.broad_phase, kind, shape_fat_aabb(w, s.id), s.filter.category_bits, s.id, true) + broad_phase.destroy_proxy(&w.broad_phase, s.proxy_key) + s.proxy_key = broad_phase.create_proxy(&w.broad_phase, kind, shape_fat_aabb(w, s.id), s.filter.category_bits, s.id, true) } else { - broad_phase.broad_phase_move_proxy(&w.broad_phase, s.proxy_key, shape_fat_aabb(w, s.id)) + broad_phase.move_proxy(&w.broad_phase, s.proxy_key, shape_fat_aabb(w, s.id)) } } else { // A disabled body's shape: its bounds kept current for when it returns. @@ -3721,10 +3721,10 @@ shape_apply_wind(id: ShapeId, wind: Vec3, drag: float, lift: float, max_speed: f } } else { h = s.geometry.hull - points = hull.hull_points(h) - edges = hull.hull_edges(h) - planes = hull.hull_planes(h) - faces = hull.hull_faces(h) + points = hull.points(h) + edges = hull.edges(h) + planes = hull.planes(h) + faces = hull.faces(h) m = math.make_matrix_from_quat(q) f = 0 while f < h.face_count { @@ -3988,7 +3988,7 @@ body_set_transform(id: BodyId, position: Vec3, rotation: Quat) { if math.aabbf_holds(old_fat, s.aabb) { set_shape_fat_aabb(w, s.id, old_fat) } else if s.proxy_key != NULL_INDEX { - broad_phase.broad_phase_move_proxy(&w.broad_phase, s.proxy_key, shape_fat_aabb(w, s.id)) + broad_phase.move_proxy(&w.broad_phase, s.proxy_key, shape_fat_aabb(w, s.id)) } shape_id = s.next_shape_id } @@ -4811,7 +4811,7 @@ create_contact(w: *World, shape_a: *WorldShape, shape_b: *WorldShape, child_inde c.flags = flags if (flags & SIM_MESH_CONTACT) != 0 { mesh_caches = (&c.caches) as MeshContactCache[] - mesh_caches[0] = mesh_contact.mesh_contact_cache_create() + mesh_caches[0] = mesh_contact.cache_create() } else { convex_caches = (&c.caches) as TriangleCache[] convex_caches[0] = TriangleCache { triangle_index: NULL_INDEX, simplex_cache: distance.empty_cache(), sat_cache: manifold.empty_sat_cache() } @@ -4919,7 +4919,7 @@ destroy_contact(w: *World, c: *Contact, wake_bodies: bool) { if c.edge_b.next_key != NULL_INDEX { set_contact_edge_prev(w, c.edge_b.next_key, c.edge_b.prev_key) } if body_b.head_contact_key == ((contact_id << 1) | 1) { body_b.head_contact_key = c.edge_b.next_key } body_b.contact_count = body_b.contact_count - 1 - if (flags & SIM_MESH_CONTACT) != 0 { mesh_contact.mesh_contact_cache_destroy(mesh_cache_of(c)) } + if (flags & SIM_MESH_CONTACT) != 0 { mesh_contact.cache_destroy(mesh_cache_of(c)) } if c.island_id != NULL_INDEX { unlink_contact(w, c) } if c.color_index != NULL_INDEX { remove_contact_from_graph(w, body_id_a, body_id_b, c.color_index, c.local_index, (flags & SIM_MESH_CONTACT) != 0) @@ -5055,7 +5055,7 @@ compute_convex_manifold(w: *World, c: *Contact, geometry_a: *Shape, xf_a: Transf offset = math.sub(xf_a.p, xf_b.p) i = 0 while i < m.point_count { - source = manifold.manifold_point(&m, i) + source = manifold.point(&m, i) p = manifold_point_at(target, i) anchor_a = math.mul_mv(matrix_a, source.point) p.anchor_a = math.narrow3(anchor_a) @@ -5259,14 +5259,14 @@ update_mesh_contact(w: *World, c: *Contact, mesh_geometry: *Shape, mesh_shape: * triangle_index = manifold_point_at(m, j).triangle_index material_index = 0 if mesh_geometry.kind == KIND_MESH { - indices = mesh.mesh_material_indices(mesh_geometry.mesh.data) + indices = mesh.material_indices(mesh_geometry.mesh.data) material_index = indices[triangle_index] if material_map != null { map = material_map as int[] material_index = map[math.clamp_int(material_index, 0, material_map_count - 1)] } } else { - cell_materials = height_field.height_field_material_indices(mesh_geometry.height_field) + cell_materials = height_field.material_indices(mesh_geometry.height_field) material_index = cell_materials[triangle_index >> 1] as int } material_index = math.clamp_int(material_index, 0, mesh_shape.material_count - 1) @@ -7333,7 +7333,7 @@ joint_set_collide_connected(id: JointId, should_collide: bool) { if body_a.shape_count < body_b.shape_count { shape_id = body_a.head_shape_id } while shape_id != NULL_INDEX { s = world_shape_at(w, shape_id) - if s.proxy_key != NULL_INDEX { broad_phase.broad_phase_mark_proxy_moved_serial(&w.broad_phase, s.proxy_key) } + if s.proxy_key != NULL_INDEX { broad_phase.mark_proxy_moved_serial(&w.broad_phase, s.proxy_key) } shape_id = s.next_shape_id } } else { @@ -7757,7 +7757,7 @@ body_set_type(id: BodyId, kind: int) { while shape_id != NULL_INDEX { s = world_shape_at(w, shape_id) shape_id = s.next_shape_id - if s.proxy_key != NULL_INDEX { broad_phase.broad_phase_destroy_proxy(&w.broad_phase, s.proxy_key) } + if s.proxy_key != NULL_INDEX { broad_phase.destroy_proxy(&w.broad_phase, s.proxy_key) } create_shape_proxy(w, s, kind, t, true) } key = body.head_joint_key @@ -7804,7 +7804,7 @@ body_disable(id: BodyId) { s = world_shape_at(w, shape_id) shape_id = s.next_shape_id if s.proxy_key != NULL_INDEX { - broad_phase.broad_phase_destroy_proxy(&w.broad_phase, s.proxy_key) + broad_phase.destroy_proxy(&w.broad_phase, s.proxy_key) s.proxy_key = NULL_INDEX } } @@ -8179,9 +8179,9 @@ overlap_sensors(w: *World) { qc = SensorQueryContext { w: w, sensor: sensor, sensor_shape: sensor_shape, transform: get_body_sim(w, body).transform } bounds = sensor_shape.aabb mask = sensor_shape.filter.mask_bits - dynamic_tree.tree_query(broad_phase.broad_phase_tree(&w.broad_phase, BODY_STATIC), bounds, mask, false, sensor_query_visitor, (&qc) as ptr) - dynamic_tree.tree_query(broad_phase.broad_phase_tree(&w.broad_phase, BODY_KINEMATIC), bounds, mask, false, sensor_query_visitor, (&qc) as ptr) - dynamic_tree.tree_query(broad_phase.broad_phase_tree(&w.broad_phase, BODY_DYNAMIC), bounds, mask, false, sensor_query_visitor, (&qc) as ptr) + dynamic_tree.tree_query(broad_phase.tree(&w.broad_phase, BODY_STATIC), bounds, mask, false, sensor_query_visitor, (&qc) as ptr) + dynamic_tree.tree_query(broad_phase.tree(&w.broad_phase, BODY_KINEMATIC), bounds, mask, false, sensor_query_visitor, (&qc) as ptr) + dynamic_tree.tree_query(broad_phase.tree(&w.broad_phase, BODY_DYNAMIC), bounds, mask, false, sensor_query_visitor, (&qc) as ptr) overlaps = sensor.overlaps2.data as long[] broad_phase.sort_longs(overlaps, sensor.overlaps2.count) unique = 0 @@ -8295,14 +8295,14 @@ shape_user_material_id(s: *WorldShape, child_index: int, triangle_index: int) -> material_index = 0 kind = s.geometry.kind if kind == KIND_MESH { - indices = mesh.mesh_material_indices(s.geometry.mesh.data) + indices = mesh.material_indices(s.geometry.mesh.data) material_index = indices[triangle_index] } else if kind == KIND_HEIGHT_FIELD { material_index = height_field.get_height_field_material(s.geometry.height_field, triangle_index) } else if kind == KIND_COMPOUND { child = compound.get_compound_child(s.geometry.compound, child_index) if child.kind == KIND_MESH { - indices = mesh.mesh_material_indices(child.mesh.data) + indices = mesh.material_indices(child.mesh.data) material_index = compound.child_material(child, indices[triangle_index]) } else { material_index = child.material_index0 diff --git a/aephysics/files/module.ae b/aephysics/files/module.ae index ba67a65..1f9024b 100644 --- a/aephysics/files/module.ae +++ b/aephysics/files/module.ae @@ -49,7 +49,7 @@ read_aligned(path: string) -> (ptr, int) { return null, 0 } block = basics.alloc(length) - memcpy(block, string.aether_string_raw_ptr(data), length) + memcpy(block, (string.bytes(data) as ptr), length) string.free(data) return block, length } @@ -170,7 +170,7 @@ load_height_field(path: string) -> *HeightFieldData { if string.length(err) > 0 { return null } // The reader takes the text over and frees it when it goes. r = Reader { text: text, length: length, at: 0, ok: true } - def = height_field.height_field_def() + def = height_field.def() def.count_x = read_int(&r) def.count_z = read_int(&r) def.scale.x = read_float(&r) diff --git a/aephysics/manifold/module.ae b/aephysics/manifold/module.ae index bafe691..f217e4d 100644 --- a/aephysics/manifold/module.ae +++ b/aephysics/manifold/module.ae @@ -343,10 +343,10 @@ const EDGE_SCRATCH_BYTES = AXIS_FIELDS * AXIS_SLOTS * 4 + B_FRAME_SLOTS * 24 // normal, then the more anti-parallel of that edge's two faces. Extended // from the plain search so a wedge gets the right incident face. find_incident_face(h: *HullData, ref_normal: Vec3, vertex_index: int) -> int { - vertices = hull.hull_vertices(h) - edges = hull.hull_edges(h) - planes = hull.hull_planes(h) - points = hull.hull_points(h) + vertices = hull.vertices(h) + edges = hull.edges(h) + planes = hull.planes(h) + points = hull.points(h) min_edge = NULL_INDEX min_projection = math.MAX_FLOAT start = vertices[vertex_index] @@ -454,10 +454,10 @@ clip_segment(segment: ClipVertex[], plane: Plane) -> int { // The segment against the side planes of a hull face: 2 if it survives, else 0. clip_segment_to_hull_face(segment: ClipVertex[], h: *HullData, ref_face: int) -> int { - faces = hull.hull_faces(h) - planes = hull.hull_planes(h) - edges = hull.hull_edges(h) - points = hull.hull_points(h) + faces = hull.faces(h) + planes = hull.planes(h) + edges = hull.edges(h) + points = hull.points(h) ref_plane = planes[ref_face] start = faces[ref_face] edge_index = start @@ -646,7 +646,7 @@ collide_hull_and_sphere(m: *LocalManifold, capacity: int, hull_a: *HullData, sph m.point_count = 0 if capacity == 0 { return } center = math.transform_point(transform_b_to_a, sphere_b.center) - input = DistanceInput { proxy_a: hull.hull_proxy(hull_a), proxy_b: distance.shape_proxy((¢er) as ptr, 1, 0.0), + input = DistanceInput { proxy_a: hull.proxy(hull_a), proxy_b: distance.shape_proxy((¢er) as ptr, 1, 0.0), transform: math.transform_identity(), use_radii: false } radius_a = 0.0 radius_b = sphere_b.radius @@ -666,7 +666,7 @@ collide_hull_and_sphere(m: *LocalManifold, capacity: int, hull_a: *HullData, sph return } // Deep: the face the centre is least below. - planes = hull.hull_planes(hull_a) + planes = hull.planes(hull_a) best_index = NULL_INDEX best_distance = 0.0 - math.MAX_FLOAT i = 0 @@ -753,7 +753,7 @@ collide_capsules(m: *LocalManifold, capacity: int, capsule_a: Capsule, capsule_b // The hull face the capsule is least below, and the capsule end that is. query_face_direction_hull_and_capsule(h: *HullData, c: Capsule, capsule_transform: Transform) -> SeparatingAxis { - planes = hull.hull_planes(h) + planes = hull.planes(h) p1 = math.transform_point(capsule_transform, c.center1) p2 = math.transform_point(capsule_transform, c.center2) max_face = NULL_INDEX @@ -789,9 +789,9 @@ query_edge_direction_hull_and_capsule(h: *HullData, c: Capsule, capsule_transfor p_a = math.transform_point(capsule_transform, c.center1) q_a = math.transform_point(capsule_transform, c.center2) e_a = math.sub(q_a, p_a) - edges = hull.hull_edges(h) - points = hull.hull_points(h) - planes = hull.hull_planes(h) + edges = hull.edges(h) + points = hull.points(h) + planes = hull.planes(h) squared_tolerance = PARALLEL_EDGE_TOL * PARALLEL_EDGE_TOL index = 0 while index < h.edge_count { @@ -824,7 +824,7 @@ query_edge_direction_hull_and_capsule(h: *HullData, c: Capsule, capsule_transfor build_hull_face_and_capsule_contact(m: *LocalManifold, hull_a: *HullData, capsule_b: Capsule, transform_b_to_a: Transform, query: SeparatingAxis) -> bool { reach = speculative_distance() - planes = hull.hull_planes(hull_a) + planes = hull.planes(hull_a) ref_face = query.index_a ref_plane = planes[ref_face] scratch_ready() @@ -856,8 +856,8 @@ build_hull_and_capsule_edge_contact(m: *LocalManifold, capacity: int, hull_a: *H p_c = math.transform_point(transform_b_to_a, capsule_b.center1) q_c = math.transform_point(transform_b_to_a, capsule_b.center2) e_c = math.sub(q_c, p_c) - edges = hull.hull_edges(hull_a) - points = hull.hull_points(hull_a) + edges = hull.edges(hull_a) + points = hull.points(hull_a) edge2 = query.index_b p_h = points[edges[edge2].origin] q_h = points[edges[edges[edge2].twin].origin] @@ -878,7 +878,7 @@ collide_hull_and_capsule(m: *LocalManifold, capacity: int, hull_a: *HullData, ca reach = speculative_distance() m.point_count = 0 if capacity < 2 { return } - input = DistanceInput { proxy_a: hull.hull_proxy(hull_a), proxy_b: distance.shape_proxy((&capsule_b) as ptr, 2, 0.0), + input = DistanceInput { proxy_a: hull.proxy(hull_a), proxy_b: distance.shape_proxy((&capsule_b) as ptr, 2, 0.0), transform: transform_b_to_a, use_radii: false } output = distance.shape_distance(&input, cache, null, 0) if output.distance > capsule_b.radius + reach { @@ -888,7 +888,7 @@ collide_hull_and_capsule(m: *LocalManifold, capacity: int, hull_a: *HullData, ca if output.distance > 100.0 * FLOAT_EPSILON { // Shallow. - planes = hull.hull_planes(hull_a) + planes = hull.planes(hull_a) delta = output.normal ref_face = hull.find_hull_support_face(hull_a, delta) ref_plane = planes[ref_face] @@ -954,12 +954,12 @@ compute_separating_axis(hull_a: *HullData, hull_b: *HullData, xf_b: Transform, e edge: SeparatingAxis { normal: math.vec3_zero(), separation: 0.0 - math.MAX_FLOAT, index_a: NULL_INDEX, index_b: NULL_INDEX, kind: AXIS_EDGE_PAIR }, separated_feature: AXIS_INVALID } - planes_a = hull.hull_planes(hull_a) - points_a = hull.hull_points(hull_a) - planes_b = hull.hull_planes(hull_b) - points_b = hull.hull_points(hull_b) - edges_a = hull.hull_edges(hull_a) - edges_b = hull.hull_edges(hull_b) + planes_a = hull.planes(hull_a) + points_a = hull.points(hull_a) + planes_b = hull.planes(hull_b) + points_b = hull.points(hull_b) + edges_a = hull.edges(hull_a) + edges_b = hull.edges(hull_b) // No face can separate the hulls by more than its normal's reach along // the line between their centres less their inner spheres' radii @@ -1093,8 +1093,8 @@ compute_separating_axis(hull_a: *HullData, hull_b: *HullData, xf_b: Transform, e center_a_in_b = math.mul_mv(inv_r, math.sub(hull_a.center, xf_b.p)) scratch_ready() lists = edge_lists() as int[] - cosines_a = hull.hull_edge_cosines(hull_a) - cosines_b = hull.hull_edge_cosines(hull_b) + cosines_a = hull.edge_cosines(hull_a) + cosines_b = hull.edge_cosines(hull_b) na = 0 i = 0 while i < hull_a.edge_count { @@ -1352,10 +1352,10 @@ support_index_wide(h: *HullData, direction: Vec3) -> int { // reduction costs more than eight or sixteen dot products (measured on // the reference's rocks, ten vertices, and boxes and six-sided // cylinders), and the answer is the same first maximum. - if h.vertex_count <= WIDE_SUPPORT_MIN_VERTICES { return support_index(hull.hull_points(h), h.vertex_count, direction) } + if h.vertex_count <= WIDE_SUPPORT_MIN_VERTICES { return support_index(hull.points(h), h.vertex_count, direction) } scratch_ready() - stride = hull.hull_soa_stride(h.vertex_count) - rows = hull.hull_soa_vertices(h) + stride = hull.soa_stride(h.vertex_count) + rows = hull.soa_vertices(h) d = math.narrow3(direction) dx = lanes.splat8(d.x) dy = lanes.splat8(d.y) @@ -1424,9 +1424,9 @@ get_best_axis(query: *AxisQuery) -> SeparatingAxis { // The incident face of the hull, in frame A, as a clip polygon with each // vertex tagged by its edges; separations from the reference plane. build_polygon(out: ClipVertex[], transform: Transform, h: *HullData, inc_face: int, ref_plane: Plane) -> int { - faces = hull.hull_faces(h) - edges = hull.hull_edges(h) - points = hull.hull_points(h) + faces = hull.faces(h) + edges = hull.edges(h) + points = hull.points(h) start = faces[inc_face] edge_index = start out_count = 0 @@ -1455,12 +1455,12 @@ build_polygon(out: ClipVertex[], transform: Transform, h: *HullData, inc_face: i add_near_vertices(out: ClipVertex[], count: int, hull_a: *HullData, ref_face: int, ref_plane: Plane, hull_b: *HullData, inc_face: int, transform_b_to_a: Transform) -> int { reach = speculative_distance() - faces_a = hull.hull_faces(hull_a) - edges_a = hull.hull_edges(hull_a) - points_a = hull.hull_points(hull_a) - faces_b = hull.hull_faces(hull_b) - edges_b = hull.hull_edges(hull_b) - points_b = hull.hull_points(hull_b) + faces_a = hull.faces(hull_a) + edges_a = hull.edges(hull_a) + points_a = hull.points(hull_a) + faces_b = hull.faces(hull_b) + edges_b = hull.edges(hull_b) + points_b = hull.points(hull_b) m = math.make_matrix_from_quat(transform_b_to_a.q) out_count = count v = 0 @@ -1507,10 +1507,10 @@ add_near_vertices(out: ClipVertex[], count: int, hull_a: *HullData, ref_face: in build_face_a_contact(m: *LocalManifold, capacity: int, hull_a: *HullData, hull_b: *HullData, transform_b_to_a: Transform, query: SeparatingAxis, cache: *SATCache) -> bool { reach = speculative_distance() - faces_a = hull.hull_faces(hull_a) - edges_a = hull.hull_edges(hull_a) - planes_a = hull.hull_planes(hull_a) - points_a = hull.hull_points(hull_a) + faces_a = hull.faces(hull_a) + edges_a = hull.edges(hull_a) + planes_a = hull.planes(hull_a) + points_a = hull.points(hull_a) ref_face = query.index_a ref_plane = planes_a[ref_face] @@ -1601,10 +1601,10 @@ build_face_b_contact(m: *LocalManifold, capacity: int, hull_a: *HullData, hull_b // both segments; halfway between them. build_edge_contact(m: *LocalManifold, hull_a: *HullData, hull_b: *HullData, transform_b_to_a: Transform, query: SeparatingAxis, cache: *SATCache) -> bool { - edges_a = hull.hull_edges(hull_a) - points_a = hull.hull_points(hull_a) - edges_b = hull.hull_edges(hull_b) - points_b = hull.hull_points(hull_b) + edges_a = hull.edges(hull_a) + points_a = hull.points(hull_a) + edges_b = hull.edges(hull_b) + points_b = hull.points(hull_b) p_a = points_a[edges_a[query.index_a].origin] q_a = points_a[edges_a[edges_a[query.index_a].twin].origin] e_a = math.sub(q_a, p_a) @@ -1637,12 +1637,12 @@ collide_hulls(m: *LocalManifold, capacity: int, hull_a: *HullData, hull_b: *Hull reach = speculative_distance() m.point_count = 0 if capacity < 4 { return } - edges_a = hull.hull_edges(hull_a) - planes_a = hull.hull_planes(hull_a) - points_a = hull.hull_points(hull_a) - edges_b = hull.hull_edges(hull_b) - planes_b = hull.hull_planes(hull_b) - points_b = hull.hull_points(hull_b) + edges_a = hull.edges(hull_a) + planes_a = hull.planes(hull_a) + points_a = hull.points(hull_a) + edges_b = hull.edges(hull_b) + planes_b = hull.planes(hull_b) + points_b = hull.points(hull_b) cache.hit = 0 if cache.kind == AXIS_FACE_A { diff --git a/aephysics/mesh/module.ae b/aephysics/mesh/module.ae index 00f2b12..c492a68 100644 --- a/aephysics/mesh/module.ae +++ b/aephysics/mesh/module.ae @@ -1663,7 +1663,7 @@ collide_mover_and_hull(planes: PlaneResult[], h: *HullData, mover: Capsule) -> i segment = local_points() as Vec3[] segment[0] = mover.center1 segment[1] = mover.center2 - input = DistanceInput { proxy_a: hull.hull_proxy(h), proxy_b: distance.shape_proxy(local_points(), 2, mover.radius), + input = DistanceInput { proxy_a: hull.proxy(h), proxy_b: distance.shape_proxy(local_points(), 2, mover.radius), transform: math.transform_identity(), use_radii: false } cache = distance.empty_cache() output = distance.shape_distance(&input, &cache, null, 0) diff --git a/aephysics/mesh_contact/module.ae b/aephysics/mesh_contact/module.ae index 8d5a86b..f294cda 100644 --- a/aephysics/mesh_contact/module.ae +++ b/aephysics/mesh_contact/module.ae @@ -566,10 +566,10 @@ compute_mesh_manifolds(cache: *MeshContactCache, mesh_shape: *Shape, xf_a: Trans accepted[accepted_count] = this_manifold accepted_count = accepted_count + 1 } else { - min_separation = manifold.manifold_point(m, 0).separation + min_separation = manifold.point(m, 0).separation i = 1 while i < point_count { - min_separation = math.min_float(min_separation, manifold.manifold_point(m, i).separation) + min_separation = math.min_float(min_separation, manifold.point(m, i).separation) i = i + 1 } if min_separation < 0.0 - 2.0 * linear_slop { @@ -695,7 +695,7 @@ compute_mesh_manifolds(cache: *MeshContactCache, mesh_shape: *Shape, xf_a: Trans target = clusters[cluster_index].points as LocalManifoldPoint[] j = 0 while j < m.point_count { - p = manifold.manifold_point(m, j) + p = manifold.point(m, j) p.triangle_index = m.triangle_index target[clusters[cluster_index].point_count] = p clusters[cluster_index].point_count = clusters[cluster_index].point_count + 1 diff --git a/aephysics/physics_world/module.ae b/aephysics/physics_world/module.ae index 5b40d3d..1218d04 100644 --- a/aephysics/physics_world/module.ae +++ b/aephysics/physics_world/module.ae @@ -480,8 +480,8 @@ get_counters(id: WorldId) -> Counters { joints = dynamics.world_joint_count(w) islands = dynamics.world_island_count(w) stack_used = basics.stack_max_allocation(&w.stack) - static_height = dynamic_tree.tree_get_height(broad_phase.broad_phase_tree(&w.broad_phase, dynamics.BODY_STATIC)) - dynamic_height = dynamic_tree.tree_get_height(broad_phase.broad_phase_tree(&w.broad_phase, dynamics.BODY_DYNAMIC)) + static_height = dynamic_tree.tree_get_height(broad_phase.tree(&w.broad_phase, dynamics.BODY_STATIC)) + dynamic_height = dynamic_tree.tree_get_height(broad_phase.tree(&w.broad_phase, dynamics.BODY_DYNAMIC)) // As the reference counts them (b3World_GetCounters): every colour's // convex and scalar contacts and joints, and the awake contacts are // those plus the awake set's non-touching ones. @@ -606,9 +606,9 @@ world_byte_count(w: *World, logged: bool) -> long { basics.log("owned geometry: ${owned}") } - static_tree = dynamic_tree.tree_get_byte_count(broad_phase.broad_phase_tree(&w.broad_phase, dynamics.BODY_STATIC)) - kinematic_tree = dynamic_tree.tree_get_byte_count(broad_phase.broad_phase_tree(&w.broad_phase, dynamics.BODY_KINEMATIC)) - dynamic_tree_bytes = dynamic_tree.tree_get_byte_count(broad_phase.broad_phase_tree(&w.broad_phase, dynamics.BODY_DYNAMIC)) + static_tree = dynamic_tree.tree_get_byte_count(broad_phase.tree(&w.broad_phase, dynamics.BODY_STATIC)) + kinematic_tree = dynamic_tree.tree_get_byte_count(broad_phase.tree(&w.broad_phase, dynamics.BODY_KINEMATIC)) + dynamic_tree_bytes = dynamic_tree.tree_get_byte_count(broad_phase.tree(&w.broad_phase, dynamics.BODY_DYNAMIC)) pair_set = basics.set_bytes(&w.broad_phase.pair_set) total = total + ((static_tree + kinematic_tree + dynamic_tree_bytes + pair_set) as long) if logged { @@ -743,7 +743,7 @@ get_bounds(id: WorldId) -> AABB { bounds = math.aabb_empty() i = 0 while i < BODY_KIND_COUNT { - tree = broad_phase.broad_phase_tree(&w.broad_phase, i) + tree = broad_phase.tree(&w.broad_phase, i) if dynamic_tree.tree_get_proxy_count(tree) > 0 { bounds = math.aabb_union(bounds, dynamic_tree.tree_get_root_bounds(tree)) } i = i + 1 } @@ -753,7 +753,7 @@ get_bounds(id: WorldId) -> AABB { rebuild_static_tree(id: WorldId) { if dynamics.recording_on(id.index1 - 1) { recorder = dynamics.rec_begin(id.index1 - 1, dynamics.OP_REBUILD_STATIC_TREE); dynamics.rec_end(recorder) } w = world_of(id) - if w != null { dynamic_tree.tree_rebuild(broad_phase.broad_phase_tree(&w.broad_phase, dynamics.BODY_STATIC), true) } + if w != null { dynamic_tree.tree_rebuild(broad_phase.tree(&w.broad_phase, dynamics.BODY_STATIC), true) } } // --- the world queries ------------------------------------------------------------------------------------------------ @@ -805,7 +805,7 @@ overlap_aabb_internal(id: WorldId, box: AABB, filter: QueryFilter, visitor: fn(S qc = WorldQueryContext { w: w, visitor: visitor, filter: filter, proxy: distance.shape_proxy(null, 0, 0.0), origin: math.vec3_zero(), user_context: context } i = 0 while i < BODY_KIND_COUNT { - result = dynamic_tree.tree_query(broad_phase.broad_phase_tree(&w.broad_phase, i), box, filter.mask_bits, false, aabb_query_visitor, (&qc) as ptr) + result = dynamic_tree.tree_query(broad_phase.tree(&w.broad_phase, i), box, filter.mask_bits, false, aabb_query_visitor, (&qc) as ptr) stats.node_visits = stats.node_visits + result.node_visits stats.leaf_visits = stats.leaf_visits + result.leaf_visits i = i + 1 @@ -834,7 +834,7 @@ overlap_shape_internal(id: WorldId, origin: Vec3, proxy: ShapeProxy, filter: Que qc = WorldQueryContext { w: w, visitor: visitor, filter: filter, proxy: proxy, origin: origin, user_context: context } i = 0 while i < BODY_KIND_COUNT { - result = dynamic_tree.tree_query(broad_phase.broad_phase_tree(&w.broad_phase, i), box, filter.mask_bits, false, shape_overlap_visitor, (&qc) as ptr) + result = dynamic_tree.tree_query(broad_phase.tree(&w.broad_phase, i), box, filter.mask_bits, false, shape_overlap_visitor, (&qc) as ptr) stats.node_visits = stats.node_visits + result.node_visits stats.leaf_visits = stats.leaf_visits + result.leaf_visits i = i + 1 @@ -882,7 +882,7 @@ collide_mover_internal(id: WorldId, origin: Vec3, mover: Capsule, filter: QueryF mc = WorldMoverContext { w: w, visitor: visitor, filter: filter, mover: mover, origin: origin, user_context: context, planes: planes_block } i = 0 while i < BODY_KIND_COUNT { - dynamic_tree.tree_query(broad_phase.broad_phase_tree(&w.broad_phase, i), box, filter.mask_bits, false, mover_collide_visitor, (&mc) as ptr) + dynamic_tree.tree_query(broad_phase.tree(&w.broad_phase, i), box, filter.mask_bits, false, mover_collide_visitor, (&mc) as ptr) i = i + 1 } basics.free_bytes(planes_block, MOVER_PLANE_CAPACITY * sizeof(PlaneResult)) @@ -930,7 +930,7 @@ cast_ray_internal(id: WorldId, origin: Vec3, translation: Vec3, filter: QueryFil translation: translation, can_encroach: false, user_context: context } i = 0 while i < BODY_KIND_COUNT { - result = dynamic_tree.tree_ray_cast(broad_phase.broad_phase_tree(&w.broad_phase, i), &input, filter.mask_bits, false, ray_cast_visitor, (&cc) as ptr) + result = dynamic_tree.tree_ray_cast(broad_phase.tree(&w.broad_phase, i), &input, filter.mask_bits, false, ray_cast_visitor, (&cc) as ptr) stats.node_visits = stats.node_visits + result.node_visits stats.leaf_visits = stats.leaf_visits + result.leaf_visits if cc.fraction == 0.0 { return stats } @@ -967,7 +967,7 @@ cast_ray_closest_internal(id: WorldId, origin: Vec3, translation: Vec3, filter: translation: translation, can_encroach: false, user_context: (&result) as ptr } i = 0 while i < BODY_KIND_COUNT { - stats = dynamic_tree.tree_ray_cast(broad_phase.broad_phase_tree(&w.broad_phase, i), &input, filter.mask_bits, false, ray_cast_visitor, (&cc) as ptr) + stats = dynamic_tree.tree_ray_cast(broad_phase.tree(&w.broad_phase, i), &input, filter.mask_bits, false, ray_cast_visitor, (&cc) as ptr) result.node_visits = result.node_visits + stats.node_visits result.leaf_visits = result.leaf_visits + stats.leaf_visits if cc.fraction == 0.0 { return result } @@ -983,7 +983,7 @@ shape_cast_visitor(input: *BoxCastInput, proxy_id: int, user_data: long, context s = dynamics.world_shape_at(w, user_data as int) if shape.should_query_collide(s.filter, cc.filter) == false { return input.max_fraction } t = relative_transform(w, s.body_id, cc.origin) - output = shape.shape_cast_shape(&s.geometry, t, cc.proxy, cc.translation, input.max_fraction, cc.can_encroach) + output = shape.cast_shape(&s.geometry, t, cc.proxy, cc.translation, input.max_fraction, cc.can_encroach) if output.hit { visitor = cc.visitor fraction = visitor(dynamics.make_shape_id(w, s), math.add(cc.origin, output.point), output.normal, output.fraction, @@ -1007,7 +1007,7 @@ cast_shape_internal(id: WorldId, origin: Vec3, proxy: ShapeProxy, translation: V can_encroach: false, user_context: context } i = 0 while i < BODY_KIND_COUNT { - result = dynamic_tree.tree_box_cast(broad_phase.broad_phase_tree(&w.broad_phase, i), &input, filter.mask_bits, false, shape_cast_visitor, (&cc) as ptr) + result = dynamic_tree.tree_box_cast(broad_phase.tree(&w.broad_phase, i), &input, filter.mask_bits, false, shape_cast_visitor, (&cc) as ptr) stats.node_visits = stats.node_visits + result.node_visits stats.leaf_visits = stats.leaf_visits + result.leaf_visits if cc.fraction == 0.0 { return stats } @@ -1042,7 +1042,7 @@ mover_cast_visitor(input: *BoxCastInput, proxy_id: int, user_data: long, context if filter_fn(dynamics.make_shape_id(w, s), mc.user_context) == false { return mc.fraction } } t = relative_transform(w, s.body_id, mc.origin) - output = shape.shape_cast_shape(&s.geometry, t, mc.proxy, mc.translation, input.max_fraction, mc.can_encroach) + output = shape.cast_shape(&s.geometry, t, mc.proxy, mc.translation, input.max_fraction, mc.can_encroach) // A shape already overlapped is ignored. if output.fraction == 0.0 { return mc.fraction } mc.fraction = output.fraction @@ -1067,7 +1067,7 @@ cast_mover_internal(id: WorldId, origin: Vec3, mover: Capsule, translation: Vec3 input = BoxCastInput { box: math.offset_aabb(math.make_aabb(mc.proxy.points as float[], 2, mover.radius), origin), translation: translation, max_fraction: 1.0 } i = 0 while i < BODY_KIND_COUNT { - dynamic_tree.tree_box_cast(broad_phase.broad_phase_tree(&w.broad_phase, i), &input, filter.mask_bits, false, mover_cast_visitor, (&mc) as ptr) + dynamic_tree.tree_box_cast(broad_phase.tree(&w.broad_phase, i), &input, filter.mask_bits, false, mover_cast_visitor, (&mc) as ptr) if mc.fraction == 0.0 { return 0.0 } input.max_fraction = mc.fraction i = i + 1 @@ -1433,7 +1433,7 @@ world_draw(id: WorldId, draw: *DebugDraw, mask_bits: long) { dc = DrawContext { w: w, draw: draw } i = 0 while i < BODY_KIND_COUNT { - dynamic_tree.tree_query(broad_phase.broad_phase_tree(&w.broad_phase, i), draw.drawing_bounds, mask_bits, false, draw_query_visitor, (&dc) as ptr) + dynamic_tree.tree_query(broad_phase.tree(&w.broad_phase, i), draw.drawing_bounds, mask_bits, false, draw_query_visitor, (&dc) as ptr) i = i + 1 } body_id = 0 @@ -1656,7 +1656,7 @@ body_cast_shape(id: BodyId, origin: Vec3, proxy: ShapeProxy, translation: Vec3, s = dynamics.world_shape_at(w, shape_id) shape_id = s.next_shape_id if shape.should_query_collide(s.filter, filter) == false { continue } - output = shape.shape_cast_shape(&s.geometry, t, proxy, translation, fraction_left, can_encroach) + output = shape.cast_shape(&s.geometry, t, proxy, translation, fraction_left, can_encroach) if output.hit == false || output.fraction > fraction_left { continue } result = BodyCastResult { shape_id: dynamics.make_shape_id(w, s), point: math.add(origin, output.point), normal: output.normal, fraction: output.fraction, triangle_index: output.triangle_index, @@ -1814,6 +1814,6 @@ explode(id: WorldId, def: *ExplosionDef) { ec = WorldExplosionContext { w: w, position: def.position, radius: def.radius, falloff: def.falloff, impulse_per_area: def.impulse_per_area, local_position: math.vec3_zero() } extent = def.radius + def.falloff box = math.offset_aabb(AABB { lower: math.vec3(0.0 - extent, 0.0 - extent, 0.0 - extent), upper: math.vec3(extent, extent, extent) }, def.position) - dynamic_tree.tree_query(broad_phase.broad_phase_tree(&w.broad_phase, dynamics.BODY_DYNAMIC), box, def.mask_bits, false, explosion_visitor, (&ec) as ptr) + dynamic_tree.tree_query(broad_phase.tree(&w.broad_phase, dynamics.BODY_DYNAMIC), box, def.mask_bits, false, explosion_visitor, (&ec) as ptr) w.locked = false } diff --git a/aephysics/recording/module.ae b/aephysics/recording/module.ae index 9b9df64..a6a553e 100644 --- a/aephysics/recording/module.ae +++ b/aephysics/recording/module.ae @@ -183,7 +183,7 @@ load_recording_from_file(path: string) -> *Recording { return null } r = create_recording() - dynamics.rec_raw(r, string.aether_string_raw_ptr(data), length) + dynamics.rec_raw(r, (string.bytes(data) as ptr), length) string.free(data) return r } @@ -406,9 +406,9 @@ load_geometry(p: *Player, offset: int, byte_count: int) -> bool { g.byte_count = n g.block = block valid = false - if kind == shape.SHAPE_HULL { valid = hull.hull_bytes_valid(block as *HullData, n) && (block as *HullData).byte_count == n } - else if kind == shape.SHAPE_MESH { valid = mesh.mesh_bytes_valid(block as *MeshData, n) && (block as *MeshData).byte_count == n } - else if kind == shape.SHAPE_HEIGHT_FIELD { valid = height_field.height_field_bytes_valid(block as *HeightFieldData, n) && (block as *HeightFieldData).byte_count == n } + if kind == shape.SHAPE_HULL { valid = hull.bytes_valid(block as *HullData, n) && (block as *HullData).byte_count == n } + else if kind == shape.SHAPE_MESH { valid = mesh.bytes_valid(block as *MeshData, n) && (block as *MeshData).byte_count == n } + else if kind == shape.SHAPE_HEIGHT_FIELD { valid = height_field.bytes_valid(block as *HeightFieldData, n) && (block as *HeightFieldData).byte_count == n } else if kind == shape.SHAPE_COMPOUND { valid = compound.convert_bytes_to_compound(block, n) != null } if valid == false { return false } i = i + 1 diff --git a/aephysics/shape/module.ae b/aephysics/shape/module.ae index 0c71d55..5e1492b 100644 --- a/aephysics/shape/module.ae +++ b/aephysics/shape/module.ae @@ -377,8 +377,8 @@ collide_mover(planes: PlaneResult[], capacity: int, shape: *Shape, t: Transform, make_shape_proxy(shape: *Shape) -> ShapeProxy { kind = shape.kind if kind == SHAPE_CAPSULE { return sphere.capsule_proxy(shape.capsule) } - if kind == SHAPE_SPHERE { return sphere.sphere_proxy(shape.sphere) } - if kind == SHAPE_HULL { return hull.hull_proxy(shape.hull) } + if kind == SHAPE_SPHERE { return sphere.proxy(shape.sphere) } + if kind == SHAPE_HULL { return hull.proxy(shape.hull) } return distance.shape_proxy(null, 0, 0.0) } @@ -497,7 +497,7 @@ compound_time_of_impact_visitor(c: *CompoundData, child_index: int, context: ptr ctx.input.proxy_a = distance.shape_proxy(toi_points_a(), 2, child.capsule.radius) better_hit(ctx, distance.time_of_impact(&ctx.input)) } else if kind == SHAPE_HULL { - ctx.input.proxy_a = hull.hull_proxy(child.hull) + ctx.input.proxy_a = hull.proxy(child.hull) better_hit(ctx, distance.time_of_impact(&ctx.input)) } else if kind == SHAPE_SPHERE { points = toi_points_a() as Vec3[] diff --git a/aephysics/snapshot/module.ae b/aephysics/snapshot/module.ae index 512b14c..fc6e118 100644 --- a/aephysics/snapshot/module.ae +++ b/aephysics/snapshot/module.ae @@ -547,7 +547,7 @@ save_world(world_id: WorldId) -> (ptr, int) { // slots in place, so a walk over it meets the pairs in the same order). t = 0 while t < broad_phase.BODY_TYPE_COUNT { - block, byte_count = dynamic_tree.tree_save_bytes(broad_phase.broad_phase_tree(&w.broad_phase, t)) + block, byte_count = dynamic_tree.tree_save_bytes(broad_phase.tree(&w.broad_phase, t)) w_int(&wr, byte_count) w_bytes(&wr, block, byte_count) if block != null { basics.free_bytes(block, byte_count) } @@ -777,9 +777,9 @@ fill_world(w: *World, rd: *SnapReader) -> bool { memcpy(block, rd.data + rd.at, byte_count) rd.at = rd.at + byte_count valid = false - if kind == shape.SHAPE_HULL { valid = hull.hull_bytes_valid(block as *HullData, byte_count) && (block as *HullData).byte_count == byte_count } - else if kind == shape.SHAPE_MESH { valid = mesh.mesh_bytes_valid(block as *MeshData, byte_count) && (block as *MeshData).byte_count == byte_count } - else if kind == shape.SHAPE_HEIGHT_FIELD { valid = height_field.height_field_bytes_valid(block as *HeightFieldData, byte_count) && (block as *HeightFieldData).byte_count == byte_count } + if kind == shape.SHAPE_HULL { valid = hull.bytes_valid(block as *HullData, byte_count) && (block as *HullData).byte_count == byte_count } + else if kind == shape.SHAPE_MESH { valid = mesh.bytes_valid(block as *MeshData, byte_count) && (block as *MeshData).byte_count == byte_count } + else if kind == shape.SHAPE_HEIGHT_FIELD { valid = height_field.bytes_valid(block as *HeightFieldData, byte_count) && (block as *HeightFieldData).byte_count == byte_count } else if kind == shape.SHAPE_COMPOUND { valid = compound.convert_bytes_to_compound(block, byte_count) != null } if valid == false { basics.free_bytes(block, byte_count) @@ -972,7 +972,7 @@ fill_world(w: *World, rd: *SnapReader) -> bool { rd.ok = false break } - slot = broad_phase.broad_phase_tree(&w.broad_phase, t) + slot = broad_phase.tree(&w.broad_phase, t) dynamic_tree.tree_destroy(slot) memcpy(slot as ptr, (&tree) as ptr, sizeof(DynamicTree)) t = t + 1 diff --git a/aephysics/solver/module.ae b/aephysics/solver/module.ae index 283f0c3..19fd78f 100644 --- a/aephysics/solver/module.ae +++ b/aephysics/solver/module.ae @@ -323,7 +323,7 @@ continuous_query_visitor(proxy_id: int, user_data: long, context: ptr) -> bool { if filter(dynamics.make_shape_id(w, s), dynamics.make_shape_id(w, fast_shape), w.custom_filter_context) == false { return true } } sweep_a = dynamics.make_relative_sweep(sim, ctx.base) - output = shape.shape_time_of_impact(&s.geometry, &fast_shape.geometry, sweep_a, ctx.sweep, ctx.fraction, is_sensor) + output = shape.time_of_impact(&s.geometry, &fast_shape.geometry, sweep_a, ctx.sweep, ctx.fraction, is_sensor) if is_sensor { if output.fraction <= ctx.fraction && ctx.sensor_count < MAX_CONTINUOUS_SENSOR_HITS { hits = ctx.sensor_hits as SensorHit[] @@ -359,7 +359,7 @@ grow_bounds(w: *World, s: *WorldShape, sim: *BodySim, is_bullet: bool, covered: dynamics.set_shape_fat_aabb(w, s.id, fat) native.atomic_or((&sim.flags) as ptr, dynamics.ENLARGE_BOUNDS) // A bullet's proxies are enlarged later, serially. - if is_bullet == false { broad_phase.broad_phase_mark_proxy_moved(&w.broad_phase, s.proxy_key, fat) } + if is_bullet == false { broad_phase.mark_proxy_moved(&w.broad_phase, s.proxy_key, fat) } } // A shape's bounds `aabb`, and with every contact reaching out over the @@ -386,9 +386,9 @@ solve_continuous(w: *World, sim_index: int, dt: float) { base = fast_sim.center0 sweep = dynamics.make_relative_sweep(fast_sim, base) xf2 = Transform { p: math.sub(sweep.c2, math.rotate_vector(sweep.q2, sweep.local_center)), q: sweep.q2 } - static_tree = broad_phase.broad_phase_tree(&w.broad_phase, dynamics.BODY_STATIC) - kinematic_tree = broad_phase.broad_phase_tree(&w.broad_phase, dynamics.BODY_KINEMATIC) - dynamic_tree_ = broad_phase.broad_phase_tree(&w.broad_phase, dynamics.BODY_DYNAMIC) + static_tree = broad_phase.tree(&w.broad_phase, dynamics.BODY_STATIC) + kinematic_tree = broad_phase.tree(&w.broad_phase, dynamics.BODY_KINEMATIC) + dynamic_tree_ = broad_phase.tree(&w.broad_phase, dynamics.BODY_DYNAMIC) fast_body = dynamics.body_at(w, fast_sim.body_id) ctx = ContinuousContext { w: w, fast_sim: fast_sim, fast_shape: null, sweep: sweep, base: base, fraction: 1.0, sensor_hits: sensor_hits(), sensor_fractions: sensor_fractions(), sensor_count: 0, @@ -595,7 +595,7 @@ finalize_range(start: int, end: int, worker: int, context: ptr) { margin = math.vec3(s.aabb_margin, s.aabb_margin, s.aabb_margin) fat = math.aabb_round_out(AABB { lower: math.sub(covered.lower, margin), upper: math.add(covered.upper, margin) }) dynamics.set_shape_fat_aabb(w, shape_id, fat) - broad_phase.broad_phase_mark_proxy_moved(&w.broad_phase, s.proxy_key, fat) + broad_phase.mark_proxy_moved(&w.broad_phase, s.proxy_key, fat) } shape_id = s.next_shape_id } @@ -1372,8 +1372,8 @@ solve_step(w: *World, ctx: *StepContext) { report_joint_events(w) if has_hits { report_hit_events(w) } else { basics.buffer_clear(&w.contact_hit_events) } - dynamic_tree.tree_refit(broad_phase.broad_phase_tree(&w.broad_phase, dynamics.BODY_KINEMATIC)) - dynamic_tree.tree_refit(broad_phase.broad_phase_tree(&w.broad_phase, dynamics.BODY_DYNAMIC)) + dynamic_tree.tree_refit(broad_phase.tree(&w.broad_phase, dynamics.BODY_KINEMATIC)) + dynamic_tree.tree_refit(broad_phase.tree(&w.broad_phase, dynamics.BODY_DYNAMIC)) profile.refit = lap(&ticks) // Bullets: swept against everything now that the trees hold the step's @@ -1382,7 +1382,7 @@ solve_step(w: *World, ctx: *StepContext) { bullets = ctx.bullet_bodies as int[] basics.sort_ints(bullets, ctx.bullet_body_count) parallel.parallel_for(w.scheduler as *Scheduler, bullet_range, ctx.bullet_body_count, 8, ctx as ptr) - tree = broad_phase.broad_phase_tree(&w.broad_phase, dynamics.BODY_DYNAMIC) + tree = broad_phase.tree(&w.broad_phase, dynamics.BODY_DYNAMIC) i = 0 while i < ctx.bullet_body_count { bullet_sim = dynamics.body_sim_at(awake, bullets[i]) diff --git a/aephysics/triangle_manifold/module.ae b/aephysics/triangle_manifold/module.ae index 7911f61..9fd0a1c 100644 --- a/aephysics/triangle_manifold/module.ae +++ b/aephysics/triangle_manifold/module.ae @@ -446,7 +446,7 @@ collide_triangle_and_capsule(m: *LocalManifold, capacity: int, v1: Vec3, v2: Vec face_separation = face_query.separation - radius build_triangle_and_capsule_face_contact(m, v1, v2, v3, plane, capsule_b) if m.point_count == 2 { - face_separation = math.min_float(manifold.manifold_point(m, 0).separation, manifold.manifold_point(m, 1).separation) + face_separation = math.min_float(manifold.point(m, 0).separation, manifold.point(m, 1).separation) } if edge_query.index_a == NULL_INDEX { return } edge_separation = edge_query.separation - radius @@ -471,7 +471,7 @@ triangle_support(t: *TriangleData, direction: Vec3) -> int { // The triangle's plane against the hull's deepest vertex. query_triangle_face(t: *TriangleData, h: *HullData) -> SeparatingAxis { - points = hull.hull_points(h) + points = hull.points(h) vertex_index = hull.find_hull_support_vertex(h, math.neg(t.plane.normal)) return SeparatingAxis { normal: t.plane.normal, separation: math.plane_separation(t.plane, points[vertex_index]), index_a: 0, index_b: vertex_index, kind: AXIS_FACE_A } @@ -480,7 +480,7 @@ query_triangle_face(t: *TriangleData, h: *HullData) -> SeparatingAxis { // The hull's faces against the triangle's vertices; the normal from the // triangle to the hull. query_hull_face(t: *TriangleData, h: *HullData) -> SeparatingAxis { - planes = hull.hull_planes(h) + planes = hull.planes(h) max_normal = math.vec3_zero() max_separation = 0.0 - math.MAX_FLOAT max_face = NULL_INDEX @@ -505,9 +505,9 @@ query_hull_face(t: *TriangleData, h: *HullData) -> SeparatingAxis { query_triangle_and_hull_edges(t: *TriangleData, h: *HullData) -> SeparatingAxis { result = SeparatingAxis { normal: math.vec3_zero(), separation: 0.0 - math.MAX_FLOAT, index_a: NULL_INDEX, index_b: NULL_INDEX, kind: AXIS_EDGE_PAIR } tri_normal = t.plane.normal - edges = hull.hull_edges(h) - points = hull.hull_points(h) - planes = hull.hull_planes(h) + edges = hull.edges(h) + points = hull.points(h) + planes = hull.planes(h) squared_tolerance = 0.005 * 0.005 i = 0 while i < h.edge_count { @@ -550,10 +550,10 @@ collide_hull_face(m: *LocalManifold, point_capacity: int, t: *TriangleData, h: * cache: *SATCache, enable_speculative: bool) -> float { reach = manifold.speculative_distance() m.point_count = 0 - faces = hull.hull_faces(h) - edges = hull.hull_edges(h) - planes = hull.hull_planes(h) - points = hull.hull_points(h) + faces = hull.faces(h) + edges = hull.edges(h) + planes = hull.planes(h) + points = hull.points(h) ref_plane = planes[query.index_b] scratch_ready() input = clip1() as ClipVertex[] @@ -622,9 +622,9 @@ collide_hull_face(m: *LocalManifold, point_capacity: int, t: *TriangleData, h: * collide_triangle_face(m: *LocalManifold, point_capacity: int, t: *TriangleData, h: *HullData, query: SeparatingAxis, cache: *SATCache, enable_speculative: bool) -> float { reach = manifold.speculative_distance() - faces = hull.hull_faces(h) - edges = hull.hull_edges(h) - points = hull.hull_points(h) + faces = hull.faces(h) + edges = hull.edges(h) + points = hull.points(h) ref_plane = t.plane inc_face = manifold.find_incident_face(h, ref_plane.normal, query.index_b) scratch_ready() @@ -691,8 +691,8 @@ collide_triangle_face(m: *LocalManifold, point_capacity: int, t: *TriangleData, // lines, if within both segments. collide_triangle_and_hull_edges(m: *LocalManifold, capacity: int, triangle_point: Vec3, triangle_edge_vector: Vec3, h: *HullData, query: SeparatingAxis, cache: *SATCache) { - edges = hull.hull_edges(h) - points = hull.hull_points(h) + edges = hull.edges(h) + points = hull.points(h) p_b = points[edges[query.index_b].origin] e_b = math.sub(points[edges[edges[query.index_b].twin].origin], p_b) result = math.line_distance(triangle_point, triangle_edge_vector, p_b, e_b) @@ -739,9 +739,9 @@ collide_triangle_and_hull(m: *LocalManifold, capacity: int, v1: Vec3, v2: Vec3, } t = TriangleData { v1: v1, v2: v2, v3: v3, e1: math.sub(v2, v1), e2: math.sub(v3, v2), e3: math.sub(v1, v3), plane: plane, flags: triangle_flags } - edges = hull.hull_edges(hull_b) - planes = hull.hull_planes(hull_b) - points = hull.hull_points(hull_b) + edges = hull.edges(hull_b) + planes = hull.planes(hull_b) + points = hull.points(hull_b) speculative = 0.0 if enable_speculative { speculative = reach } cache.hit = 1 @@ -862,7 +862,7 @@ collide_triangle_and_hull(m: *LocalManifold, capacity: int, v1: Vec3, v2: Vec3, // Speculation can leave the SAT with no points; GJK then, so nothing tunnels. if m.point_count == 0 { - input = DistanceInput { proxy_a: triangle_proxy(v1, v2, v3), proxy_b: hull.hull_proxy(hull_b), + input = DistanceInput { proxy_a: triangle_proxy(v1, v2, v3), proxy_b: hull.proxy(hull_b), transform: math.transform_identity(), use_radii: false } simplex_cache = distance.empty_cache() output = distance.shape_distance(&input, &simplex_cache, null, 0) diff --git a/bench/broad_phase.ae b/bench/broad_phase.ae index 9ffc2ce..9065f39 100644 --- a/bench/broad_phase.ae +++ b/bench/broad_phase.ae @@ -61,11 +61,11 @@ main() { jitter = 0.3 * sin(7.0 * (i as float)) c = math.vec3(1.4 * x + jitter, 1.0 + 1.4 * y, 1.4 * z + 0.3 * cos(11.0 * (i as float))) boxes[i] = AABB { lower: math.sub(c, math.vec3(0.5, 0.5, 0.5)), upper: math.add(c, math.vec3(0.5, 0.5, 0.5)) } - keys[i] = broad_phase.broad_phase_create_proxy(bp, broad_phase.BODY_DYNAMIC, math.aabb_round_out(boxes[i]), all_bits(), i, false) + keys[i] = broad_phase.create_proxy(bp, broad_phase.BODY_DYNAMIC, math.aabb_round_out(boxes[i]), all_bits(), i, false) i = i + 1 } boxes[COUNT] = AABB { lower: math.vec3(0.0 - 5.0, 0.0 - 1.0, 0.0 - 5.0), upper: math.vec3(40.0, 0.6, 40.0) } - keys[COUNT] = broad_phase.broad_phase_create_proxy(bp, broad_phase.BODY_STATIC, math.aabb_round_out(boxes[COUNT]), all_bits(), COUNT, false) + keys[COUNT] = broad_phase.create_proxy(bp, broad_phase.BODY_STATIC, math.aabb_round_out(boxes[COUNT]), all_bits(), COUNT, false) t1 = clock() first = broad_phase.update_broad_phase_pairs(bp, visitors, false) adopt(bp) @@ -78,7 +78,7 @@ main() { while i < COUNT { d = math.vec3(0.05 * sin((i + step) as float), 0.05 * cos((3 * i + step) as float), 0.05 * sin((5 * i + 2 * step) as float)) boxes[i] = AABB { lower: math.add(boxes[i].lower, d), upper: math.add(boxes[i].upper, d) } - broad_phase.broad_phase_move_proxy(bp, keys[i], math.aabb_round_out(boxes[i])) + broad_phase.move_proxy(bp, keys[i], math.aabb_round_out(boxes[i])) i = i + 1 } n = broad_phase.update_broad_phase_pairs(bp, visitors, false) diff --git a/bench/manifold.ae b/bench/manifold.ae index 179b4fc..87c9599 100644 --- a/bench/manifold.ae +++ b/bench/manifold.ae @@ -30,7 +30,7 @@ sum_separations(m: *LocalManifold) -> float { s = 0.0 k = 0 while k < m.point_count { - s = s + manifold.manifold_point(m, k).separation + s = s + manifold.point(m, k).separation k = k + 1 } return s diff --git a/bench/mesh_contact.ae b/bench/mesh_contact.ae index 2ebaa2e..6bcab9c 100644 --- a/bench/mesh_contact.ae +++ b/bench/mesh_contact.ae @@ -38,7 +38,7 @@ var g_arena: ptr = null run(name: string, ground: *Shape, convex: *Shape, radius: float) { arena = g_arena as *Arena - cache = mesh_contact.mesh_contact_cache_create() + cache = mesh_contact.cache_create() xf_a = math.transform_identity() clusters = 0 points = 0 @@ -74,7 +74,7 @@ run(name: string, ground: *Shape, convex: *Shape, radius: float) { } t1 = clock() println("aephysics mesh_contact: ${name} ${STEPS} steps ${ms(t1 - t0)} ms (${clusters} clusters, ${points} points, separation sum ${separation_sum}, ${hits} cache hits)") - mesh_contact.mesh_contact_cache_destroy(&cache) + mesh_contact.cache_destroy(&cache) } main() { diff --git a/bench/shape.ae b/bench/shape.ae index aa09cdd..b14ad9e 100644 --- a/bench/shape.ae +++ b/bench/shape.ae @@ -81,7 +81,7 @@ main() { local = math.vec3(2.5 * cos(5.0 * a), 4.0, 1.2 * sin(5.0 * a)) start[0] = math.transform_point(transform, local) translation = math.rotate_vector(q, math.vec3(0.0, 0.0 - 8.0, 0.5 * sin(9.0 * a))) - out = shape.shape_cast_shape(&sh, transform, distance.shape_proxy(start_block, 1, 0.3), translation, 1.0, false) + out = shape.cast_shape(&sh, transform, distance.shape_proxy(start_block, 1, 0.3), translation, 1.0, false) if out.hit { cast_hits = cast_hits + 1 cast_sum = cast_sum + out.fraction diff --git a/bench/suite.ae b/bench/suite.ae index ae94291..058aabe 100644 --- a/bench/suite.ae +++ b/bench/suite.ae @@ -709,8 +709,8 @@ struct SatPass { } sat_pass(a: *HullData, b: *HullData, transforms: Transform[], caches: SimplexCache[], warm: bool) -> SatPass { - input = DistanceInput { proxy_a: ShapeProxy { points: hull.hull_points(a) as ptr, count: a.vertex_count, radius: 0.0 }, - proxy_b: ShapeProxy { points: hull.hull_points(b) as ptr, count: b.vertex_count, radius: 0.0 }, + input = DistanceInput { proxy_a: ShapeProxy { points: hull.points(a) as ptr, count: a.vertex_count, radius: 0.0 }, + proxy_b: ShapeProxy { points: hull.points(b) as ptr, count: b.vertex_count, radius: 0.0 }, transform: math.transform_identity(), use_radii: false } sum = 0.0 t0 = clock() @@ -755,8 +755,8 @@ sat_pair(kind_a: int, kind_b: int) { caches_block = calloc(SAT_PAIRS, sizeof(SimplexCache)) transforms = transforms_block as Transform[] caches = caches_block as SimplexCache[] - input = DistanceInput { proxy_a: ShapeProxy { points: hull.hull_points(a) as ptr, count: a.vertex_count, radius: 0.0 }, - proxy_b: ShapeProxy { points: hull.hull_points(b) as ptr, count: b.vertex_count, radius: 0.0 }, + input = DistanceInput { proxy_a: ShapeProxy { points: hull.points(a) as ptr, count: a.vertex_count, radius: 0.0 }, + proxy_b: ShapeProxy { points: hull.points(b) as ptr, count: b.vertex_count, radius: 0.0 }, transform: math.transform_identity(), use_radii: false } // Each pair placed half the speculative distance apart, as the // reference places them: from far out along a random direction, moved diff --git a/bench/triangle.ae b/bench/triangle.ae index 80ea8a7..0ed44a5 100644 --- a/bench/triangle.ae +++ b/bench/triangle.ae @@ -31,7 +31,7 @@ sum_separations(m: *LocalManifold) -> float { s = 0.0 k = 0 while k < m.point_count { - s = s + manifold.manifold_point(m, k).separation + s = s + manifold.point(m, k).separation k = k + 1 } return s diff --git a/tests/test_basics.ae b/tests/test_basics.ae index 07639be..17ffcbe 100644 --- a/tests/test_basics.ae +++ b/tests/test_basics.ae @@ -59,8 +59,8 @@ name_cache_unit() { ensure("names: the same name, the same id, one entry", basics.name_cache_add(&c, "crate") == crate && basics.name_cache_count(&c) == 2) // The id is the reference's b3Hash32 of the bytes (FNV-1a, Murmur3's // finish), its values from the reference's own function compiled in C. - ensure("names: the reference's hash of crate", basics.name_hash32(string.aether_string_raw_ptr("crate"), 5) == (3832558763 as long) && crate == 0 - 462408533) - ensure("names: the reference's hash of barrel", basics.name_hash32(string.aether_string_raw_ptr("barrel"), 6) == (2907559280 as long) && barrel == 0 - 1387408016) + ensure("names: the reference's hash of crate", basics.name_hash32((string.bytes("crate") as ptr), 5) == (3832558763 as long) && crate == 0 - 462408533) + ensure("names: the reference's hash of barrel", basics.name_hash32((string.bytes("barrel") as ptr), 6) == (2907559280 as long) && barrel == 0 - 1387408016) long_block = calloc(4096, 1) long_bytes = long_block as byte[] i = 0 @@ -76,7 +76,7 @@ name_cache_unit() { // Another name on a taken id: loaded under crate's id first, "crate" // itself then probes past it. d = basics.name_cache_create() - other = string.aether_string_raw_ptr("other") + other = (string.bytes("other") as ptr) ensure("names: loaded with its id", basics.name_cache_load(&d, crate, other, 5)) moved = basics.name_cache_add(&d, "crate") ensure("names: a collision takes the next id", moved == crate + 1 && string.equals(basics.name_cache_text(&d, crate), "other") == 1 && string.equals(basics.name_cache_text(&d, moved), "crate") == 1) @@ -84,7 +84,7 @@ name_cache_unit() { ensure("names: a taken id refused", basics.name_cache_load(&d, crate, other, 5) == false) ensure("names: the null id refused", basics.name_cache_load(&d, basics.NULL_NAME, other, 5) == false) ensure("names: an empty name refused", basics.name_cache_load(&d, 77, other, 0) == false) - ensure("names: a zero inside refused", basics.name_cache_load(&d, 78, string.aether_string_raw_ptr("crate"), 6) == false) + ensure("names: a zero inside refused", basics.name_cache_load(&d, 78, (string.bytes("crate") as ptr), 6) == false) basics.name_cache_destroy(&d) basics.name_cache_destroy(&c) ensure("names: nothing left (${basics.alloc_count() - before} blocks, ${basics.bytes_in_use() - before_bytes} bytes)", basics.alloc_count() == before && basics.bytes_in_use() == before_bytes) diff --git a/tests/test_broad_phase.ae b/tests/test_broad_phase.ae index 2c29ce8..d61a4db 100644 --- a/tests/test_broad_phase.ae +++ b/tests/test_broad_phase.ae @@ -186,15 +186,15 @@ test_grid() { while i < 36 { boxes[i] = box_at(math.vec3(1.5 * ((i % 6) as float), 1.0, 1.5 * ((i / 6) as float)), 0.5) bodies[i] = i - keys[i] = broad_phase.broad_phase_create_proxy(bp, broad_phase.BODY_DYNAMIC, math.aabb_round_out(boxes[i]), all_bits(), i, false) + keys[i] = broad_phase.create_proxy(bp, broad_phase.BODY_DYNAMIC, math.aabb_round_out(boxes[i]), all_bits(), i, false) i = i + 1 } boxes[36] = AABB { lower: math.vec3(0.0 - 2.0, 0.0 - 1.0, 0.0 - 2.0), upper: math.vec3(10.0, 0.6, 10.0) } bodies[36] = 36 - keys[36] = broad_phase.broad_phase_create_proxy(bp, broad_phase.BODY_STATIC, math.aabb_round_out(boxes[36]), all_bits(), 36, false) + keys[36] = broad_phase.create_proxy(bp, broad_phase.BODY_STATIC, math.aabb_round_out(boxes[36]), all_bits(), 36, false) boxes[37] = AABB { lower: math.vec3(0.0 - 2.0, 0.8, 0.0 - 0.6), upper: math.vec3(10.0, 1.2, 0.0 - 0.4) } bodies[37] = 37 - keys[37] = broad_phase.broad_phase_create_proxy(bp, broad_phase.BODY_KINEMATIC, math.aabb_round_out(boxes[37]), all_bits(), 37, false) + keys[37] = broad_phase.create_proxy(bp, broad_phase.BODY_KINEMATIC, math.aabb_round_out(boxes[37]), all_bits(), 37, false) ensure("proxy kinds", broad_phase.proxy_type(keys[0]) == broad_phase.BODY_DYNAMIC && broad_phase.proxy_type(keys[36]) == broad_phase.BODY_STATIC && broad_phase.proxy_type(keys[37]) == broad_phase.BODY_KINEMATIC) ensure("shape index round trip", broad_phase.broad_phase_get_shape_index(bp, keys[36]) == 36 && broad_phase.broad_phase_get_shape_index(bp, keys[17]) == 17) ensure("test overlap", broad_phase.broad_phase_test_overlap(bp, keys[0], keys[36]) && broad_phase.broad_phase_test_overlap(bp, keys[0], keys[1]) == false) @@ -217,19 +217,19 @@ test_grid() { ensure("quiet update", n == 0) // A proxy moved onto its neighbour reports only the new pair, not the ones it has. boxes[8] = box_at(math.vec3(1.5 * 2.0 - 0.7, 1.0, 1.5), 0.5) - broad_phase.broad_phase_move_proxy(bp, keys[8], math.aabb_round_out(boxes[8])) + broad_phase.move_proxy(bp, keys[8], math.aabb_round_out(boxes[8])) expected = brute_force_count(scene, bp) n = broad_phase.update_broad_phase_pairs(bp, visitors(scene), false) ensure("move finds the new pair (${n} of ${expected})", n == expected && n == 1) ensure("the new pair", broad_phase.pair_key_shape_a(broad_phase.pair_key(bp, 0)) == 7 && broad_phase.pair_key_shape_b(broad_phase.pair_key(bp, 0)) == 8) adopt_keys(bp) // Marking a proxy moved without moving it finds nothing new. - broad_phase.broad_phase_mark_proxy_moved(bp, keys[8], math.aabb_round_out(boxes[8])) + broad_phase.mark_proxy_moved(bp, keys[8], math.aabb_round_out(boxes[8])) n = broad_phase.update_broad_phase_pairs(bp, visitors(scene), false) ensure("a mark alone finds nothing", n == 0) // A contact ends: its pair leaves the set, and the next move finds it again. broad_phase.remove_pair(bp, basics.shape_pair_key(7, 8, 0)) - broad_phase.broad_phase_move_proxy(bp, keys[7], math.aabb_round_out(boxes[7])) + broad_phase.move_proxy(bp, keys[7], math.aabb_round_out(boxes[7])) n = broad_phase.update_broad_phase_pairs(bp, visitors(scene), false) ensure("a removed pair is found again", n == 1 && broad_phase.pair_key_shape_a(broad_phase.pair_key(bp, 0)) == 7) adopt_keys(bp) @@ -237,7 +237,7 @@ test_grid() { i = 12 while i < 18 { boxes[i] = box_at(math.vec3(0.7 * ((i - 12) as float), 1.0, 0.0 - 0.3), 0.5) - broad_phase.broad_phase_move_proxy(bp, keys[i], math.aabb_round_out(boxes[i])) + broad_phase.move_proxy(bp, keys[i], math.aabb_round_out(boxes[i])) i = i + 1 } expected = brute_force_count(scene, bp) @@ -248,11 +248,11 @@ test_grid() { // The filter: with the odd shapes refused, a move of an even one onto an odd finds nothing. scene.filter_odd = true boxes[20] = box_at(math.vec3(1.5 * 3.0 - 0.7, 1.0, 1.5 * 3.0), 0.5) - broad_phase.broad_phase_move_proxy(bp, keys[20], math.aabb_round_out(boxes[20])) + broad_phase.move_proxy(bp, keys[20], math.aabb_round_out(boxes[20])) n = broad_phase.update_broad_phase_pairs(bp, visitors(scene), false) ensure("the filter refuses", n == 0) scene.filter_odd = false - broad_phase.broad_phase_move_proxy(bp, keys[20], math.aabb_round_out(boxes[20])) + broad_phase.move_proxy(bp, keys[20], math.aabb_round_out(boxes[20])) n = broad_phase.update_broad_phase_pairs(bp, visitors(scene), false) ensure("the same move without the filter", n == 1 && broad_phase.pair_key_shape_a(broad_phase.pair_key(bp, 0)) == 20 && broad_phase.pair_key_shape_b(broad_phase.pair_key(bp, 0)) == 21) adopt_keys(bp) @@ -260,34 +260,34 @@ test_grid() { bodies[21] = 22 bodies[22] = 22 boxes[21] = box_at(math.vec3(1.5 * 4.0 - 0.7, 1.0, 1.5 * 3.0), 0.5) - broad_phase.broad_phase_move_proxy(bp, keys[21], math.aabb_round_out(boxes[21])) + broad_phase.move_proxy(bp, keys[21], math.aabb_round_out(boxes[21])) n = broad_phase.update_broad_phase_pairs(bp, visitors(scene), false) ensure("one body's shapes do not pair", n == 0) // A static proxy forced into pairs finds the dynamic boxes over it. boxes[36] = AABB { lower: math.vec3(0.0 - 2.0, 0.0 - 1.0, 0.0 - 2.0), upper: math.vec3(10.0, 0.6, 10.0) } broad_phase.remove_pair(bp, basics.shape_pair_key(0, 36, 0)) broad_phase.remove_pair(bp, basics.shape_pair_key(1, 36, 0)) - broad_phase.broad_phase_destroy_proxy(bp, keys[36]) - keys[36] = broad_phase.broad_phase_create_proxy(bp, broad_phase.BODY_STATIC, math.aabb_round_out(boxes[36]), all_bits(), 36, true) + broad_phase.destroy_proxy(bp, keys[36]) + keys[36] = broad_phase.create_proxy(bp, broad_phase.BODY_STATIC, math.aabb_round_out(boxes[36]), all_bits(), 36, true) n = broad_phase.update_broad_phase_pairs(bp, visitors(scene), false) ensure("a forced static proxy pairs (${n})", n == 2) adopt_keys(bp) // A static proxy created without the force finds nothing until a dynamic one moves onto it. broad_phase.remove_pair(bp, basics.shape_pair_key(0, 36, 0)) - broad_phase.broad_phase_destroy_proxy(bp, keys[36]) - keys[36] = broad_phase.broad_phase_create_proxy(bp, broad_phase.BODY_STATIC, math.aabb_round_out(boxes[36]), all_bits(), 36, false) + broad_phase.destroy_proxy(bp, keys[36]) + keys[36] = broad_phase.create_proxy(bp, broad_phase.BODY_STATIC, math.aabb_round_out(boxes[36]), all_bits(), 36, false) n = broad_phase.update_broad_phase_pairs(bp, visitors(scene), false) ensure("a quiet static proxy", n == 0) - broad_phase.broad_phase_move_proxy(bp, keys[0], math.aabb_round_out(boxes[0])) + broad_phase.move_proxy(bp, keys[0], math.aabb_round_out(boxes[0])) n = broad_phase.update_broad_phase_pairs(bp, visitors(scene), false) ensure("then the mover finds it", n == 1 && broad_phase.pair_key_shape_b(broad_phase.pair_key(bp, 0)) == 36) adopt_keys(bp) // A destroyed proxy is found by no one. - broad_phase.broad_phase_destroy_proxy(bp, keys[7]) + broad_phase.destroy_proxy(bp, keys[7]) keys[7] = 0 - 1 boxes[7] = AABB { lower: math.vec3(100.0, 100.0, 100.0), upper: math.vec3(100.0, 100.0, 100.0) } boxes[6] = box_at(math.vec3(1.5, 1.0, 1.5), 0.5) - broad_phase.broad_phase_move_proxy(bp, keys[6], math.aabb_round_out(boxes[6])) + broad_phase.move_proxy(bp, keys[6], math.aabb_round_out(boxes[6])) n = broad_phase.update_broad_phase_pairs(bp, visitors(scene), false) ensure("a destroyed proxy is not found", keys_are_overlaps(scene, bp)) ensure("trees valid at the end", broad_phase.validate_broad_phase(bp)) @@ -335,8 +335,8 @@ test_compound_pairs() { // The box straddles the children at local x 0 and 3 (world y 10 and 13), not the one at 6. boxes[0] = AABB { lower: math.vec3(0.0 - 0.6, 9.5, 0.0 - 0.6), upper: math.vec3(0.6, 13.2, 0.6) } boxes[1] = compound.compute_compound_aabb(c, scene.compound_transform) - keys[0] = broad_phase.broad_phase_create_proxy(bp, broad_phase.BODY_DYNAMIC, math.aabb_round_out(boxes[0]), all_bits(), 0, false) - keys[1] = broad_phase.broad_phase_create_proxy(bp, broad_phase.BODY_STATIC, math.aabb_round_out(boxes[1]), all_bits(), 1, false) + keys[0] = broad_phase.create_proxy(bp, broad_phase.BODY_DYNAMIC, math.aabb_round_out(boxes[0]), all_bits(), 0, false) + keys[1] = broad_phase.create_proxy(bp, broad_phase.BODY_STATIC, math.aabb_round_out(boxes[1]), all_bits(), 1, false) n = broad_phase.update_broad_phase_pairs(bp, visitors(scene), true) ensure("compound pairs (${n})", n == 2) if n == 2 { @@ -348,13 +348,13 @@ test_compound_pairs() { adopt_keys(bp) // Without the compound flag the pair is plain; with it and the pair set holding one child, only the other comes. broad_phase.remove_pair(bp, basics.shape_pair_key(0, 1, 1)) - broad_phase.broad_phase_move_proxy(bp, keys[0], math.aabb_round_out(boxes[0])) + broad_phase.move_proxy(bp, keys[0], math.aabb_round_out(boxes[0])) n = broad_phase.update_broad_phase_pairs(bp, visitors(scene), true) ensure("compound child already paired is skipped", n == 1 && broad_phase.pair_key_child(broad_phase.pair_key(bp, 0)) == 1) // The gauntlet refusing the pair drops every child. bodies[1] = 0 broad_phase.remove_pair(bp, basics.shape_pair_key(0, 1, 0)) - broad_phase.broad_phase_move_proxy(bp, keys[0], math.aabb_round_out(boxes[0])) + broad_phase.move_proxy(bp, keys[0], math.aabb_round_out(boxes[0])) n = broad_phase.update_broad_phase_pairs(bp, visitors(scene), true) ensure("compound pair refused drops every child", n == 0) broad_phase.destroy_broad_phase(bp) diff --git a/tests/test_collision.ae b/tests/test_collision.ae index 067aab8..f7cdd62 100644 --- a/tests/test_collision.ae +++ b/tests/test_collision.ae @@ -113,7 +113,7 @@ test_large_world_manifold() { ensure("large world manifold: four points at the origin", origin.point_count == 4) i = 0 while i < origin.point_count { - small("large world manifold: a tenth deep", manifold.manifold_point(&origin, i).separation + 0.1, 0.01) + small("large world manifold: a tenth deep", manifold.point(&origin, i).separation + 0.1, 0.01) i = i + 1 } base = math.vec3(10000000.0, 10000000.0, 10000000.0) @@ -127,8 +127,8 @@ test_large_world_manifold() { small("large world manifold: the normal", math.length(math.sub(far.normal, origin.normal)), 0.0001) i = 0 while i < far.point_count { - p = manifold.manifold_point(&far, i) - q = manifold.manifold_point(&origin, i) + p = manifold.point(&far, i) + q = manifold.point(&origin, i) small("large world manifold: the separation", p.separation - q.separation, 0.0001) small("large world manifold: the point", math.length(math.sub(p.point, q.point)), 0.0001) i = i + 1 diff --git a/tests/test_compound.ae b/tests/test_compound.ae index 4e2459e..b72ea03 100644 --- a/tests/test_compound.ae +++ b/tests/test_compound.ae @@ -558,7 +558,7 @@ test_many_mesh_materials() { ensure("six materials in the table", c.material_count == 6) child = compound.get_compound_child(c, 0) ensure("the child carries all six", child.material_count == 6) - triangle_materials = mesh.mesh_material_indices(md) + triangle_materials = mesh.material_indices(md) seen_block = calloc(6, 4) seen = seen_block as int[] all_match = true @@ -806,7 +806,7 @@ test_through_shape() { ensure("compound ray sphere material", compound.get_compound_material(c, out.material_index).user_material_id == (6 as long)) // A shape cast of a small sphere down onto the hull. point_block = calloc(1, sizeof(Vec3)) - out = shape.shape_cast_shape(&sh, t, point_proxy(point_block, math.vec3(10.0, 30.0, 30.0), 0.5), math.vec3(0.0, 0.0 - 8.0, 0.0), 1.0, false) + out = shape.cast_shape(&sh, t, point_proxy(point_block, math.vec3(10.0, 30.0, 30.0), 0.5), math.vec3(0.0, 0.0 - 8.0, 0.0), 1.0, false) ensure("compound shape cast hit", out.hit && out.child_index == 0) small("compound shape cast fraction", out.fraction - 3.5 / 8.0, 0.001) small("compound shape cast normal", out.normal.y - 1.0, 0.001) @@ -1133,7 +1133,7 @@ test_serialize_big_hulls() { return } ensure("big hulls: past 128 half-edges (${cylinder.edge_count}, ${complex.edge_count})", cylinder.edge_count > 128 && complex.edge_count > 128) - ensure("big hulls: each valid as bytes", hull.hull_bytes_valid(cylinder, cylinder.byte_count) && hull.hull_bytes_valid(complex, complex.byte_count)) + ensure("big hulls: each valid as bytes", hull.bytes_valid(cylinder, cylinder.byte_count) && hull.bytes_valid(complex, complex.byte_count)) mat = material.default_surface_material() hulls_block = calloc(2, sizeof(CompoundHullDef)) defs = hulls_block as CompoundHullDef[] @@ -1176,7 +1176,7 @@ corrupt_refused(good: ptr, byte_count: int, what: int) -> bool { else if what == 4 { instances = (copy + c.hull_offset) as HullInstance[] h = (copy + instances[0].hull_offset) as *HullData - edges = hull.hull_edges(h) + edges = hull.edges(h) edges[3].twin = h.edge_count + 5 } else if what == 5 { diff --git a/tests/test_dynamics.ae b/tests/test_dynamics.ae index 711d6e7..8aaeea2 100644 --- a/tests/test_dynamics.ae +++ b/tests/test_dynamics.ae @@ -434,8 +434,8 @@ test_sets_and_shapes() { ensure("static shape in the static tree", broad_phase.proxy_type(static_shape.proxy_key) == broad_phase.BODY_STATIC) awake_shape = dynamics.get_shape_full_id(w, s2) ensure("dynamic shape in the dynamic tree", broad_phase.proxy_type(awake_shape.proxy_key) == broad_phase.BODY_DYNAMIC) - ensure("dynamic tree holds three", dynamic_tree.tree_get_proxy_count(broad_phase.broad_phase_tree(&w.broad_phase, broad_phase.BODY_DYNAMIC)) == 3) - ensure("kinematic tree holds one", dynamic_tree.tree_get_proxy_count(broad_phase.broad_phase_tree(&w.broad_phase, broad_phase.BODY_KINEMATIC)) == 1) + ensure("dynamic tree holds three", dynamic_tree.tree_get_proxy_count(broad_phase.tree(&w.broad_phase, broad_phase.BODY_DYNAMIC)) == 3) + ensure("kinematic tree holds one", dynamic_tree.tree_get_proxy_count(broad_phase.tree(&w.broad_phase, broad_phase.BODY_KINEMATIC)) == 1) // The awake body's mass came from both shapes (water density by default). small("awake body mass", dynamics.body_get_mass(awake_id) - 1000.0 * (1.0 + (4.0 / 3.0) * math.PI * 0.015625), 0.001) ensure("shape bounds hold the sphere", dynamics.shape_get_aabb(s2).upper.x > 1.24 && dynamics.shape_get_aabb(s2).lower.x < 0.76) @@ -510,7 +510,7 @@ test_sets_and_shapes() { ensure("valid after the disabled", valid(world_id)) dynamics.destroy_body(awake_id) ensure("valid after the awake", valid(world_id)) - ensure("proxies gone with the bodies", dynamic_tree.tree_get_proxy_count(broad_phase.broad_phase_tree(&w.broad_phase, broad_phase.BODY_DYNAMIC)) == 1) + ensure("proxies gone with the bodies", dynamic_tree.tree_get_proxy_count(broad_phase.tree(&w.broad_phase, broad_phase.BODY_DYNAMIC)) == 1) dynamics.destroy_body(static_id) dynamics.destroy_body(asleep_id) ensure("no bodies, no islands", dynamics.world_body_count(w) == 0 && dynamics.world_island_count(w) == 0 && dynamics.world_shape_count(w) == 0) @@ -1213,7 +1213,7 @@ test_shape_filter() { ensure("filter: no pair under the new filter (${dynamics.world_contact_count(w)})", dynamics.world_contact_count(w) == 1) g_tree_hits = 0 box = AABB { lower: math.vec3(0.0 - 5.0, 0.0 - 5.0, 0.0 - 5.0), upper: math.vec3(5.0, 5.0, 5.0) } - dynamic_tree.tree_query(broad_phase.broad_phase_tree(&w.broad_phase, dynamics.BODY_DYNAMIC), box, 2 as long, false, count_tree_hit, null) + dynamic_tree.tree_query(broad_phase.tree(&w.broad_phase, dynamics.BODY_DYNAMIC), box, 2 as long, false, count_tree_hit, null) ensure("filter: the tree queries it under its new category (${g_tree_hits})", g_tree_hits == 1) ensure("filter: valid", all_valid(world_id)) dynamics.shape_set_filter(shapes[0], old) diff --git a/tests/test_edge_cases.ae b/tests/test_edge_cases.ae index 09b7b2c..6e73197 100644 --- a/tests/test_edge_cases.ae +++ b/tests/test_edge_cases.ae @@ -647,7 +647,7 @@ mesh_of(vertices: ptr, indices: ptr, materials: ptr) -> *MeshData { } field_of(heights: ptr, scale: Vec3, lower: float, upper: float) -> *HeightFieldData { - hdef = height_field.height_field_def() + hdef = height_field.def() hdef.heights = heights hdef.count_x = 2 hdef.count_z = 2 diff --git a/tests/test_height_field.ae b/tests/test_height_field.ae index eeb0c7c..a98db08 100644 --- a/tests/test_height_field.ae +++ b/tests/test_height_field.ae @@ -43,7 +43,7 @@ small(name: string, value: float, tolerance: float) { // A flat 3 x 3 field at y = 0 with the given cell materials. make_flat_field(materials: ptr, clockwise: bool) -> *HeightFieldData { heights = calloc(9, 8) - def = height_field.height_field_def() + def = height_field.def() def.heights = heights def.material_indices = materials def.scale = math.vec3(1.0, 1.0, 1.0) @@ -127,7 +127,7 @@ test_create() { heights = heights_block as float[] heights[4] = 0.75 heights[8] = 3.0 - def = height_field.height_field_def() + def = height_field.def() def.heights = heights_block def.scale = math.vec3(2.0, 0.5, 2.0) def.count_x = 3 @@ -157,7 +157,7 @@ test_flags() { materials = materials_block as int[] materials[4] = height_field.HEIGHT_FIELD_HOLE heights = calloc(16, 8) - def = height_field.height_field_def() + def = height_field.def() def.heights = heights def.material_indices = materials_block def.count_x = 4 @@ -208,7 +208,7 @@ test_ray_and_overlap() { // A flat field with a tight range recovers y = 0 to a hundred-thousandth. heights = calloc(16, 8) materials = calloc(9, 4) - def = height_field.height_field_def() + def = height_field.def() def.heights = heights def.material_indices = materials def.count_x = 4 @@ -295,7 +295,7 @@ brute_force_shape_cast(h: *HeightFieldData, proxy: ShapeProxy, translation: Vec3 best = distance.empty_cast_output() best_fraction = 1.0 tri = tri_block as Vec3[] - materials = height_field.height_field_material_indices(h) + materials = height_field.material_indices(h) count = height_field.height_field_triangle_count(h) t = 0 while t < count { @@ -321,7 +321,7 @@ brute_force_shape_cast(h: *HeightFieldData, proxy: ShapeProxy, translation: Vec3 brute_force_ray_cast(h: *HeightFieldData, origin: Vec3, translation: Vec3) -> CastOutput { best = distance.empty_cast_output() best_fraction = 1.0 - materials = height_field.height_field_material_indices(h) + materials = height_field.material_indices(h) count = height_field.height_field_triangle_count(h) t = 0 while t < count { @@ -637,7 +637,7 @@ test_file_roundtrip() { materials[1] = height_field.HEIGHT_FIELD_HOLE materials[2] = 1 materials[3] = 2 - def = height_field.height_field_def() + def = height_field.def() def.heights = heights_block def.material_indices = materials_block def.scale = math.vec3(1.5, 2.0, 0.75) @@ -661,7 +661,7 @@ test_file_roundtrip() { small("file: scale z", loaded.scale.z - def.scale.z, 0.0000002) small("file: the lowest height", loaded.min_height - def.global_minimum_height, 0.0000002) small("file: the highest height", loaded.max_height - def.global_maximum_height, 0.0000002) - cells = height_field.height_field_material_indices(loaded) + cells = height_field.material_indices(loaded) i = 0 while i < 4 { ensure("file: cell ${i}'s material", (cells[i] as int) == materials[i]) @@ -690,7 +690,7 @@ test_file_roundtrip() { ensure("file: the reference's text loads", theirs != null) if theirs != null { ensure("file: its counts and winding", theirs.row_count == 3 && theirs.column_count == 3 && theirs.clockwise) - cells = height_field.height_field_material_indices(theirs) + cells = height_field.material_indices(theirs) ensure("file: its materials", (cells[0] as int) == 0 && (cells[1] as int) == 255 && (cells[2] as int) == 1 && (cells[3] as int) == 2) quanta = height_field.height_field_quanta(theirs) quantum = 2.0 / 65535.0 @@ -725,7 +725,7 @@ test_file_roundtrip() { good = text_field("2 2\n1 1 1\n-1 1\n0\n0\n0.5\n0\n0\n7\n") ensure("file: the smallest good file loads", good != null && good.row_count == 2 && good.column_count == 2) if good != null { height_field.destroy_height_field(good) } - empty = height_field.height_field_def() + empty = height_field.def() ensure("file: a definition without heights is not dumped", files.dump_height_data(&empty, path) == false) free(heights_block) free(materials_block) diff --git a/tests/test_host_api.ae b/tests/test_host_api.ae index d9ee13d..d90d962 100644 --- a/tests/test_host_api.ae +++ b/tests/test_host_api.ae @@ -268,8 +268,8 @@ main() { ensure("version: the reference's 0.2.0", version.major == 0 && version.minor == 2 && version.revision == 0) ensure("precision: world positions are double", physics_world.is_double_precision()) ensure("avx2: the CPU's", physics_world.is_avx2_available() == native.has_avx2()) - ensure("hash: the reference's b3Hash of 13 bytes", basics.djb2_hash(basics.HASH_INIT as long, string.aether_string_raw_ptr("hello physics"), 13) == (4027293447 as long)) - ensure("hash: of 16 bytes", basics.djb2_hash(basics.HASH_INIT as long, string.aether_string_raw_ptr("0123456789abcdef"), 16) == (1204685188 as long)) + ensure("hash: the reference's b3Hash of 13 bytes", basics.djb2_hash(basics.HASH_INIT as long, (string.bytes("hello physics") as ptr), 13) == (4027293447 as long)) + ensure("hash: of 16 bytes", basics.djb2_hash(basics.HASH_INIT as long, (string.bytes("0123456789abcdef") as ptr), 16) == (1204685188 as long)) ensure("hash: of none", basics.djb2_hash(basics.HASH_INIT as long, null, 0) == (5381 as long)) println("host api: ${checks} checks") diff --git a/tests/test_hull.ae b/tests/test_hull.ae index c7d6958..8b9ba10 100644 --- a/tests/test_hull.ae +++ b/tests/test_hull.ae @@ -486,7 +486,7 @@ corner_sign(i: int) -> Vec3 { check_transformed_box(name: string, h: Vec3, xf: Transform) { box = hull.make_transformed_box_hull(h.x, h.y, h.z, xf) tol = 0.00001 - points = hull.hull_points(box) + points = hull.points(box) i = 0 while i < 8 { expected = math.transform_point(xf, math.mul(corner_sign(i), h)) @@ -495,7 +495,7 @@ check_transformed_box(name: string, h: Vec3, xf: Transform) { small("${name}: corner ${i} z", points[i].z - expected.z, tol) i = i + 1 } - planes = hull.hull_planes(box) + planes = hull.planes(box) i = 0 while i < 6 { local_normal = math.vec3_axis_z() @@ -549,10 +549,10 @@ test_transformed_box() { ensure("ray: from inside hits at the origin", inside.hit && inside.fraction == 0.0) // The support vertex along +x+y+z is the (1,1,1) corner; the shadow along x is 4. sv = hull.find_hull_support_vertex(box, math.vec3(1.0, 1.0, 1.0)) - points = hull.hull_points(box) + points = hull.points(box) small("support vertex", points[sv].x + points[sv].y + points[sv].z - 3.0, 0.00001) sf = hull.find_hull_support_face(box, math.vec3(0.0, 1.0, 0.0)) - planes = hull.hull_planes(box) + planes = hull.planes(box) small("support face", planes[sf].normal.y - 1.0, 0.00001) small("projected area", hull.compute_hull_projected_area(box, math.vec3(1.0, 0.0, 0.0)) - 4.0, 0.00001) extent = hull.compute_hull_extent(box, math.vec3_zero()) @@ -739,7 +739,7 @@ test_overlap_planes() { p[1] = math.add(p[0], math.vec3(0.5 * cos(t), 0.4 * sin(2.0 * t), 0.3)) proxy = distance.shape_proxy(points_block, 1 + k % 2, 0.05 + 0.2 * math.abs_float(cos(3.1 * t))) quick = hull.overlap_hull(box, at, proxy) - input = DistanceInput { proxy_a: hull.hull_proxy(box), proxy_b: proxy, transform: math.inv_mul_transforms(at, math.transform_identity()), use_radii: true } + input = DistanceInput { proxy_a: hull.proxy(box), proxy_b: proxy, transform: math.inv_mul_transforms(at, math.transform_identity()), use_radii: true } cache = distance.empty_cache() exact = distance.shape_distance(&input, &cache, null, 0).distance < 0.1 * math.LINEAR_SLOP if quick != exact { disagree = disagree + 1 } diff --git a/tests/test_manifold.ae b/tests/test_manifold.ae index 2291e32..092a825 100644 --- a/tests/test_manifold.ae +++ b/tests/test_manifold.ae @@ -59,7 +59,7 @@ min_separation(m: *LocalManifold) -> float { s = math.MAX_FLOAT i = 0 while i < m.point_count { - s = math.min_float(s, manifold.manifold_point(m, i).separation) + s = math.min_float(s, manifold.point(m, i).separation) i = i + 1 } return s @@ -69,14 +69,14 @@ min_separation(m: *LocalManifold) -> float { // The projection gap min over B minus max over A along a unit axis from A to B. sep_along(hull_a: *HullData, hull_b: *HullData, xf_b: Transform, n: Vec3) -> float { - points_a = hull.hull_points(hull_a) + points_a = hull.points(hull_a) max_a = 0.0 - math.MAX_FLOAT i = 0 while i < hull_a.vertex_count { max_a = math.max_float(max_a, math.dot(n, points_a[i])) i = i + 1 } - points_b = hull.hull_points(hull_b) + points_b = hull.points(hull_b) min_b = math.MAX_FLOAT i = 0 while i < hull_b.vertex_count { @@ -89,14 +89,14 @@ sep_along(hull_a: *HullData, hull_b: *HullData, xf_b: Transform, n: Vec3) -> flo // Every face normal and every edge cross product, both ways: the true maximum. oracle_separation(hull_a: *HullData, hull_b: *HullData, xf_b: Transform) -> float { best = 0.0 - math.MAX_FLOAT - planes_a = hull.hull_planes(hull_a) + planes_a = hull.planes(hull_a) i = 0 while i < hull_a.face_count { best = math.max_float(best, sep_along(hull_a, hull_b, xf_b, planes_a[i].normal)) best = math.max_float(best, sep_along(hull_a, hull_b, xf_b, math.neg(planes_a[i].normal))) i = i + 1 } - planes_b = hull.hull_planes(hull_b) + planes_b = hull.planes(hull_b) i = 0 while i < hull_b.face_count { n = math.rotate_vector(xf_b.q, planes_b[i].normal) @@ -104,10 +104,10 @@ oracle_separation(hull_a: *HullData, hull_b: *HullData, xf_b: Transform) -> floa best = math.max_float(best, sep_along(hull_a, hull_b, xf_b, math.neg(n))) i = i + 1 } - edges_a = hull.hull_edges(hull_a) - points_a = hull.hull_points(hull_a) - edges_b = hull.hull_edges(hull_b) - points_b = hull.hull_points(hull_b) + edges_a = hull.edges(hull_a) + points_a = hull.points(hull_a) + edges_b = hull.edges(hull_b) + points_b = hull.points(hull_b) i = 0 while i < hull_a.edge_count { dir_a = math.sub(points_a[edges_a[i + 1].origin], points_a[edges_a[i].origin]) @@ -150,9 +150,9 @@ test_sat_fixed() { small("sat face a: separation", q.separation - 0.2, 0.00001) small("sat face a: normal x", q.normal.x - 1.0, 0.000001) small("sat face a: normal y", q.normal.y, 0.000001) - planes_a = hull.hull_planes(a) + planes_a = hull.planes(a) small("sat face a: the +x face", planes_a[q.index_a].normal.x - 1.0, 0.000001) - points_b = hull.hull_points(b) + points_b = hull.points(b) small("sat face a: B's -x vertex", points_b[q.index_b].x + 0.5, 0.000001) small("sat face a: consistent", sep_along(a, b, xf, q.normal) - q.separation, 0.00001) hull.destroy_hull(b) @@ -168,9 +168,9 @@ test_sat_fixed() { ensure("sat face b: kind", q.kind == manifold.AXIS_FACE_B) small("sat face b: separation", q.separation - gap, 0.00001) small("sat face b: normal x", q.normal.x - 1.0, 0.00001) - planes_b = hull.hull_planes(b) + planes_b = hull.planes(b) small("sat face b: B's -x face", planes_b[q.index_b].normal.x + 1.0, 0.00001) - points_a = hull.hull_points(a_rot) + points_a = hull.points(a_rot) small("sat face b: A's +x vertex", points_a[q.index_a].x - a_extent, 0.0001) small("sat face b: consistent", sep_along(a_rot, b, xf, q.normal) - q.separation, 0.0001) hull.destroy_hull(b) @@ -330,7 +330,7 @@ test_crossed_edge() { small("crossed: normal y", m.normal.y, 0.000001) small("crossed: normal z", m.normal.z, 0.000001) if m.point_count == 1 { - p = manifold.manifold_point(&m, 0) + p = manifold.point(&m, 0) small("crossed: separation", p.separation - expected, 0.00001) small("crossed: point x", p.point.x - 0.5 * d, 0.00001) small("crossed: point y", p.point.y, 0.00001) @@ -342,7 +342,7 @@ test_crossed_edge() { manual_cache.kind = manifold.AXIS_MANUAL_EDGE_PAIR manifold.collide_hulls(&manual, 8, a, b, slide_x(d), &manual_cache) ensure("crossed manual: one point", manual.point_count == 1) - if manual.point_count == 1 { small("crossed manual: separation", manifold.manifold_point(&manual, 0).separation - expected, 0.00001) } + if manual.point_count == 1 { small("crossed manual: separation", manifold.point(&manual, 0).separation - expected, 0.00001) } i = i + 1 } // Beyond the speculative distance: the axis, no contact. @@ -370,7 +370,7 @@ test_crossed_edge() { ensure("scale ${s}: edge pair", cache.kind == manifold.AXIS_EDGE_PAIR) small("scale ${s}: normal x", m.normal.x - 1.0, 0.000001) if m.point_count == 1 { - p = manifold.manifold_point(&m, 0) + p = manifold.point(&m, 0) small("scale ${s}: separation", p.separation - expected, tolerance) small("scale ${s}: point x", p.point.x - 0.5 * d, tolerance) small("scale ${s}: point y", p.point.y, tolerance) @@ -393,7 +393,7 @@ test_crossed_edge() { manifold.collide_hulls(&m, 8, a, b, slide_x(1.4105), &cache) ensure("edge cache: still one point", m.point_count == 1) ensure("edge cache: the cache answered", cache.separation == seeded && cache.hit == 1) - if m.point_count == 1 { small("edge cache: new separation", manifold.manifold_point(&m, 0).separation - (1.4105 - ROOT2), 0.00001) } + if m.point_count == 1 { small("edge cache: new separation", manifold.point(&m, 0).separation - (1.4105 - ROOT2), 0.00001) } small("edge cache: normal x", m.normal.x - 1.0, 0.000001) manifold.collide_hulls(&m, 8, a, b, slide_x(1.5), &cache) ensure("edge cache: past the band, no points", m.point_count == 0) @@ -407,8 +407,8 @@ test_crossed_edge() { manifold.collide_hulls(&m, 8, a, b, inside, &cache) ensure("endpoint inside: one point", m.point_count == 1) if m.point_count == 1 { - small("endpoint inside: separation", manifold.manifold_point(&m, 0).separation - (1.41 - ROOT2), 0.00001) - small("endpoint inside: point y", manifold.manifold_point(&m, 0).point.y - 0.49, 0.00001) + small("endpoint inside: separation", manifold.point(&m, 0).separation - (1.41 - ROOT2), 0.00001) + small("endpoint inside: point y", manifold.point(&m, 0).point.y - 0.49, 0.00001) } off = Transform { p: math.vec3(1.41, 0.55, 0.0), q: math.quat_identity() } m = manifold.local_manifold(points) @@ -453,7 +453,7 @@ test_parallel_edges() { bound = half_diagonal * angle + 0.00001 k = 0 while k < m.point_count { - small("parallel: separation", manifold.manifold_point(&m, k).separation + overlap, bound) + small("parallel: separation", manifold.point(&m, k).separation + overlap, bound) k = k + 1 } // Forced edge: exactly parallel finds nothing; otherwise never positive. @@ -466,7 +466,7 @@ test_parallel_edges() { } else if manual.point_count > 0 { ensure("parallel manual: one point", manual.point_count == 1) ensure("parallel manual: normal up", math.dot(manual.normal, math.vec3_axis_y()) > 0.99) - s = manifold.manifold_point(&manual, 0).separation + s = manifold.point(&manual, 0).separation ensure("parallel manual: not positive (${s})", s <= 0.0 && s >= 0.0 - overlap - half_diagonal * angle - 0.0001) } j = j + 1 @@ -546,7 +546,7 @@ test_ridge_crossing() { small("ridge deep ${i}: normal y", m.normal.y - 1.0, 0.0001) small("ridge deep ${i}: normal z", m.normal.z, 0.0001) if m.point_count == 1 { - p = manifold.manifold_point(&m, 0) + p = manifold.point(&m, 0) small("ridge deep ${i}: separation", p.separation + overlap, 0.0001) small("ridge deep ${i}: point y", p.point.y - (ridge_y - 0.5 * overlap), 0.0001) small("ridge deep ${i}: point x", p.point.x, 0.01) @@ -582,7 +582,7 @@ check_edge_contact(name: string, m: *LocalManifold, p1: Vec3, e1: Vec3, p2: Vec3 small("${name}: perpendicular to e2", math.dot(m.normal, math.normalize(e2)), normal_tol) closest = math.line_distance(p1, e1, p2, e2) expected_separation = math.dot(axis, math.sub(closest.point2, closest.point1)) - radius - p = manifold.manifold_point(m, 0) + p = manifold.point(m, 0) small("${name}: separation", p.separation - expected_separation, sep_tol) expected_point = math.mul_sv(0.5, math.add(math.mul_sub(closest.point1, radius, axis), closest.point2)) small("${name}: point x", p.point.x - expected_point.x, point_tol) @@ -591,14 +591,14 @@ check_edge_contact(name: string, m: *LocalManifold, p1: Vec3, e1: Vec3, p2: Vec3 } hull_edge_tail(h: *HullData, edge_index: int, t: Transform) -> Vec3 { - edges = hull.hull_edges(h) - points = hull.hull_points(h) + edges = hull.edges(h) + points = hull.points(h) return math.transform_point(t, points[edges[edge_index].origin]) } hull_edge_vector(h: *HullData, edge_index: int, t: Transform) -> Vec3 { - edges = hull.hull_edges(h) - points = hull.hull_points(h) + edges = hull.edges(h) + points = hull.points(h) tail = math.transform_point(t, points[edges[edge_index].origin]) head = math.transform_point(t, points[edges[edges[edge_index].twin].origin]) return math.sub(head, tail) @@ -701,12 +701,12 @@ cylinder_hull(radius: float, half_height: float) -> *HullData { // for an axis (arcs crossing past its epsilon, not near parallel). brute_edge_separation(a: *HullData, b: *HullData, xf: Transform) -> float { r = math.make_matrix_from_quat(xf.q) - planes_a = hull.hull_planes(a) - points_a = hull.hull_points(a) - edges_a = hull.hull_edges(a) - planes_b = hull.hull_planes(b) - points_b = hull.hull_points(b) - edges_b = hull.hull_edges(b) + planes_a = hull.planes(a) + points_a = hull.points(a) + edges_a = hull.edges(a) + planes_b = hull.planes(b) + points_b = hull.points(b) + edges_b = hull.edges(b) tol = manifold.PARALLEL_EDGE_TOL * manifold.PARALLEL_EDGE_TOL eps = 0.0 - 0.0001 best = 0.0 - math.MAX_FLOAT @@ -811,7 +811,7 @@ test_hull_capsule_edge_deep() { manifold.collide_hull_and_capsule(&m, 8, h, c, math.transform_identity(), &cache) ensure("capsule edge deep: one point (${i} ${j} ${k})", m.point_count == 1) if m.point_count == 1 { - ensure("capsule edge deep: penetrating", manifold.manifold_point(&m, 0).separation < 0.0) + ensure("capsule edge deep: penetrating", manifold.point(&m, 0).separation < 0.0) check_edge_contact("capsule edge deep", &m, edge_point, edge_dir, c1, math.sub(c2, c1), outward, radii[j], 0.0001, 0.0001, 0.0001) } count = count + 1 @@ -848,7 +848,7 @@ test_sphere_hull_seam() { manifold.collide_hull_and_sphere(&m, 8, h, s, math.transform_identity(), &cache) ensure("sphere seam: one point", m.point_count == 1) if m.point_count == 1 { - separation = manifold.manifold_point(&m, 0).separation + separation = manifold.point(&m, 0).separation expected = (y - 0.5) - radius small("sphere seam: separation", separation - expected, 0.00001) small("sphere seam: normal x", m.normal.x, 0.00001) @@ -878,7 +878,7 @@ test_sphere_hull_seam() { expected = (y - 0.5) - radius k = 0 while k < m.point_count { - small("capsule seam: separation", manifold.manifold_point(&m, k).separation - expected, 0.00001) + small("capsule seam: separation", manifold.point(&m, k).separation - expected, 0.00001) k = k + 1 } small("capsule seam: normal x", m.normal.x, 0.00001) @@ -919,8 +919,8 @@ test_sphere_hull_seam() { small("capsule face deep: normal z", m.normal.z, 0.00001) expected = (y - 0.5) - radii[r] if m.point_count == 2 { - small("capsule face deep: separation 0", manifold.manifold_point(&m, 0).separation - expected, 0.00001) - small("capsule face deep: separation 1", manifold.manifold_point(&m, 1).separation - expected, 0.00001) + small("capsule face deep: separation 0", manifold.point(&m, 0).separation - expected, 0.00001) + small("capsule face deep: separation 1", manifold.point(&m, 1).separation - expected, 0.00001) } ensure("capsule face deep: penetrating", expected < 0.0) face_contacts = face_contacts + 1 @@ -946,8 +946,8 @@ test_rounded_pairs() { manifold.collide_spheres(&m, 8, a, b, Transform { p: math.vec3(0.7, 0.0, 0.0), q: math.quat_identity() }) ensure("spheres: one point", m.point_count == 1) if m.point_count == 1 { - small("spheres: separation", manifold.manifold_point(&m, 0).separation + 0.05, 0.00001) - small("spheres: point at the midpoint", manifold.manifold_point(&m, 0).point.x - 0.475, 0.00001) + small("spheres: separation", manifold.point(&m, 0).separation + 0.05, 0.00001) + small("spheres: point at the midpoint", manifold.point(&m, 0).point.x - 0.475, 0.00001) } small("spheres: normal", m.normal.x - 1.0, 0.00001) m = manifold.local_manifold(points) @@ -958,7 +958,7 @@ test_rounded_pairs() { m = manifold.local_manifold(points) manifold.collide_capsule_and_sphere(&m, 8, c, b, Transform { p: math.vec3(0.3, 0.4, 0.0), q: math.quat_identity() }) ensure("capsule-sphere: one point", m.point_count == 1) - if m.point_count == 1 { small("capsule-sphere: separation", manifold.manifold_point(&m, 0).separation + 0.05, 0.00001) } + if m.point_count == 1 { small("capsule-sphere: separation", manifold.point(&m, 0).separation + 0.05, 0.00001) } small("capsule-sphere: normal y", m.normal.y - 1.0, 0.00001) // Parallel capsules: two points; crossed: one. @@ -968,13 +968,13 @@ test_rounded_pairs() { ensure("capsules parallel: two points", m.point_count == 2) small("capsules parallel: normal y", m.normal.y - 1.0, 0.00001) if m.point_count == 2 { - small("capsules parallel: separation 0", manifold.manifold_point(&m, 0).separation + 0.01, 0.00001) - small("capsules parallel: separation 1", manifold.manifold_point(&m, 1).separation + 0.01, 0.00001) + small("capsules parallel: separation 0", manifold.point(&m, 0).separation + 0.01, 0.00001) + small("capsules parallel: separation 1", manifold.point(&m, 1).separation + 0.01, 0.00001) } m = manifold.local_manifold(points) manifold.collide_capsules(&m, 8, c, d, Transform { p: math.vec3(0.0, 0.39, 0.0), q: exact_quat(math.vec3_axis_y(), 0.5 * math.PI) }) ensure("capsules crossed: one point", m.point_count == 1) - if m.point_count == 1 { small("capsules crossed: separation", manifold.manifold_point(&m, 0).separation + 0.01, 0.00001) } + if m.point_count == 1 { small("capsules crossed: separation", manifold.point(&m, 0).separation + 0.01, 0.00001) } m = manifold.local_manifold(points) manifold.collide_capsules(&m, 8, c, d, Transform { p: math.vec3(0.0, 0.5, 0.0), q: math.quat_identity() }) ensure("capsules apart: empty", m.point_count == 0) diff --git a/tests/test_mesh_contact.ae b/tests/test_mesh_contact.ae index 74919d6..dd8a9b9 100644 --- a/tests/test_mesh_contact.ae +++ b/tests/test_mesh_contact.ae @@ -157,7 +157,7 @@ test_box_on_grid() { b = shape.hull_shape(box, 1.0) xf_a = math.transform_identity() xf_b = Transform { p: math.vec3(0.3, 0.49, 0.2), q: math.quat_identity() } - cache = mesh_contact.mesh_contact_cache_create() + cache = mesh_contact.cache_create() r = manifolds(&cache, &ground, xf_a, &b, xf_b, false) ensure("box: triangles cached (${cache.count})", cache.count == 8) ensure("box: one cluster (${r.cluster_count})", r.cluster_count == 1) @@ -220,7 +220,7 @@ test_box_on_grid() { ensure("tilted box: the faces at the edge", math.abs_float(n0.x + n1.x - 1.0) < 0.0001 && math.abs_float(n0.y + n1.y - 1.0) < 0.0001) ensure("tilted box: touching (${min_separation(r)})", min_separation(r) < 0.0 && min_separation(r) > 0.0 - 0.02) } - mesh_contact.mesh_contact_cache_destroy(&cache) + mesh_contact.cache_destroy(&cache) hull.destroy_hull(box) mesh.destroy_mesh(grid) } @@ -230,7 +230,7 @@ test_sphere_seams() { ground = shape.mesh_shape(mesh.mesh(grid, math.vec3_one()), 1) s = shape.sphere_shape(manifold.sphere(math.vec3_zero(), 0.5), 1.0) xf_a = math.transform_identity() - cache = mesh_contact.mesh_contact_cache_create() + cache = mesh_contact.cache_create() // On an interior vertex of the grid (a whole number of cells from the corner): one point. xf_b = Transform { p: math.vec3(1.0, 0.49, 1.0), q: math.quat_identity() } r = manifolds(&cache, &ground, xf_a, &s, xf_b, false) @@ -254,8 +254,8 @@ test_sphere_seams() { mesh.destroy_mesh(grid) wave = mesh.create_wave_mesh(20, 20, 1.0, 0.5, 0.25, 0.0) ground = shape.mesh_shape(mesh.mesh(wave, math.vec3_one()), 1) - mesh_contact.mesh_contact_cache_destroy(&cache) - cache = mesh_contact.mesh_contact_cache_create() + mesh_contact.cache_destroy(&cache) + cache = mesh_contact.cache_create() // Sampling the surface for the lowest point along x at the mesh's middle row. lowest = math.MAX_FLOAT lowest_x = 0.0 @@ -272,7 +272,7 @@ test_sphere_seams() { r = manifolds(&cache, &ground, xf_a, &s, xf_b, false) ensure("sphere in a valley: clusters (${r.cluster_count})", r.cluster_count >= 1 && r.cluster_count <= 2) ensure("sphere in a valley: touching", min_separation(r) < 0.02) - mesh_contact.mesh_contact_cache_destroy(&cache) + mesh_contact.cache_destroy(&cache) mesh.destroy_mesh(wave) } @@ -280,7 +280,7 @@ test_capsule_and_field() { grid = mesh.create_grid_mesh(8, 8, 1.0, 1, true) ground = shape.mesh_shape(mesh.mesh(grid, math.vec3_one()), 1) xf_a = math.transform_identity() - cache = mesh_contact.mesh_contact_cache_create() + cache = mesh_contact.cache_create() // A capsule lying along x across three triangles: one cluster, two points at its ends. c_shape = shape.capsule_shape(manifold.capsule(math.vec3(0.0 - 1.0, 0.0, 0.0), math.vec3(1.0, 0.0, 0.0), 0.3), 1.0) xf_b = Transform { p: math.vec3(0.4, 0.29, 0.3), q: math.quat_identity() } @@ -303,7 +303,7 @@ test_capsule_and_field() { ensure("capsule: points under the axis", on_axis) ensure("capsule: both ends kept", ends == 2) } - mesh_contact.mesh_contact_cache_destroy(&cache) + mesh_contact.cache_destroy(&cache) mesh.destroy_mesh(grid) // The box on a flat height field: the same one cluster of four. @@ -311,7 +311,7 @@ test_capsule_and_field() { f_shape = shape.height_field_shape(field, 1) box = hull.make_box_hull(0.5, 0.5, 0.5) b = shape.hull_shape(box, 1.0) - cache = mesh_contact.mesh_contact_cache_create() + cache = mesh_contact.cache_create() xf_b = Transform { p: math.vec3(2.3, 0.49, 2.2), q: math.quat_identity() } r = manifolds(&cache, &f_shape, xf_a, &b, xf_b, false) ensure("field: triangles cached (${cache.count})", cache.count >= 2 && cache.count <= 8) @@ -321,7 +321,7 @@ test_capsule_and_field() { // The grid's quantum puts its surface 0.0039 under y = 0. small("field: separation", min_separation(r) + 0.01 - 0.0039, 0.001) } - mesh_contact.mesh_contact_cache_destroy(&cache) + mesh_contact.cache_destroy(&cache) height_field.destroy_height_field(field) // A ridge: a field rising to a crest along z at x = 3; a box straddling the crest @@ -337,7 +337,7 @@ test_capsule_and_field() { heights[i] = h i = i + 1 } - def = height_field.height_field_def() + def = height_field.def() def.heights = heights_block def.count_x = 6 def.count_z = 6 @@ -345,7 +345,7 @@ test_capsule_and_field() { def.global_maximum_height = 1.0 ridge = height_field.create_height_field(&def) r_shape = shape.height_field_shape(ridge, 1) - cache = mesh_contact.mesh_contact_cache_create() + cache = mesh_contact.cache_create() xf_b = Transform { p: math.vec3(3.0, 0.5 + 0.5 - 0.01, 2.5), q: math.quat_identity() } r = manifolds(&cache, &r_shape, xf_a, &b, xf_b, false) ensure("ridge: clusters (${r.cluster_count})", r.cluster_count >= 2) @@ -361,7 +361,7 @@ test_capsule_and_field() { } ensure("ridge: triangle normals lean both ways", leaning_left >= 1 && leaning_right >= 1) ensure("ridge: touching", min_separation(r) < 0.0 && min_separation(r) > 0.0 - 0.05) - mesh_contact.mesh_contact_cache_destroy(&cache) + mesh_contact.cache_destroy(&cache) height_field.destroy_height_field(ridge) free(heights_block) @@ -373,7 +373,7 @@ test_capsule_and_field() { xf_a = Transform { p: math.vec3(10.0, 20.0, 30.0), q: q } // Local (0.3, 0.49, 0.2) in world: p + q * local = (10 - 0.49, 20 + 0.3, 30.2). xf_b = Transform { p: math.transform_point(xf_a, math.vec3(0.3, 0.49, 0.2)), q: q } - cache = mesh_contact.mesh_contact_cache_create() + cache = mesh_contact.cache_create() r = manifolds(&cache, &ground, xf_a, &b, xf_b, false) ensure("moved mesh: one cluster of four", r.cluster_count == 1 && cluster(r, 0).point_count == 4) if r.cluster_count == 1 { @@ -381,7 +381,7 @@ test_capsule_and_field() { small("moved mesh: normal in the box's frame", cluster(r, 0).manifold_normal.y - 1.0, 0.0001) small("moved mesh: separation", min_separation(r) + 0.01, 0.0001) } - mesh_contact.mesh_contact_cache_destroy(&cache) + mesh_contact.cache_destroy(&cache) mesh.destroy_mesh(grid) hull.destroy_hull(box) } diff --git a/tests/test_mover.ae b/tests/test_mover.ae index c257f60..b068f16 100644 --- a/tests/test_mover.ae +++ b/tests/test_mover.ae @@ -181,7 +181,7 @@ test_shape_movers() { count = mesh.collide_mover_and_mesh(planes, 4, sh, manifold.capsule(math.vec3(0.0 - 2.0, 0.15, 0.0), math.vec3(0.0 - 2.0, 0.35, 0.0), 0.2)) ensure("mirrored mesh front", count == 1 && planes[0].plane.normal.y > 0.99) ensure("mirrored mesh material", count == 1 && planes[0].material_index == 1) - mesh_materials = mesh.mesh_material_indices(md) + mesh_materials = mesh.material_indices(md) ensure("mirrored mesh triangle material", count == 1 && mesh_materials[planes[0].triangle_index] == 1) ensure("mirrored mesh back culled", mesh.collide_mover_and_mesh(planes, 4, sh, manifold.capsule(math.vec3(0.0 - 2.0, 0.0 - 0.35, 0.0), math.vec3(0.0 - 2.0, 0.0 - 0.15, 0.0), 0.2)) == 0) mesh.destroy_mesh(md) @@ -194,7 +194,7 @@ test_shape_movers() { heights = calloc(9, 8) materials_block = calloc(4, 4) materials = materials_block as int[] - def = height_field.height_field_def() + def = height_field.def() def.heights = heights def.material_indices = materials_block def.count_x = 3 diff --git a/tests/test_mover_world.ae b/tests/test_mover_world.ae index 6495417..1003861 100644 --- a/tests/test_mover_world.ae +++ b/tests/test_mover_world.ae @@ -170,7 +170,7 @@ test_world_mesh_materials() { sdef.material_count = 2 dynamics.create_mesh_shape(body_id, &sdef, m, math.vec3_one()) physics_world.step(world_id, 1.0 / 60.0, 1) - baked = mesh.mesh_material_indices(m) + baked = mesh.material_indices(m) filter = shape.default_query_filter() // A mover hanging just above the first triangle, its radius reaching 0.05 into the surface. kept = calloc(16, sizeof(PlaneResult)) diff --git a/tests/test_physics_world.ae b/tests/test_physics_world.ae index 5c445b5..65ec352 100644 --- a/tests/test_physics_world.ae +++ b/tests/test_physics_world.ae @@ -1805,7 +1805,7 @@ test_body_queries() { // test_body_query.c, the rest (#143): the hull-proxy overlaps, the // compound on a turned body through both overlaps, and the mover's time // of impact against one body in every case the reference asks. -hull_proxy(h: *HullData) -> ShapeProxy { return distance.shape_proxy(hull.hull_points(h) as ptr, h.vertex_count, 0.0) } +hull_proxy(h: *HullData) -> ShapeProxy { return distance.shape_proxy(hull.points(h) as ptr, h.vertex_count, 0.0) } overlap_count(world_id: WorldId, origin: Vec3, proxy: ShapeProxy) -> int { counter = calloc(1, 4) diff --git a/tests/test_shape.ae b/tests/test_shape.ae index 1875d01..38e580a 100644 --- a/tests/test_shape.ae +++ b/tests/test_shape.ae @@ -655,7 +655,7 @@ test_dispatch() { surface = 21.0 if k == 1 { surface = 23.0 } point[0] = world_origin - out = shape.shape_cast_shape(&sh, t, proxy, world_translation, 1.0, false) + out = shape.cast_shape(&sh, t, proxy, world_translation, 1.0, false) ensure("dispatch shape cast ${k} hit", out.hit) small("dispatch shape cast ${k} fraction", out.fraction - (24.0 - surface - 0.5) / 8.0, 0.001) small("dispatch shape cast ${k} normal", out.normal.y - 1.0, 0.001) @@ -666,7 +666,7 @@ test_dispatch() { while k < 5 { sh = shapes[k] point[0] = math.vec3(7.0, 21.0, 31.0) - out = shape.shape_cast_shape(&sh, t, proxy, math.vec3(8.0, 0.0, 0.0), 1.0, false) + out = shape.cast_shape(&sh, t, proxy, math.vec3(8.0, 0.0, 0.0), 1.0, false) ensure("dispatch shape cast ${k} hit", out.hit) small("dispatch shape cast ${k} fraction", out.fraction - 2.5 / 8.0, 0.01) small("dispatch shape cast ${k} normal", out.normal.x + 1.0, 0.001) diff --git a/tests/test_triangle_manifold.ae b/tests/test_triangle_manifold.ae index 0c1d27a..1d9fead 100644 --- a/tests/test_triangle_manifold.ae +++ b/tests/test_triangle_manifold.ae @@ -57,7 +57,7 @@ min_separation(m: *LocalManifold) -> float { s = math.MAX_FLOAT i = 0 while i < m.point_count { - s = math.min_float(s, manifold.manifold_point(m, i).separation) + s = math.min_float(s, manifold.point(m, i).separation) i = i + 1 } return s @@ -76,7 +76,7 @@ check_edge_contact(name: string, m: *LocalManifold, p1: Vec3, e1: Vec3, p2: Vec3 small("${name}: perpendicular to e2", math.dot(m.normal, math.normalize(e2)), normal_tol) closest = math.line_distance(p1, e1, p2, e2) expected_separation = math.dot(axis, math.sub(closest.point2, closest.point1)) - radius - p = manifold.manifold_point(m, 0) + p = manifold.point(m, 0) small("${name}: separation", p.separation - expected_separation, sep_tol) expected_point = math.mul_sv(0.5, math.add(math.mul_sub(closest.point1, radius, axis), closest.point2)) small("${name}: point x", p.point.x - expected_point.x, point_tol) @@ -85,14 +85,14 @@ check_edge_contact(name: string, m: *LocalManifold, p1: Vec3, e1: Vec3, p2: Vec3 } hull_edge_tail(h: *HullData, edge_index: int) -> Vec3 { - edges = hull.hull_edges(h) - points = hull.hull_points(h) + edges = hull.edges(h) + points = hull.points(h) return points[edges[edge_index].origin] } hull_edge_vector(h: *HullData, edge_index: int) -> Vec3 { - edges = hull.hull_edges(h) - points = hull.hull_points(h) + edges = hull.edges(h) + points = hull.points(h) return math.sub(points[edges[edges[edge_index].twin].origin], points[edges[edge_index].origin]) } @@ -128,7 +128,7 @@ test_triangle_edge() { small("triangle edge ${i}: normal y", m.normal.y - expected_normal.y, 0.00001) small("triangle edge ${i}: normal z", m.normal.z - expected_normal.z, 0.00001) if m.point_count == 1 { - p = manifold.manifold_point(&m, 0) + p = manifold.point(&m, 0) small("triangle edge ${i}: separation", p.separation - gap, 0.00001) small("triangle edge ${i}: point x", p.point.x - expected_point.x, 0.00001) small("triangle edge ${i}: point y", p.point.y - expected_point.y, 0.00001) @@ -319,8 +319,8 @@ test_triangle_capsule() { if m.point_count == 2 { lower = math.min_float(c1.y, c2.y) - radii2[r] upper = math.max_float(c1.y, c2.y) - radii2[r] - s0 = manifold.manifold_point(&m, 0).separation - s1 = manifold.manifold_point(&m, 1).separation + s0 = manifold.point(&m, 0).separation + s1 = manifold.point(&m, 1).separation small("capsule face deep: min separation", math.min_float(s0, s1) - lower, 0.00001) small("capsule face deep: max separation", math.max_float(s0, s1) - upper, 0.00001) ensure("capsule face deep: penetrating", math.min_float(s0, s1) < 0.0) @@ -362,14 +362,14 @@ test_sphere_and_closest() { ensure("sphere face: one point", m.point_count == 1) ensure("sphere face: feature", m.feature == triangle_manifold.FEATURE_TRIANGLE_FACE) small("sphere face: normal y", m.normal.y - 1.0, 0.00001) - if m.point_count == 1 { small("sphere face: separation", manifold.manifold_point(&m, 0).separation - 0.01, 0.00001) } + if m.point_count == 1 { small("sphere face: separation", manifold.point(&m, 0).separation - 0.01, 0.00001) } // Off an edge: the normal from the edge to the centre. s = manifold.sphere(math.vec3(0.0, 0.15, 0.0 - 1.15), 0.25) m = manifold.local_manifold(points) triangle_manifold.collide_triangle_and_sphere(&m, 8, v1, v2, v3, s) ensure("sphere edge: one point", m.point_count == 1) ensure("sphere edge: feature", m.feature == triangle_manifold.FEATURE_EDGE3) - if m.point_count == 1 { small("sphere edge: separation", manifold.manifold_point(&m, 0).separation - (sqrt(0.045) - 0.25), 0.00001) } + if m.point_count == 1 { small("sphere edge: separation", manifold.point(&m, 0).separation - (sqrt(0.045) - 0.25), 0.00001) } // Below the plane: culled. Far above: nothing. s = manifold.sphere(math.vec3(0.0, 0.0 - 0.1, 0.0), 0.25) m = manifold.local_manifold(points) @@ -400,7 +400,7 @@ test_resting_cube() { small("resting: normal up", m.normal.y - 1.0, 0.00001) i = 0 while i < m.point_count { - small("resting: separation", manifold.manifold_point(&m, i).separation - 0.005, 0.00001) + small("resting: separation", manifold.point(&m, i).separation - 0.005, 0.00001) i = i + 1 } ensure("resting: cache miss the first time", cache.hit == 0) diff --git a/tests/test_world_queries.ae b/tests/test_world_queries.ae index 72a346a..9dc912c 100644 --- a/tests/test_world_queries.ae +++ b/tests/test_world_queries.ae @@ -147,7 +147,7 @@ main() { ws = dynamics.get_shape_full_id(w, shapes[j]) bt = dynamics.get_body_transform(w, ws.body_id) rel = Transform { p: math.sub(bt.p, origin), q: bt.q } - out = shape.shape_cast_shape(&ws.geometry, rel, proxy, translation, 1.0, false) + out = shape.cast_shape(&ws.geometry, rel, proxy, translation, 1.0, false) if out.hit && out.fraction > 0.0 && out.fraction < best { best = out.fraction } j = j + 1 } From a8de776124c8c77e3132143db5d4e63f5fad02c3 Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Tue, 20 Oct 2026 11:18:33 -0300 Subject: [PATCH 6/7] Run-time SIMD width: the wide stages' AVX2 copy chosen per world 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 --- aephysics/basics/module.ae | 20 ++++++++++++++ aephysics/contact_solver_wide/module.ae | 8 +++++- aephysics/dynamics/module.ae | 2 ++ aephysics/native/aephysics_native.c | 13 --------- aephysics/native/module.ae | 6 +---- aephysics/physics_world/module.ae | 17 +++++++++--- aephysics/solver/module.ae | 36 ++++++++++++++++++------- bench/physics_world.ae | 8 ++++++ tests/test_determinism.ae | 35 ++++++++++++++++++++++++ tests/test_host_api.ae | 28 ++++++++++++++++++- 10 files changed, 140 insertions(+), 33 deletions(-) diff --git a/aephysics/basics/module.ae b/aephysics/basics/module.ae index ed76e41..2359717 100644 --- a/aephysics/basics/module.ae +++ b/aephysics/basics/module.ae @@ -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, @@ -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 { diff --git a/aephysics/contact_solver_wide/module.ae b/aephysics/contact_solver_wide/module.ae index c580666..11bd9bb 100644 --- a/aephysics/contact_solver_wide/module.ae +++ b/aephysics/contact_solver_wide/module.ae @@ -31,7 +31,8 @@ import aephysics.contact_solver exports ( WIDTH, Vec3W, IntW, WideConstraintPoint, WideConstraint, wide_constraint_at, wide_constraint_count, wide_point_at, lane, int_lane, - prepare_wide_contacts, prepare_wide_range, zero_wide_tail, warm_start_wide_contacts, solve_wide_contacts, apply_wide_restitution, store_wide_impulses, store_wide_range + prepare_wide_contacts, prepare_wide_range, zero_wide_tail, warm_start_wide_contacts, solve_wide_contacts, apply_wide_restitution, store_wide_impulses, store_wide_range, + lanes_native ) const WIDTH = 8 @@ -364,6 +365,11 @@ zero_record() -> ptr { return g_zero_record } +// Whether this copy of the module holds an f32x8 in one register: the +// build's answer in the plain import, true in the solver's @target("avx2") +// copy on x86 (#156). +lanes_native() -> bool { return lanes.native8() } + f8(a0: float, a1: float, a2: float, a3: float, a4: float, a5: float, a6: float, a7: float) -> f32x8 { return lanes.f32x8(a0 as f32, a1 as f32, a2 as f32, a3 as f32, a4 as f32, a5 as f32, a6 as f32, a7 as f32) } // The contact of a lane: index `index` of the colour's convex contacts, or diff --git a/aephysics/dynamics/module.ae b/aephysics/dynamics/module.ae index bbbcf53..f8e8621 100644 --- a/aephysics/dynamics/module.ae +++ b/aephysics/dynamics/module.ae @@ -1349,6 +1349,7 @@ struct World { enable_warm_starting: bool enable_continuous: bool enable_speculative: bool + simd_width: int // 8: the wide stages run their AVX2 copy; 4: the build's (basics.get_simd_width at creation) in_use: bool user_data: ptr recording: ptr // *Recording while the world records (aephysics.recording), else null @@ -1638,6 +1639,7 @@ init_world_contents(w: *World, def: *WorldDef) { w.enable_warm_starting = true w.enable_continuous = def.enable_continuous w.enable_speculative = true + w.simd_width = basics.get_simd_width() } // Everything the world holds is freed; ids into it die with its generation. diff --git a/aephysics/native/aephysics_native.c b/aephysics/native/aephysics_native.c index 0f43631..a2a3061 100644 --- a/aephysics/native/aephysics_native.c +++ b/aephysics/native/aephysics_native.c @@ -222,19 +222,6 @@ int aephysics_processor_count(void) #endif } -// Whether this CPU runs AVX2 (the reference's b3IsAVX2Available): the -// instruction set and the operating system's saving of its registers, as -// GCC's and Clang's CPU check reads them. Never on a CPU that is not x86. -int aephysics_has_avx2(void) -{ -#if defined(__x86_64__) || defined(__i386__) - __builtin_cpu_init(); - return __builtin_cpu_supports("avx2") ? 1 : 0; -#else - return 0; -#endif -} - // A counting semaphore (the reference's b3Semaphore): the scheduler's // threads wait on it for work and are signalled one per task, or once // each to shut down. Win32's, Apple's dispatch one, POSIX's elsewhere. diff --git a/aephysics/native/module.ae b/aephysics/native/module.ae index 596471d..1a3abb5 100644 --- a/aephysics/native/module.ae +++ b/aephysics/native/module.ae @@ -8,7 +8,7 @@ @source("aephysics_native.c") @c_include("aephysics_inline.h") exports ( - MAX_WORKERS, worker_index, set_worker_index, claim_slots, release_slots, enter_traversal, leave_traversal, yield_thread, pause, processor_count, has_avx2, scratch, + MAX_WORKERS, worker_index, set_worker_index, claim_slots, release_slots, enter_traversal, leave_traversal, yield_thread, pause, processor_count, scratch, atomic_load, atomic_store, atomic_add, atomic_or, atomic_cas, semaphore_bytes, semaphore_init, semaphore_destroy, semaphore_wait, semaphore_signal, name_text, prefetch, load_word, load_u8, load_u16, f32_bits, f64_bits, @@ -27,7 +27,6 @@ extern aephysics_enter_traversal(depths: ptr) -> int extern aephysics_leave_traversal(depths: ptr) extern aephysics_yield() extern aephysics_processor_count() -> int -extern aephysics_has_avx2() -> int extern aephysics_pause() @c_import extern aephysics_atomic_load(p: ptr) -> int @c_import extern aephysics_atomic_store(p: ptr, value: int) @c_import @@ -96,9 +95,6 @@ yield_thread() { aephysics_yield() } pause() { aephysics_pause() } processor_count() -> int { return aephysics_processor_count() } -// Whether this CPU runs AVX2 instructions. -has_avx2() -> bool { return aephysics_has_avx2() != 0 } - // Atomics on an int where it sits (`(&s.field) as ptr`), sequentially // consistent: the solver's block claims and completion counts, its sync // bits, the bullets' count and the tree's moved flags, which workers diff --git a/aephysics/physics_world/module.ae b/aephysics/physics_world/module.ae index 1218d04..8433f02 100644 --- a/aephysics/physics_world/module.ae +++ b/aephysics/physics_world/module.ae @@ -16,6 +16,7 @@ // reference does. import std.string import std.os +import std.lanes import aephysics.math import aephysics.native import aephysics.basics @@ -38,7 +39,7 @@ exports ( body_event_count, body_event, contact_begin_event_count, contact_begin_event, contact_end_event_count, contact_end_event, contact_hit_event_count, contact_hit_event, sensor_begin_event_count, sensor_begin_event, sensor_end_event_count, sensor_end_event, joint_event_count, joint_event, - enable_sleeping, is_sleeping_enabled, enable_warm_starting, is_warm_starting_enabled, enable_continuous, is_continuous_enabled, + enable_sleeping, is_sleeping_enabled, enable_warm_starting, is_warm_starting_enabled, enable_sse2_fallback, enable_continuous, is_continuous_enabled, enable_speculative, set_restitution_threshold, get_restitution_threshold, set_restitution_iterations, get_restitution_iterations, enable_restitution_propagation, is_restitution_propagation_enabled, enable_kinematic_static_softness, is_kinematic_static_softness_enabled, enable_kinematic_speculation, is_kinematic_speculation_enabled, enable_travel_speculation, is_travel_speculation_enabled, set_hit_event_threshold, get_hit_event_threshold, set_contact_tuning, set_contact_recycle_distance, get_contact_recycle_distance, set_maximum_linear_speed, get_maximum_linear_speed, @@ -115,9 +116,9 @@ get_version() -> Version { return Version { major: 0, minor: 2, revision: 0 } } // (b3IsDoublePrecision); the rest single where the reference has it. is_double_precision() -> bool { return true } -// Whether this CPU runs AVX2 (b3IsAVX2Available). The wide contact solver's -// width is still the build's (-mavx2), #156. -is_avx2_available() -> bool { return native.has_avx2() } +// Whether this CPU runs AVX2 (b3IsAVX2Available): where it does, a world's +// wide stages run their copy compiled for AVX2 (basics.set_simd_width). +is_avx2_available() -> bool { return lanes.cpu_has_avx2() } // The worlds there are, and the most there have been at once. get_world_count() -> int { return dynamics.get_world_count() } @@ -350,6 +351,14 @@ enable_warm_starting(id: WorldId, flag: bool) { if w != null { w.enable_warm_starting = flag } } is_warm_starting_enabled(id: WorldId) -> bool { return dynamics.get_world(id.index1 - 1).enable_warm_starting } +// Run the world's wide stages on the build's own lanes even where the CPU has +// AVX2 (b3World_EnableSSE2Fallback, "for testing"): the same bits either +// way. Off, the world goes back to basics.get_simd_width(). +enable_sse2_fallback(id: WorldId, flag: bool) { + w = world_of(id) + if w == null { return } + if flag { w.simd_width = 4 } else { w.simd_width = basics.get_simd_width() } +} enable_continuous(id: WorldId, flag: bool) { if dynamics.recording_on(id.index1 - 1) { recorder = dynamics.rec_begin(id.index1 - 1, dynamics.OP_ENABLE_CONTINUOUS); dynamics.rec_bool(recorder, flag); dynamics.rec_end(recorder) } w = world_of(id) diff --git a/aephysics/solver/module.ae b/aephysics/solver/module.ae index 19fd78f..abc88d2 100644 --- a/aephysics/solver/module.ae +++ b/aephysics/solver/module.ae @@ -33,6 +33,10 @@ import aephysics.dynamics import aephysics.parallel import aephysics.contact_solver import aephysics.contact_solver_wide +// #156: the wide stages a second time, compiled for AVX2, chosen per world by +// its simd_width (basics.set_simd_width, b3SetSIMDWidth): one binary runs +// eight lanes to a register where the CPU has AVX2 and anywhere else. +@target("avx2") import aephysics.contact_solver_wide as contact_solver_wide8 import aephysics.joint_solver exports ( @@ -892,14 +896,26 @@ execute_block(stage: *SolverStage, sc: *SolverContext, b: *SolverBlock, worker: slots = color.wide_constraints + start * sizeof(WideConstraint) first = start * contact_solver_wide.WIDTH last = math.min_int(end * contact_solver_wide.WIDTH, color.convex_contacts.count) - if kind == STAGE_PREPARE_WIDE { - contact_solver_wide.prepare_wide_range(ctx, color, first, last) - } else if kind == STAGE_WARM_START { contact_solver_wide.warm_start_wide_contacts(ctx, slots, count) } - else if kind == STAGE_SOLVE { contact_solver_wide.solve_wide_contacts(ctx, slots, count, true) } - else if kind == STAGE_RELAX { contact_solver_wide.solve_wide_contacts(ctx, slots, count, false) } - else if kind == STAGE_RESTITUTION { contact_solver_wide.apply_wide_restitution(ctx, slots, count) } - else if kind == STAGE_STORE_WIDE { - if contact_solver_wide.store_wide_range(ctx, color, first, last, &tc.hit_event_bits) { tc.has_hit_events = true } + if w.simd_width == 8 { + if kind == STAGE_PREPARE_WIDE { + contact_solver_wide8.prepare_wide_range(ctx, color, first, last) + } else if kind == STAGE_WARM_START { contact_solver_wide8.warm_start_wide_contacts(ctx, slots, count) } + else if kind == STAGE_SOLVE { contact_solver_wide8.solve_wide_contacts(ctx, slots, count, true) } + else if kind == STAGE_RELAX { contact_solver_wide8.solve_wide_contacts(ctx, slots, count, false) } + else if kind == STAGE_RESTITUTION { contact_solver_wide8.apply_wide_restitution(ctx, slots, count) } + else if kind == STAGE_STORE_WIDE { + if contact_solver_wide8.store_wide_range(ctx, color, first, last, &tc.hit_event_bits) { tc.has_hit_events = true } + } + } else { + if kind == STAGE_PREPARE_WIDE { + contact_solver_wide.prepare_wide_range(ctx, color, first, last) + } else if kind == STAGE_WARM_START { contact_solver_wide.warm_start_wide_contacts(ctx, slots, count) } + else if kind == STAGE_SOLVE { contact_solver_wide.solve_wide_contacts(ctx, slots, count, true) } + else if kind == STAGE_RELAX { contact_solver_wide.solve_wide_contacts(ctx, slots, count, false) } + else if kind == STAGE_RESTITUTION { contact_solver_wide.apply_wide_restitution(ctx, slots, count) } + else if kind == STAGE_STORE_WIDE { + if contact_solver_wide.store_wide_range(ctx, color, first, last, &tc.hit_event_bits) { tc.has_hit_events = true } + } } if timed { stage_note(kind, at) } return @@ -1190,7 +1206,9 @@ solve_step(w: *World, ctx: *StepContext) { wide_base = wide_base + color.wide_constraint_count contact_base = contact_base + color.contact_constraint_count manifold_base = manifold_base + color.manifold_constraint_count - if wide { contact_solver_wide.zero_wide_tail(color) } + // The copy that runs the stages makes its zero record, here on one thread. + if wide && w.simd_width == 8 { contact_solver_wide8.zero_wide_tail(color) } + if wide && w.simd_width != 8 { contact_solver_wide.zero_wide_tail(color) } c = c + 1 } overflow = dynamics.graph_color(w, dynamics.OVERFLOW_INDEX) diff --git a/bench/physics_world.ae b/bench/physics_world.ae index 3016ab6..866e2e2 100644 --- a/bench/physics_world.ae +++ b/bench/physics_world.ae @@ -111,6 +111,14 @@ wanted(scene: string) -> bool { } main() { + // AEPHYSICS_SIMD_WIDTH=4|8 runs the wide stages on the build's own lanes + // or on their AVX2 copy (basics.set_simd_width); unset, the CPU's. + width = os.getenv("AEPHYSICS_SIMD_WIDTH") + if width != null { + asked, err = string.to_int(width) + if err == "" { basics.set_simd_width(asked) } + } + println("aephysics physics_world: simd width ${basics.get_simd_width()}") g_box = hull.make_box_hull(0.5, 0.5, 0.5) as ptr g_slabs = calloc(8, 8) wdef = dynamics.default_world_def() diff --git a/tests/test_determinism.ae b/tests/test_determinism.ae index 6ffb7e8..99a28e6 100644 --- a/tests/test_determinism.ae +++ b/tests/test_determinism.ae @@ -28,6 +28,7 @@ import aephysics.dynamics import aephysics.human import aephysics.physics_world import aephysics.parallel +@target("avx2") import aephysics.contact_solver_wide as contact_solver_wide8 extern calloc(count: int, size: int) -> ptr extern exit(code: int) @@ -647,6 +648,38 @@ rolling_mix(workers: int) -> Outcome { return Outcome { sleep_step: 120, hash: total, query_hits: 0, query_hash: 0.0 } } +// Box3D's SIMDWidthTest: every scene stepped by the wide stages' copy for +// AVX2 and by the build's own copy, in one process, gives the same sleep +// step, checksum and query results. Where the CPU has no AVX2 there is +// one width and nothing to compare. +test_simd_widths() { + basics.set_simd_width(0) + native_width = basics.get_simd_width() + if native_width == 4 { + println("determinism: simd widths skipped, this CPU runs one width") + return + } + ensure("simd widths: the AVX2 copy holds eight lanes a register", contact_solver_wide8.lanes_native()) + basics.set_simd_width(4) + a4 = falling_ragdolls(1) + b4 = wave_pile(1) + c4 = query_spawn(1) + d4 = mesh_drop(1) + e4 = rolling_mix(1) + basics.set_simd_width(native_width) + a8 = falling_ragdolls(1) + b8 = wave_pile(1) + c8 = query_spawn(1) + d8 = mesh_drop(1) + e8 = rolling_mix(1) + basics.set_simd_width(0) + ensure("simd widths: falling ragdolls at 4 and ${native_width}, bit for bit", same_outcome(a4, a8)) + ensure("simd widths: wave pile at 4 and ${native_width}, bit for bit", same_outcome(b4, b8)) + ensure("simd widths: query spawn at 4 and ${native_width}, bit for bit", same_outcome(c4, c8)) + ensure("simd widths: mesh drop at 4 and ${native_width}, bit for bit", same_outcome(d4, d8)) + ensure("simd widths: rolling mix at 4 and ${native_width}, bit for bit", same_outcome(e4, e8)) +} + main() { test_concurrent_worlds() value = os.getenv("AEPHYSICS_TRACE") @@ -707,6 +740,8 @@ main() { ensure("rolling mix: the same scene twice, bit for bit", same_outcome(first, second)) ensure("rolling mix: the same over four workers, bit for bit", same_outcome(first, four)) + test_simd_widths() + println("determinism: ${checks} checks") if failures == 0 { println("determinism: all checks passed") diff --git a/tests/test_host_api.ae b/tests/test_host_api.ae index d90d962..6a12c51 100644 --- a/tests/test_host_api.ae +++ b/tests/test_host_api.ae @@ -267,7 +267,33 @@ main() { version = physics_world.get_version() ensure("version: the reference's 0.2.0", version.major == 0 && version.minor == 2 && version.revision == 0) ensure("precision: world positions are double", physics_world.is_double_precision()) - ensure("avx2: the CPU's", physics_world.is_avx2_available() == native.has_avx2()) + // The lane width (b3SetSIMDWidth, b3World_EnableSSE2Fallback): what the + // CPU runs by default, 4 or 8 when asked (8 only with AVX2), another + // number ignored, and a world keeps the width it was created with. + avx2 = physics_world.is_avx2_available() + native_width = 4 + if avx2 { native_width = 8 } + ensure("simd width: the CPU's by default", basics.get_simd_width() == native_width) + basics.set_simd_width(4) + ensure("simd width: 4 when asked", basics.get_simd_width() == 4) + wdef4 = dynamics.default_world_def() + narrow_id = dynamics.create_world(&wdef4) + basics.set_simd_width(8) + ensure("simd width: 8 when asked, with AVX2 only", basics.get_simd_width() == native_width) + basics.set_simd_width(5) + ensure("simd width: another number leaves it", basics.get_simd_width() == native_width) + basics.set_simd_width(0) + ensure("simd width: 0 is the CPU's", basics.get_simd_width() == native_width) + wdef8 = dynamics.default_world_def() + wide_id = dynamics.create_world(&wdef8) + ensure("simd width: a world keeps the width it was made with", dynamics.get_world(narrow_id.index1 - 1).simd_width == 4) + ensure("simd width: a new world takes the CPU's", dynamics.get_world(wide_id.index1 - 1).simd_width == native_width) + physics_world.enable_sse2_fallback(wide_id, true) + ensure("sse2 fallback: on, the world runs 4", dynamics.get_world(wide_id.index1 - 1).simd_width == 4) + physics_world.enable_sse2_fallback(wide_id, false) + ensure("sse2 fallback: off, the CPU's again", dynamics.get_world(wide_id.index1 - 1).simd_width == native_width) + dynamics.destroy_world(wide_id) + dynamics.destroy_world(narrow_id) ensure("hash: the reference's b3Hash of 13 bytes", basics.djb2_hash(basics.HASH_INIT as long, (string.bytes("hello physics") as ptr), 13) == (4027293447 as long)) ensure("hash: of 16 bytes", basics.djb2_hash(basics.HASH_INIT as long, (string.bytes("0123456789abcdef") as ptr), 16) == (1204685188 as long)) ensure("hash: of none", basics.djb2_hash(basics.HASH_INIT as long, null, 0) == (5381 as long)) From 4e4d3a7d58d7a2c6858a53e122674172a97661d1 Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Tue, 20 Oct 2026 11:55:25 -0300 Subject: [PATCH 7/7] The separating axis test as Box3D's wide one, at both widths 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 --- README.md | 21 +- aephysics/manifold/module.ae | 566 ++----------------------- aephysics/sat/module.ae | 641 +++++++++++++++++++++++++++++ bench/RESULTS.md | 46 +++ tests/golden/determinism_trace.txt | 558 +++++++++++++------------ tests/test_collision.ae | 10 +- tests/test_manifold.ae | 13 +- 7 files changed, 1016 insertions(+), 839 deletions(-) create mode 100644 aephysics/sat/module.ae diff --git a/README.md b/README.md index 79da5ba..bb235a1 100644 --- a/README.md +++ b/README.md @@ -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) | @@ -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) | @@ -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 @@ -166,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 diff --git a/aephysics/manifold/module.ae b/aephysics/manifold/module.ae index f217e4d..c55b5a2 100644 --- a/aephysics/manifold/module.ae +++ b/aephysics/manifold/module.ae @@ -27,6 +27,9 @@ import aephysics.native import aephysics.basics import aephysics.distance import aephysics.hull +import aephysics.sat +// The separating axis test a second time, compiled for AVX2 (#150, #156). +@target("avx2") import aephysics.sat as sat8 exports ( reserve_scratch, @@ -224,17 +227,9 @@ var g_clip2: ptr = null clip2() -> ptr { return native.scratch(g_clip2, CLIP_CAPACITY * sizeof(ClipVertex)) } var g_clip_points: ptr = null clip_points() -> ptr { return native.scratch(g_clip_points, MAX_CLIP_POINTS * sizeof(LocalManifoldPoint)) } -// A's edges, gathered once before the edge-pair loop reads them a -// hundred times (see edge_axes). -var g_edge_axes: ptr = null -edge_axes() -> ptr { return native.scratch(g_edge_axes, EDGE_SCRATCH_BYTES) } -// The two hulls' candidate half-edges: A's first, B's from EDGE_LIST_B. -var g_edge_lists: ptr = null -edge_lists() -> ptr { return native.scratch(g_edge_lists, EDGE_LIST_BYTES) } -// Sixteen lanes stored for a reduction: a support search's best dots and their indices. -var g_lane_scratch: ptr = null -lane_scratch() -> ptr { return native.scratch(g_lane_scratch, 64) } -b_frame_scratch() -> ptr { return edge_axes() + AXIS_FIELDS * AXIS_SLOTS * 4 } +// The edge contact's point, built apart before it may replace a face contact's. +var g_edge_point: ptr = null +edge_point() -> ptr { return native.scratch(g_edge_point, 2 * sizeof(LocalManifoldPoint)) } // Each worker's speculative distance: how far apart two shapes keep // points, SPECULATIVE_DISTANCE but while the collide pass reaches out for @@ -268,73 +263,19 @@ scratch_ready() { g_clip1 = basics.alloc(native.MAX_WORKERS * (CLIP_CAPACITY * sizeof(ClipVertex))) g_clip2 = basics.alloc(native.MAX_WORKERS * (CLIP_CAPACITY * sizeof(ClipVertex))) g_clip_points = basics.alloc(native.MAX_WORKERS * (MAX_CLIP_POINTS * sizeof(LocalManifoldPoint))) - g_edge_axes = basics.alloc(native.MAX_WORKERS * EDGE_SCRATCH_BYTES) - g_edge_lists = basics.alloc(native.MAX_WORKERS * EDGE_LIST_BYTES) - g_lane_scratch = basics.alloc(native.MAX_WORKERS * 64) + g_edge_point = basics.alloc(native.MAX_WORKERS * (2 * sizeof(LocalManifoldPoint))) } } // Every scratch block of the module allocated now, on the calling // thread: a world's creation reserves every module's before a step can // reach them from several workers at once. -reserve_scratch() { scratch_ready() } - -// Hull A's edges, as the edge-pair test reads them: the edge's -// direction, its two faces' normals, its first vertex and the tolerance -// that rejects a near-parallel pair. The inner loop below runs over -// every edge of A for every edge of B -- a hundred and forty-four times -// for two boxes, and MAX_HULL_EDGES squared over four for two hulls -- -// and reading them through the half-edge structure each time (a face -// index into the planes, an origin index into the points) is a pointer -// chase per pair. The reference gathers the same values first; this is -// that gather, in single precision, since a separating axis is a -// direction and the reference finds it in floats. -// -// Thirteen flat arrays over one block rather than a record per edge, so -// four edges of A are one load per field and the whole first test -- -// two dot products, two more, a maximum and four comparisons -- is four -// edges at a time (std.lanes, Aether 0.706). Each lane computes what the -// edge computed on its own, in the same order, so the axis a pair -// chooses is the axis it chose before: the tests hold the traces. -// A hull has up to MAX_HULL_EDGES full edges (2 * MAX_HULL_EDGES half-edges, -// the builder's limit), so the table holds that many, a multiple of four. -const AXIS_SLOTS = 128 -// A hull has at most MAX_HULL_EDGES full edges: each list holds them all. -const EDGE_LIST_B = 128 -const EDGE_LIST_BYTES = 2 * 128 * 4 -// The pairs left that pay for the probe's second cull (B3_EDGE_PROBE_MIN_TESTS). -const EDGE_PROBE_MIN_TESTS = 10 -// The hulls at or under this many vertices are searched one vertex at a time. -const WIDE_SUPPORT_MIN_VERTICES = 16 - -// How far under the best edge separation a lane's single-precision -// separation may fall and still be measured in double: well past a float's -// rounding at the hulls' scale (the test runs in A's frame, the hulls -// touching), so the double alone decides which edge wins. -const EDGE_LANE_SLACK = 0.0001 -const AXIS_FIELDS = 14 -// Where each array starts, in elements: x, y, z of the direction, of -// both face normals and of the origin, then the tolerance. -const AXIS_DIR_X = 0 * AXIS_SLOTS -const AXIS_DIR_Y = 1 * AXIS_SLOTS -const AXIS_DIR_Z = 2 * AXIS_SLOTS -const AXIS_N0_X = 3 * AXIS_SLOTS -const AXIS_N0_Y = 4 * AXIS_SLOTS -const AXIS_N0_Z = 5 * AXIS_SLOTS -const AXIS_N1_X = 6 * AXIS_SLOTS -const AXIS_N1_Y = 7 * AXIS_SLOTS -const AXIS_N1_Z = 8 * AXIS_SLOTS -const AXIS_V0_X = 9 * AXIS_SLOTS -const AXIS_V0_Y = 10 * AXIS_SLOTS -const AXIS_V0_Z = 11 * AXIS_SLOTS -const AXIS_TOL = 12 * AXIS_SLOTS -const AXIS_EDGE = 13 * AXIS_SLOTS // the candidate's half-edge index (exact in an f32) -// The table, then room for B's face normals and vertices taken into A's frame -// after it: hull.MAX_HULL_FACES + hull.MAX_HULL_VERTICES of them, written out -// (a constant naming another module's is expanded where this module is -// imported, and not every importer imports the hull). -const B_FRAME_SLOTS = 256 -const EDGE_SCRATCH_BYTES = AXIS_FIELDS * AXIS_SLOTS * 4 + B_FRAME_SLOTS * 24 +reserve_scratch() { + scratch_ready() + sat.reserve_scratch() + sat8.reserve_scratch() +} + // --- the incident face and clipping ---------------------------------------------------- @@ -943,472 +884,25 @@ collide_hull_and_capsule(m: *LocalManifold, capacity: int, hull_a: *HullData, ca // against A's, then the edge pairs whose Gauss-map arcs cross (Gregorius), // with the axis from the arc intersection. Normals point from A to B in // frame A. With early_return the scan stops at the first axis past the -// speculative distance. The reference's scalar path, exactly. +// speculative distance. Box3D's wide test (aephysics.sat), on the copy the +// SIMD width names: compiled for AVX2 at 8 (basics.set_simd_width), the +// build's own at 4, the same bits either way (#150, #156). compute_separating_axis(hull_a: *HullData, hull_b: *HullData, xf_b: Transform, early_return: bool) -> AxisQuery { - reach = speculative_distance() - r = math.make_matrix_from_quat(xf_b.q) - inv_r = math.transpose(r) - res = AxisQuery { - face_a: SeparatingAxis { normal: math.vec3_zero(), separation: 0.0 - math.MAX_FLOAT, index_a: NULL_INDEX, index_b: NULL_INDEX, kind: AXIS_FACE_A }, - face_b: SeparatingAxis { normal: math.vec3_zero(), separation: 0.0 - math.MAX_FLOAT, index_a: NULL_INDEX, index_b: NULL_INDEX, kind: AXIS_FACE_B }, - edge: SeparatingAxis { normal: math.vec3_zero(), separation: 0.0 - math.MAX_FLOAT, index_a: NULL_INDEX, index_b: NULL_INDEX, kind: AXIS_EDGE_PAIR }, - separated_feature: AXIS_INVALID - } - planes_a = hull.planes(hull_a) - points_a = hull.points(hull_a) - planes_b = hull.planes(hull_b) - points_b = hull.points(hull_b) - edges_a = hull.edges(hull_a) - edges_b = hull.edges(hull_b) - - // No face can separate the hulls by more than its normal's reach along - // the line between their centres less their inner spheres' radii - // (Box3D adec25b): a face whose bound is under the best separation so - // far -- or under the seed's, the face most aligned with that line, - // taken first when an early return can use it -- needs no support - // search. The slack keeps the best face from being skipped; a skipped - // face could not have won, so the result is the full scan's. - delta_center = math.sub(math.add(math.mul_mv(r, hull_b.center), xf_b.p), hull_a.center) - center_distance = math.length(delta_center) - radius = hull_a.inner_radius + hull_b.inner_radius - radius_bound = radius - (math.LINEAR_SLOP + 0.001 * (center_distance + radius)) - - // A's face planes against B's vertices. - seed_a = 0 - max_dot = math.dot(planes_a[0].normal, delta_center) - i = 1 - while i < hull_a.face_count { - dot_i = math.dot(planes_a[i].normal, delta_center) - if dot_i > max_dot { - max_dot = dot_i - seed_a = i - } - i = i + 1 - } - floor_a = 0.0 - math.MAX_FLOAT - seed_separation = 0.0 - math.MAX_FLOAT - seed_vertex = 0 - if early_return { - plane = planes_a[seed_a] - direction = math.neg(math.mul_mv(inv_r, plane.normal)) - seed_vertex = support_index_wide(hull_b, direction) - seed_separation = math.dot(plane.normal, xf_b.p) - plane.offset - math.dot(direction, points_b[seed_vertex]) - floor_a = math.min_float(seed_separation, reach) - } - i = 0 - while i < hull_a.face_count { - plane = planes_a[i] - if math.dot(plane.normal, delta_center) - radius_bound >= math.max_float(floor_a, res.face_a.separation) { - vertex_index = seed_vertex - separation = seed_separation - if early_return == false || i != seed_a { - direction = math.neg(math.mul_mv(inv_r, plane.normal)) - plane_separation = math.dot(plane.normal, xf_b.p) - plane.offset - vertex_index = support_index_wide(hull_b, direction) - separation = plane_separation - math.dot(direction, points_b[vertex_index]) - } - if separation > res.face_a.separation { - res.face_a.normal = plane.normal - res.face_a.separation = separation - res.face_a.index_a = i - res.face_a.index_b = vertex_index - if separation > reach && early_return { - res.separated_feature = AXIS_FACE_A - return res - } - } - } - i = i + 1 - } - - // B's face planes against A's vertices, the line between the centres - // taken into B's frame and reversed. - delta_b = math.neg(math.mul_mv(inv_r, delta_center)) - seed_b = 0 - max_dot = math.dot(planes_b[0].normal, delta_b) - i = 1 - while i < hull_b.face_count { - dot_i = math.dot(planes_b[i].normal, delta_b) - if dot_i > max_dot { - max_dot = dot_i - seed_b = i - } - i = i + 1 - } - floor_b = 0.0 - math.MAX_FLOAT - seed_separation = 0.0 - math.MAX_FLOAT - seed_vertex = 0 - if early_return { - plane = planes_b[seed_b] - direction = math.neg(math.mul_mv(r, plane.normal)) - seed_vertex = support_index_wide(hull_a, direction) - seed_separation = math.dot(direction, xf_b.p) - plane.offset - math.dot(direction, points_a[seed_vertex]) - // A's faces have set a floor of their own. - floor_b = math.min_float(math.max_float(seed_separation, res.face_a.separation), reach) - } - i = 0 - while i < hull_b.face_count { - plane = planes_b[i] - if math.dot(plane.normal, delta_b) - radius_bound >= math.max_float(floor_b, res.face_b.separation) { - direction = math.neg(math.mul_mv(r, plane.normal)) - vertex_index = seed_vertex - separation = seed_separation - if early_return == false || i != seed_b { - plane_separation = math.dot(direction, xf_b.p) - plane.offset - vertex_index = support_index_wide(hull_a, direction) - separation = plane_separation - math.dot(direction, points_a[vertex_index]) - } - if separation > res.face_b.separation { - res.face_b.normal = direction - res.face_b.separation = separation - res.face_b.index_a = vertex_index - res.face_b.index_b = i - if separation > reach && early_return { - res.separated_feature = AXIS_FACE_B - return res - } - } - } - i = i + 1 - } - - // The edge pairs. Only the edges that can still beat the best face take - // part (Box3D's inscribed-sphere test, b3TestEdgeCandidateSorted): an - // edge pair's axis lies on both edges' arcs, and no axis on an edge's - // arc separates by more than the other hull's centre reaches along it - // less that hull's inner radius. The cosine between an edge's faces is - // the hull's own (hull_edge_cosines). Without an early return every - // edge is kept. - squared_tol = PARALLEL_EDGE_TOL * PARALLEL_EDGE_TOL - eps = 0.0 - 0.0001 - max_face_separation = math.max_float(res.face_a.separation, res.face_b.separation) - bound_slack = radius - radius_bound - threshold_a = 0.0 - math.MAX_FLOAT - threshold_b = 0.0 - math.MAX_FLOAT - if early_return { - threshold_a = max_face_separation + hull_b.inner_radius - bound_slack - threshold_b = max_face_separation + hull_a.inner_radius - bound_slack - } - center_b_in_a = math.add(math.mul_mv(r, hull_b.center), xf_b.p) - center_a_in_b = math.mul_mv(inv_r, math.sub(hull_a.center, xf_b.p)) - scratch_ready() - lists = edge_lists() as int[] - cosines_a = hull.edge_cosines(hull_a) - cosines_b = hull.edge_cosines(hull_b) - na = 0 - i = 0 - while i < hull_a.edge_count { - if edge_can_reach(math.plane_separation(planes_a[edges_a[i].face], center_b_in_a), math.plane_separation(planes_a[edges_a[i + 1].face], center_b_in_a), - cosines_a[i >> 1] as float, threshold_a) { - lists[na] = i - na = na + 1 - } - i = i + 2 - } - nb = 0 - j = 0 - while j < hull_b.edge_count { - if edge_can_reach(math.plane_separation(planes_b[edges_b[j].face], center_a_in_b), math.plane_separation(planes_b[edges_b[j + 1].face], center_a_in_b), - cosines_b[j >> 1] as float, threshold_b) { - lists[EDGE_LIST_B + nb] = j - nb = nb + 1 - } - j = j + 2 - } - // The best faces' support vertices are points of the other hull, so no - // edge pair's axis separates by more than such a point's plane - // separations over the edge's arc: with enough pairs left to pay for - // it, each list is culled again against them (b3FilterEdgeCandidates). - if early_return && nb * ((na + 3) >> 2) >= EDGE_PROBE_MIN_TESTS { - probe_bound = max_face_separation - (0.1 * math.LINEAR_SLOP + 0.001 * math.abs_float(center_distance + radius)) - vertex_b = res.face_a.index_b - if vertex_b == NULL_INDEX { vertex_b = 0 } - probe_b = math.add(math.mul_mv(r, points_b[vertex_b]), xf_b.p) - na = filter_edge_candidates(planes_a, edges_a, cosines_a, probe_b, probe_bound, lists, 0, na) - vertex_a = res.face_b.index_a - if vertex_a == NULL_INDEX { vertex_a = 0 } - probe_a = math.mul_mv(inv_r, math.sub(points_a[vertex_a], xf_b.p)) - nb = filter_edge_candidates(planes_b, edges_b, cosines_b, probe_a, probe_bound, lists, EDGE_LIST_B, nb) - } - if na == 0 || nb == 0 { return res } - - // A's candidate edges, gathered once into flat single-precision rows: - // what the inner loop reads for every edge of B. A separating axis is a - // direction, and the reference finds it in floats. - axes = edge_axes() - slots = axes as f32[] - k = 0 - while k < na { - i = lists[k] - twin_a = i + 1 - av0 = points_a[edges_a[i].origin] - ad = math.sub(points_a[edges_a[twin_a].origin], av0) - dir32 = math.narrow3(ad) - n0_32 = math.narrow3(planes_a[edges_a[i].face].normal) - n1_32 = math.narrow3(planes_a[edges_a[twin_a].face].normal) - v0_32 = math.narrow3(av0) - slots[AXIS_DIR_X + k] = dir32.x - slots[AXIS_DIR_Y + k] = dir32.y - slots[AXIS_DIR_Z + k] = dir32.z - slots[AXIS_N0_X + k] = n0_32.x - slots[AXIS_N0_Y + k] = n0_32.y - slots[AXIS_N0_Z + k] = n0_32.z - slots[AXIS_N1_X + k] = n1_32.x - slots[AXIS_N1_Y + k] = n1_32.y - slots[AXIS_N1_Z + k] = n1_32.z - slots[AXIS_V0_X + k] = v0_32.x - slots[AXIS_V0_Y + k] = v0_32.y - slots[AXIS_V0_Z + k] = v0_32.z - slots[AXIS_TOL + k] = (squared_tol * math.length_squared(ad)) as f32 - slots[AXIS_EDGE + k] = i as f32 - k = k + 1 - } - // The tail of the last eight. A zero direction answers zero to both of - // the first dot products, and zero is not under eps, so a lane past the - // count decides nothing. - padded = na - while padded % 8 != 0 { - slots[AXIS_DIR_X + padded] = 0.0 - slots[AXIS_DIR_Y + padded] = 0.0 - slots[AXIS_DIR_Z + padded] = 0.0 - padded = padded + 1 - } - f32 eps32 = eps - // B's face normals and vertices into A's frame, negated, once (Box3D - // adec25b) rather than four products for every B edge, into the room - // after the edge table. - b_in_a = b_frame_scratch() as Vec3[] - k = 0 - while k < hull_b.face_count { - b_in_a[k] = math.neg(math.mul_mv(r, planes_b[k].normal)) - k = k + 1 - } - k = 0 - while k < hull_b.vertex_count { - b_in_a[hull_b.face_count + k] = math.neg(math.add(math.mul_mv(r, points_b[k]), xf_b.p)) - k = k + 1 - } - // Every candidate of B against A's, eight of A's a pass (the reference's - // f32x8 loop); the lanes only choose, the axis and its separation are - // computed again in double for the pairs that pass. - eps_lane = lanes.splat8(eps32) - zero_lane = lanes.splat8(0.0) - one_lane = lanes.splat8(1.0) - m = 0 - while m < nb { - j = lists[EDGE_LIST_B + m] - m = m + 1 - twin_b = j + 1 - c = b_in_a[edges_b[j].face] - d = b_in_a[edges_b[twin_b].face] - bv0 = b_in_a[hull_b.face_count + edges_b[j].origin] - bv1 = b_in_a[hull_b.face_count + edges_b[twin_b].origin] - dc = math.sub(bv1, bv0) - c32 = math.narrow3(c) - d32 = math.narrow3(d) - dc32 = math.narrow3(dc) - cx = lanes.splat8(c32.x) - cy = lanes.splat8(c32.y) - cz = lanes.splat8(c32.z) - dx = lanes.splat8(d32.x) - dy = lanes.splat8(d32.y) - dz = lanes.splat8(d32.z) - dcx = lanes.splat8(dc32.x) - dcy = lanes.splat8(dc32.y) - dcz = lanes.splat8(dc32.z) - bv0_32 = math.narrow3(bv0) - bvx = lanes.splat8(bv0_32.x) - bvy = lanes.splat8(bv0_32.y) - bvz = lanes.splat8(bv0_32.z) - k = 0 - while k < padded { - dir_x = lanes.load8(axes, (AXIS_DIR_X + k) as long) - dir_y = lanes.load8(axes, (AXIS_DIR_Y + k) as long) - dir_z = lanes.load8(axes, (AXIS_DIR_Z + k) as long) - cba = cx * dir_x + cy * dir_y + cz * dir_z - dba = dx * dir_x + dy * dir_y + dz * dir_z - live = lanes.lt8(cba * dba, eps_lane) - if lanes.any8(live) { - n0_x = lanes.load8(axes, (AXIS_N0_X + k) as long) - n0_y = lanes.load8(axes, (AXIS_N0_Y + k) as long) - n0_z = lanes.load8(axes, (AXIS_N0_Z + k) as long) - n1_x = lanes.load8(axes, (AXIS_N1_X + k) as long) - n1_y = lanes.load8(axes, (AXIS_N1_Y + k) as long) - n1_z = lanes.load8(axes, (AXIS_N1_Z + k) as long) - adc = n0_x * dcx + n0_y * dcy + n0_z * dcz - bdc = n1_x * dcx + n1_y * dcy + n1_z * dcz - live = lanes.mask8_and(live, lanes.lt8(adc * bdc, eps_lane)) - live = lanes.mask8_and(live, lanes.lt8(cba * bdc, eps_lane)) - // Near-parallel edges are not an axis. - max_cd = lanes.max8(cba * cba, dba * dba) - live = lanes.mask8_and(live, lanes.gt8(max_cd, lanes.load8(axes, (AXIS_TOL + k) as long))) - // The axis and its separation for all eight in lanes, as the - // reference finds them: t = -CBA / (DBA - CBA), the normal - // C + t (D - C) normalised, the separation -dot(av0 + bv0, n). - // Only a lane that comes within EDGE_LANE_SLACK of the best so - // far is measured again in double, which decides: no lane the - // double would have kept is lost to the float's rounding. - lane_t = (zero_lane - cba) / (dba - cba) - lane_nx = cx + lane_t * (dx - cx) - lane_ny = cy + lane_t * (dy - cy) - lane_nz = cz + lane_t * (dz - cz) - lane_inv = one_lane / lanes.sqrt8(lane_nx * lane_nx + lane_ny * lane_ny + lane_nz * lane_nz) - lane_nx = lane_nx * lane_inv - lane_ny = lane_ny * lane_inv - lane_nz = lane_nz * lane_inv - v0x = lanes.load8(axes, (AXIS_V0_X + k) as long) + bvx - v0y = lanes.load8(axes, (AXIS_V0_Y + k) as long) + bvy - v0z = lanes.load8(axes, (AXIS_V0_Z + k) as long) + bvz - sep = zero_lane - (v0x * lane_nx + v0y * lane_ny + v0z * lane_nz) - f32 floor32 = res.edge.separation - EDGE_LANE_SLACK - live = lanes.mask8_and(live, lanes.gt8(sep, lanes.splat8(floor32))) - if lanes.any8(live) { - lane = 0 - while lane < 8 { - if lanes.mask8_lane(live, lane as long) != 0 { - // The axis itself is worth a double: it is the - // normal a contact is built on, and the - // separation is compared against the others'. - at = k + lane - edge_dir = math.vec3(slots[AXIS_DIR_X + at] as float, - slots[AXIS_DIR_Y + at] as float, - slots[AXIS_DIR_Z + at] as float) - edge_v0 = math.vec3(slots[AXIS_V0_X + at] as float, - slots[AXIS_V0_Y + at] as float, - slots[AXIS_V0_Z + at] as float) - cbad = math.dot(c, edge_dir) - dbad = math.dot(d, edge_dir) - t = (0.0 - cbad) / (dbad - cbad) - n = math.normalize(math.add(c, math.mul_sv(t, math.sub(d, c)))) - separation = 0.0 - math.dot(math.add(edge_v0, bv0), n) - if separation > res.edge.separation { - res.edge.normal = n - res.edge.separation = separation - res.edge.index_a = slots[AXIS_EDGE + at] as int - res.edge.index_b = j - if separation > reach && early_return { - res.separated_feature = AXIS_EDGE_PAIR - return res - } - } - } - lane = lane + 1 - } - } - } - k = k + 8 - } - } - return res + if basics.get_simd_width() == 8 { return axis_query_of(sat8.compute(hull_a, hull_b, xf_b, early_return, speculative_distance())) } + return axis_query_of(sat.compute(hull_a, hull_b, xf_b, early_return, speculative_distance())) } -// Whether an edge between faces whose normals meet at cosine c, with the -// other hull's centre at plane separations a1 and a2 from them, can reach -// `bound` anywhere along its Gauss-map arc (Box3D's inscribed-sphere edge -// test, b3TestEdgeCandidateSorted): the larger endpoint, or the interior -// maximum of dot(d, nlerp(n1, n2, t)) when there is one; a nearly -// degenerate arc is kept. -edge_can_reach(a1: float, a2: float, c: float, bound: float) -> bool { - hi = math.max_float(a1, a2) - lo = math.min_float(a1, a2) - if hi >= bound { return true } - u = lo - c * hi - if u < 0.0 { return false } - s = 1.0 - c * c - if s < 0.0001 { return true } - return u * u >= (bound - hi) * (bound + hi) * s +separating_axis_of(a: SatAxis, kind: int) -> SeparatingAxis { + return SeparatingAxis { normal: a.normal, separation: a.separation, index_a: a.index_a, index_b: a.index_b, kind: kind } } -// The candidates of one hull's list (from `offset`, `count` of them) that -// can still reach `bound` from the probe point's plane separations, kept -// in order at the front; how many (b3FilterEdgeCandidates). -filter_edge_candidates(planes: Plane[], edges: HalfEdge[], cosines: f32[], probe: Vec3, bound: float, lists: int[], offset: int, count: int) -> int { - kept = 0 - k = 0 - while k < count { - i = lists[offset + k] - if edge_can_reach(math.plane_separation(planes[edges[i].face], probe), math.plane_separation(planes[edges[i + 1].face], probe), - cosines[i >> 1] as float, bound) { - lists[offset + kept] = i - kept = kept + 1 - } - k = k + 1 - } - return kept -} - -// The vertex farthest along the direction, eight vertices a pass over the -// hull's single-precision rows (the reference's b3GetSupportWide). Each -// lane keeps its best dot and that vertex's index; the lanes are then -// reduced in order, the larger dot winning and the lower index on a tie, -// so the padding (vertex 0 again, past the count) never wins and the -// answer is the scalar search's first maximum. The reference embeds the -// index in each value's low mantissa bits instead (b3EmbedIndexW), which -// std.lanes cannot express yet (aether#2603); the compare here is exact -// where that one ties values within 2^-16 of each other. The caller -// measures the separation of the chosen vertex in double. -support_index_wide(h: *HullData, direction: Vec3) -> int { - // A small hull is searched in double, one vertex at a time: the lanes' - // reduction costs more than eight or sixteen dot products (measured on - // the reference's rocks, ten vertices, and boxes and six-sided - // cylinders), and the answer is the same first maximum. - if h.vertex_count <= WIDE_SUPPORT_MIN_VERTICES { return support_index(hull.points(h), h.vertex_count, direction) } - scratch_ready() - stride = hull.soa_stride(h.vertex_count) - rows = hull.soa_vertices(h) - d = math.narrow3(direction) - dx = lanes.splat8(d.x) - dy = lanes.splat8(d.y) - dz = lanes.splat8(d.z) - best = lanes.splat8(0.0 - math.infinity_f32()) - best_index = lanes.splat8(0.0 - 1.0) - index = lanes.f32x8(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0) - eight = lanes.splat8(8.0) - i = 0 - while i < stride { - x = lanes.load8(rows, i as long) - y = lanes.load8(rows, (stride + i) as long) - z = lanes.load8(rows, (2 * stride + i) as long) - dot = dx * x + dy * y + dz * z - better = lanes.gt8(dot, best) - best = lanes.select8(better, dot, best) - best_index = lanes.select8(better, index, best_index) - index = index + eight - i = i + 8 - } - // The lanes stored once and reduced in order. - out = lane_scratch() - lanes.store8(out, 0 as long, best) - lanes.store8(out, 8 as long, best_index) - values = out as f32[] - winner = values[8] - winner_dot = values[0] - lane = 1 - while lane < 8 { - v = values[lane] - if v > winner_dot || (v == winner_dot && values[8 + lane] < winner) { - winner_dot = v - winner = values[8 + lane] - } - lane = lane + 1 - } - return winner as int -} - -// The vertex farthest along the direction. -support_index(points: Vec3[], count: int, direction: Vec3) -> int { - best = 0 - best_dot = math.dot(direction, points[0]) - i = 1 - while i < count { - d = math.dot(direction, points[i]) - if d > best_dot { - best = i - best_dot = d - } - i = i + 1 - } - return best +axis_query_of(q: SatQuery) -> AxisQuery { + separated = AXIS_INVALID + if q.separated == sat.SAT_FACE_A { separated = AXIS_FACE_A } + if q.separated == sat.SAT_FACE_B { separated = AXIS_FACE_B } + if q.separated == sat.SAT_EDGE_PAIR { separated = AXIS_EDGE_PAIR } + return AxisQuery { face_a: separating_axis_of(q.face_a, AXIS_FACE_A), face_b: separating_axis_of(q.face_b, AXIS_FACE_B), + edge: separating_axis_of(q.edge, AXIS_EDGE_PAIR), separated_feature: separated } } // The axis of least penetration among the three. @@ -1770,11 +1264,9 @@ collide_hulls(m: *LocalManifold, capacity: int, hull_a: *HullData, hull_b: *Hull face_separation = math.max_float(axis_query.face_a.separation, axis_query.face_b.separation) clip_separation = cache.separation if (m.point_count == 0 && edge_query.separation > face_separation) || edge_query.separation > clip_separation + math.LINEAR_SLOP { - // The edge contact into its own point, kept only if it built one; - // the point lives in the edge axes' scratch, done with by now. + // The edge contact into its own point, kept only if it built one. scratch_ready() - edge_block = edge_axes() - edge_manifold = local_manifold(edge_block) + edge_manifold = local_manifold(edge_point()) edge_cache = empty_sat_cache() build_edge_contact(&edge_manifold, hull_a, hull_b, transform_b_to_a, edge_query, &edge_cache) if edge_manifold.point_count == 1 { diff --git a/aephysics/sat/module.ae b/aephysics/sat/module.ae new file mode 100644 index 0000000..7f1dc64 --- /dev/null +++ b/aephysics/sat/module.ae @@ -0,0 +1,641 @@ +// aephysics.sat: the separating axis test between two hulls as Box3D's wide +// one does it (convex_manifold_wide.inl, b3ComputeSeparatingAxis at e77352c, +// after Cairn Overturf, https://cairnc.github.io/posts/improvements-to-the-separating-axis/): +// single precision, eight lanes a pass, from the hulls' single-precision rows +// (hull.soa_vertices, soa_normals, edge_cosines). +// +// - The support search keeps a vertex's index in the low seven mantissa bits +// of its value (b3EmbedIndexW / b3MinIndexW), so one horizontal minimum +// gives the value and the vertex. +// - The faces' dots with the line between the centres are taken eight a pass, +// and a face whose bound cannot beat the best separation so far is skipped. +// - An edge takes part only when its Gauss-map arc can beat the best face +// (the inscribed-sphere test), culled again from the best faces' support +// points when enough pairs are left. +// - B's normals and vertices go into A's frame once, as rows, and the edge +// pairs are tested eight of A's a pass, the lanes reduced in order. +// +// The manifold module imports this twice, plain and at @target("avx2"), and +// calls the copy basics.get_simd_width() names, as Box3D calls +// b3ComputeSeparatingAxisW4 or W8 (#150, #156). Both copies give the same bits. +import std.lanes +import aephysics.math +import aephysics.native +import aephysics.basics +import aephysics.hull + +exports ( + SatAxis, SatQuery, SAT_INVALID, SAT_FACE_A, SAT_FACE_B, SAT_EDGE_PAIR, + compute, reserve_scratch, lanes_native +) + +// The axis an early return stopped on. +const SAT_INVALID = 0 +const SAT_FACE_A = 1 +const SAT_FACE_B = 2 +const SAT_EDGE_PAIR = 3 + +const NULL_INDEX = 0 - 1 + +// An axis: its normal in A's frame, pointing from A to B; its separation; +// A's face and B's vertex, A's vertex and B's face, or the two half-edges. +struct SatAxis { + normal: Vec3 + separation: float + index_a: int + index_b: int +} + +struct SatQuery { + face_a: SatAxis + face_b: SatAxis + edge: SatAxis + separated: int +} + +// A face's separation and the other hull's vertex that gives it. +struct FaceSeparation { + separation: f32 + vertex: int +} + +const PARALLEL_EDGE_TOL = 0.005 +// An edge's arc this close to degenerate is kept as a candidate (B3_PARALLEL_TOL). +const PARALLEL_TOL = 0.0001 +// The edge pair tests that pay for the probe's second cull (B3_EDGE_PROBE_MIN_TESTS). +const EDGE_PROBE_MIN_TESTS = 10 +// A vertex index fits in seven mantissa bits: a hull has at most 128 (B3_HULL_BIT_COUNT). +const HULL_BIT_MASK = 127 + +// --- scratch ---------------------------------------------------------------- + +// Each worker's arrays, in elements of four bytes: Box3D's stack arrays, NF +// faces, NE edges and NV vertices with a lane's tail each (hull.MAX_HULL_FACES, +// MAX_HULL_EDGES and MAX_HULL_VERTICES are 128). +const NF = 136 +const NE = 136 +const NV = 136 +const DOT_A = 0 // dot(normal, centre line) of A's faces +const DOT_B = 136 // of B's +const PLANE_A = 272 // plane separations of a point of B from A's faces +const PLANE_B = 408 // of a point of A from B's +const BFN_X = 544 // B's face normals in A's frame, negated +const BFN_Y = 680 +const BFN_Z = 816 +const BW_X = 952 // B's vertices in A's frame, negated +const BW_Y = 1088 +const BW_Z = 1224 +const AN0_X = 1360 // A's candidate edges: the two face normals, +const AN0_Y = 1496 +const AN0_Z = 1632 +const AN1_X = 1768 +const AN1_Y = 1904 +const AN1_Z = 2040 +const AD_X = 2176 // the edge's direction, +const AD_Y = 2312 +const AD_Z = 2448 +const AV0_X = 2584 // its first vertex, +const AV0_Y = 2720 +const AV0_Z = 2856 +const ATOL = 2992 // and its parallel tolerance +const EDGES_A = 3128 // the candidates' half-edge indices (ints) +const EDGES_B = 3264 +const LANE_SEP = 3400 // eight lanes stored for the reduction +const LANE_NX = 3408 +const LANE_NY = 3416 +const LANE_NZ = 3424 +const SCRATCH_BYTES = 13728 // 3432 elements + +var g_scratch: ptr = null + +scratch_ready() { + if g_scratch == null { g_scratch = basics.alloc(native.MAX_WORKERS * SCRATCH_BYTES) } +} + +// The scratch allocated now, on the calling thread, before a step can reach +// it from several workers at once (a world's creation reserves it). +reserve_scratch() { scratch_ready() } + +block() -> ptr { return native.scratch(g_scratch, SCRATCH_BYTES) } + +// Whether this copy holds an f32x8 in one register (true in the @target("avx2") copy on x86). +lanes_native() -> bool { return lanes.native8() } + +// --- the pieces --------------------------------------------------------------- + +round_up8(count: int) -> int { return (count + 7) & (0 - 8) } + +// The row stream `in` (x, y, z rows of `stride`) taken by out = -(R v (+ p)), +// `n` of them, into the scratch rows at ox, oy, oz (b3NegativeTransformFromSoA). +negative_transform(r: Matrix3f, p: Vec3f, rows: ptr, stride: int, n: int, out: ptr, ox: int, oy: int, oz: int, is_point: bool) { + r00 = lanes.splat8(r.cx.x) + r01 = lanes.splat8(r.cy.x) + r02 = lanes.splat8(r.cz.x) + r10 = lanes.splat8(r.cx.y) + r11 = lanes.splat8(r.cy.y) + r12 = lanes.splat8(r.cz.y) + r20 = lanes.splat8(r.cx.z) + r21 = lanes.splat8(r.cy.z) + r22 = lanes.splat8(r.cz.z) + zero = lanes.splat8(0.0) + tx = zero + ty = zero + tz = zero + if is_point { + tx = lanes.splat8(p.x) + ty = lanes.splat8(p.y) + tz = lanes.splat8(p.z) + } + i = 0 + while i < n { + x = lanes.load8(rows, i as long) + y = lanes.load8(rows, (stride + i) as long) + z = lanes.load8(rows, (2 * stride + i) as long) + wx = r00 * x + r01 * y + r02 * z + wy = r10 * x + r11 * y + r12 * z + wz = r20 * x + r21 * y + r22 * z + if is_point { + wx = wx + tx + wy = wy + ty + wz = wz + tz + } + lanes.store8(out, (ox + i) as long, -wx) + lanes.store8(out, (oy + i) as long, -wy) + lanes.store8(out, (oz + i) as long, -wz) + i = i + 8 + } +} + +// The vertex of the rows minimising bias - dot(normal, v), the bias being big +// enough to keep that positive (b3GetSupportWide). The index rides in the low +// seven mantissa bits of each value, so the padding (vertex 0 again, at an +// index past the count) never wins and a tie keeps the lower index. +support_wide(normal: Vec3f, rows: ptr, stride: int, n: int, bias: f32) -> int { + nx = lanes.splat8(normal.x) + ny = lanes.splat8(normal.y) + nz = lanes.splat8(normal.z) + b = lanes.splat8(bias) + least = lanes.splat8(math.infinity_f32()) + clear = lanes.i32splat8(0 - 128) + lane_index = lanes.i32x8(0, 1, 2, 3, 4, 5, 6, 7) + i = 0 + while i < n { + d = nx * lanes.load8(rows, i as long) + ny * lanes.load8(rows, (stride + i) as long) + nz * lanes.load8(rows, (2 * stride + i) as long) + value = b - d + tagged = lanes.from_bits8(lanes.mask8_or(lanes.mask8_and(lanes.bits8(value), clear), lane_index + lanes.i32splat8(i))) + least = lanes.min8(least, tagged) + i = i + 8 + } + bits = lanes.mask8_lane(lanes.bits8(lanes.splat8(lanes.reduce_min8(least))), 0 as long) + return (bits & (HULL_BIT_MASK as long)) as int +} + +// A face's separation: its plane's offset from the other hull's support along +// `direction`, the support measured exactly for the vertex the search chose +// (b3GetFaceSeparation). The bias is the hull's box's reach along it. +face_separation(direction: Vec3f, plane_separation: f32, rows: ptr, stride: int, n: int, center: Vec3f, extents: Vec3f) -> FaceSeparation { + f32 reach = 1.0625 + magnitude = math.vec3f(math.abs_f32(direction.x), math.abs_f32(direction.y), math.abs_f32(direction.z)) + bias = math.dotf(direction, center) + reach * math.dotf(magnitude, extents) + vi = support_wide(direction, rows, stride, n, bias) + v = rows as f32[] + support = direction.x * v[vi] + direction.y * v[stride + vi] + direction.z * v[2 * stride + vi] + return FaceSeparation { separation: plane_separation - support, vertex: vi } +} + +// dot(n, d) for every face normal of the hull, eight a pass, into the scratch +// from `at` (b3GetFaceDots); the padding normals are zero. +face_dots(h: *HullData, d: Vec3f, out: ptr, at: int) { + stride = hull.soa_stride(h.face_count) + rows = hull.soa_normals(h) + dx = lanes.splat8(d.x) + dy = lanes.splat8(d.y) + dz = lanes.splat8(d.z) + n = round_up8(h.face_count) + i = 0 + while i < n { + m = lanes.load8(rows, i as long) * dx + lanes.load8(rows, (stride + i) as long) * dy + lanes.load8(rows, (2 * stride + i) as long) * dz + lanes.store8(out, (at + i) as long, m) + i = i + 8 + } +} + +// The plane separations of `point` from every face (b3GetFacePlaneSeparations). +plane_separations(h: *HullData, point: Vec3f, out: ptr, at: int) { + face_dots(h, point, out, at) + s = out as f32[] + planes = hull.planes(h) + i = 0 + while i < h.face_count { + s[at + i] = s[at + i] - (planes[i].offset as f32) + i = i + 1 + } +} + +// Whether an edge whose faces' normals meet at cosine c, with the other +// hull's centre at plane separations a1 and a2 from them, can reach `bound` +// anywhere on its Gauss-map arc (b3TestEdgeCandidateSorted, the inscribed +// sphere edge test, https://box2d.org/posts/2026/09/inscribed-spheres/): the +// larger endpoint, or the interior maximum of dot(d, nlerp(n1, n2, t)) when +// there is one; a nearly degenerate arc is kept. 1 or 0. +edge_candidate(a1: f32, a2: f32, c: f32, bound: f32) -> int { + hi = math.max_f32(a1, a2) + lo = math.min_f32(a1, a2) + if hi >= bound { return 1 } + f32 one = 1.0 + u = lo - c * hi + if u < (0.0 as f32) { return 0 } + s = one - c * c + if s < (PARALLEL_TOL as f32) { return 1 } + bound_term = (bound - hi) * (bound + hi) + if u * u >= bound_term * s { return 1 } + return 0 +} + +// The candidates of a list (edge indices at `at` in the scratch, `count` of +// them) that can still reach `bound` from a probe point's plane separations, +// kept in order at the front; how many (b3FilterEdgeCandidates). +filter_edge_candidates(h: *HullData, s: f32[], planes_at: int, bound: f32, lists: int[], at: int, count: int) -> int { + edges = hull.edges(h) + cosines = hull.edge_cosines(h) + kept = 0 + k = 0 + while k < count { + i = lists[at + k] + lists[at + kept] = i + kept = kept + edge_candidate(s[planes_at + edges[i].face], s[planes_at + edges[i + 1].face], cosines[i >> 1], bound) + k = k + 1 + } + return kept +} + +empty_axis() -> SatAxis { + return SatAxis { normal: math.vec3_zero(), separation: 0.0 - math.infinity(), index_a: NULL_INDEX, index_b: NULL_INDEX } +} + +// --- the test ------------------------------------------------------------------- + +// The separating axis test of hull B, at xf_b in A's frame, against hull A: +// A's faces against B's vertices, B's faces against A's, then the edge pairs +// whose Gauss-map arcs cross (Gregorius), the axis from the arcs' +// intersection. With early_return the test stops at the first axis past +// `speculative`, which it reports in `separated`. +compute(hull_a: *HullData, hull_b: *HullData, xf_b: Transform, early_return: bool, speculative: float) -> SatQuery { + scratch_ready() + s = block() + f = s as f32[] + lists = s as int[] + r = math.narrow_mat3(math.make_matrix_from_quat(xf_b.q)) + inv_r = math.transposef(r) + p = math.narrow3(xf_b.p) + f32 spec = speculative as f32 + f32 slop = math.LINEAR_SLOP as f32 + f32 one_thousandth = 0.001 + f32 a_tenth = 0.1 + res = SatQuery { face_a: empty_axis(), face_b: empty_axis(), edge: empty_axis(), separated: SAT_INVALID } + f32 best_a = 0.0 - math.infinity_f32() + f32 best_b = 0.0 - math.infinity_f32() + f32 best_edge = 0.0 - math.infinity_f32() + f32 no_floor = 0.0 - math.infinity_f32() + + face_count_a = hull_a.face_count + planes_a = hull.planes(hull_a) + normals_a = hull.soa_normals(hull_a) as f32[] + face_stride_a = hull.soa_stride(face_count_a) + vertex_stride_b = hull.soa_stride(hull_b.vertex_count) + rows_b = hull.soa_vertices(hull_b) + wide_vertex_count_b = round_up8(hull_b.vertex_count) + lower_b = math.narrow3(hull_b.aabb.lower) + upper_b = math.narrow3(hull_b.aabb.upper) + f32 half = 0.5 + c_b = math.mul_svf(half, math.addf(lower_b, upper_b)) + h_b = math.mul_svf(half, math.subf(upper_b, lower_b)) + + // No axis separates the hulls by more than dot(axis, centre line) less + // their inner radii: a face whose bound is under the best separation so + // far needs no support search. The slack keeps the best face from being + // skipped. + center_a = math.narrow3(hull_a.center) + center_b = math.narrow3(hull_b.center) + delta_center = math.subf(math.addf(math.mul_mvf(r, center_b), p), center_a) + center_distance = math.lengthf(delta_center) + radius = (hull_a.inner_radius as f32) + (hull_b.inner_radius as f32) + radius_bound = radius - (slop + one_thousandth * (center_distance + radius)) + + face_dots(hull_a, delta_center, s, DOT_A) + // The face of A most aligned with the centre line: the seed. + seed_a = 0 + max_dot = f[DOT_A] + i = 1 + while i < face_count_a { + if f[DOT_A + i] > max_dot { + max_dot = f[DOT_A + i] + seed_a = i + } + i = i + 1 + } + floor_a = no_floor + seed_separation_a = no_floor + seed_vertex_b = 0 + if early_return { + n = math.vec3f(normals_a[seed_a], normals_a[face_stride_a + seed_a], normals_a[2 * face_stride_a + seed_a]) + direction = math.negf(math.mul_mvf(inv_r, n)) + plane_separation = math.dotf(n, p) - (planes_a[seed_a].offset as f32) + seed = face_separation(direction, plane_separation, rows_b, vertex_stride_b, wide_vertex_count_b, c_b, h_b) + seed_separation_a = seed.separation + seed_vertex_b = seed.vertex + floor_a = math.min_f32(seed_separation_a, spec) + } + + // A's face planes against B's vertices. + i = 0 + while i < face_count_a { + if f[DOT_A + i] - radius_bound >= math.max_f32(floor_a, best_a) { + n = math.vec3f(normals_a[i], normals_a[face_stride_a + i], normals_a[2 * face_stride_a + i]) + vertex_index = seed_vertex_b + separation = seed_separation_a + if early_return == false || i != seed_a { + direction = math.negf(math.mul_mvf(inv_r, n)) + plane_separation = math.dotf(n, p) - (planes_a[i].offset as f32) + fs = face_separation(direction, plane_separation, rows_b, vertex_stride_b, wide_vertex_count_b, c_b, h_b) + separation = fs.separation + vertex_index = fs.vertex + } + if separation > best_a { + best_a = separation + res.face_a = SatAxis { normal: math.widen3(n), separation: separation as float, index_a: i, index_b: vertex_index } + if separation > spec && early_return { + res.separated = SAT_FACE_A + return res + } + } + } + i = i + 1 + } + + face_count_b = hull_b.face_count + planes_b = hull.planes(hull_b) + normals_b = hull.soa_normals(hull_b) as f32[] + face_stride_b = hull.soa_stride(face_count_b) + vertex_stride_a = hull.soa_stride(hull_a.vertex_count) + rows_a = hull.soa_vertices(hull_a) + wide_vertex_count_a = round_up8(hull_a.vertex_count) + lower_a = math.narrow3(hull_a.aabb.lower) + upper_a = math.narrow3(hull_a.aabb.upper) + c_a = math.mul_svf(half, math.addf(lower_a, upper_a)) + h_a = math.mul_svf(half, math.subf(upper_a, lower_a)) + + // The same for B, from the centre line taken into B's frame and reversed. + face_dots(hull_b, math.negf(math.mul_mvf(inv_r, delta_center)), s, DOT_B) + seed_b = 0 + max_dot = f[DOT_B] + i = 1 + while i < face_count_b { + if f[DOT_B + i] > max_dot { + max_dot = f[DOT_B + i] + seed_b = i + } + i = i + 1 + } + floor_b = no_floor + seed_separation_b = no_floor + seed_vertex_a = 0 + if early_return { + n = math.vec3f(normals_b[seed_b], normals_b[face_stride_b + seed_b], normals_b[2 * face_stride_b + seed_b]) + direction = math.negf(math.mul_mvf(r, n)) + plane_separation = math.dotf(direction, p) - (planes_b[seed_b].offset as f32) + seed = face_separation(direction, plane_separation, rows_a, vertex_stride_a, wide_vertex_count_a, c_a, h_a) + seed_separation_b = seed.separation + seed_vertex_a = seed.vertex + // A's faces have set a floor of their own. + floor_b = math.min_f32(math.max_f32(seed_separation_b, best_a), spec) + } + + // B's face planes against A's vertices. + i = 0 + while i < face_count_b { + if f[DOT_B + i] - radius_bound >= math.max_f32(floor_b, best_b) { + n = math.vec3f(normals_b[i], normals_b[face_stride_b + i], normals_b[2 * face_stride_b + i]) + direction = math.negf(math.mul_mvf(r, n)) + vertex_index = seed_vertex_a + separation = seed_separation_b + if early_return == false || i != seed_b { + plane_separation = math.dotf(direction, p) - (planes_b[i].offset as f32) + fs = face_separation(direction, plane_separation, rows_a, vertex_stride_a, wide_vertex_count_a, c_a, h_a) + separation = fs.separation + vertex_index = fs.vertex + } + if separation > best_b { + best_b = separation + res.face_b = SatAxis { normal: math.widen3(direction), separation: separation as float, index_a: vertex_index, index_b: i } + if separation > spec && early_return { + res.separated = SAT_FACE_B + return res + } + } + } + i = i + 1 + } + + // The edges that can still beat the best face, by the inscribed-sphere + // test of each edge's arc against the other hull's centre. Every edge + // without an early return. + edges_a = hull.edges(hull_a) + edges_b = hull.edges(hull_b) + max_face_separation = math.max_f32(best_a, best_b) + bound_slack = radius - radius_bound + threshold_a = no_floor + threshold_b = no_floor + if early_return { + threshold_a = max_face_separation + (hull_b.inner_radius as f32) - bound_slack + threshold_b = max_face_separation + (hull_a.inner_radius as f32) - bound_slack + } + center_b_in_a = math.addf(math.mul_mvf(r, center_b), p) + center_a_in_b = math.mul_mvf(inv_r, math.subf(center_a, p)) + plane_separations(hull_a, center_b_in_a, s, PLANE_A) + plane_separations(hull_b, center_a_in_b, s, PLANE_B) + cosines_a = hull.edge_cosines(hull_a) + cosines_b = hull.edge_cosines(hull_b) + na = 0 + i = 0 + while i < hull_a.edge_count { + lists[EDGES_A + na] = i + na = na + edge_candidate(f[PLANE_A + edges_a[i].face], f[PLANE_A + edges_a[i + 1].face], cosines_a[i >> 1], threshold_a) + i = i + 2 + } + nb = 0 + i = 0 + while i < hull_b.edge_count { + lists[EDGES_B + nb] = i + nb = nb + edge_candidate(f[PLANE_B + edges_b[i].face], f[PLANE_B + edges_b[i + 1].face], cosines_b[i >> 1], threshold_b) + i = i + 2 + } + + // The best faces' support vertices are points of the other hull, so no + // edge pair's axis separates by more than such a point's plane + // separations over the arc: with enough pairs left to pay for it, each + // list is culled again against them. + if early_return && nb * ((na + 3) >> 2) >= EDGE_PROBE_MIN_TESTS { + probe_bound = max_face_separation - (a_tenth * slop + one_thousandth * math.abs_f32(center_distance + radius)) + rb = rows_b as f32[] + vertex_b = seed_vertex_b + if res.face_a.index_b != NULL_INDEX { vertex_b = res.face_a.index_b } + probe_b = math.vec3f(rb[vertex_b], rb[vertex_stride_b + vertex_b], rb[2 * vertex_stride_b + vertex_b]) + plane_separations(hull_a, math.addf(math.mul_mvf(r, probe_b), p), s, PLANE_A) + na = filter_edge_candidates(hull_a, f, PLANE_A, probe_bound, lists, EDGES_A, na) + ra = rows_a as f32[] + vertex_a = seed_vertex_a + if res.face_b.index_a != NULL_INDEX { vertex_a = res.face_b.index_a } + probe_a = math.vec3f(ra[vertex_a], ra[vertex_stride_a + vertex_a], ra[2 * vertex_stride_a + vertex_a]) + plane_separations(hull_b, math.mul_mvf(inv_r, math.subf(probe_a, p)), s, PLANE_B) + nb = filter_edge_candidates(hull_b, f, PLANE_B, probe_bound, lists, EDGES_B, nb) + } + if na == 0 || nb == 0 { return res } + + // B's face normals and vertices into A's frame, negated, as rows. + negative_transform(r, p, hull.soa_normals(hull_b), face_stride_b, round_up8(face_count_b), s, BFN_X, BFN_Y, BFN_Z, false) + negative_transform(r, p, rows_b, vertex_stride_b, wide_vertex_count_b, s, BW_X, BW_Y, BW_Z, true) + + // A's candidate edges as rows: the two face normals, the direction + // v1 - v0, the first vertex, and the parallel tolerance scaled by the + // edge's length. + ra = rows_a as f32[] + f32 tol = PARALLEL_EDGE_TOL as f32 + squared_tol = tol * tol + k = 0 + while k < na { + e = lists[EDGES_A + k] + twin = e + 1 + f0 = edges_a[e].face + f1 = edges_a[twin].face + f[AN0_X + k] = normals_a[f0] + f[AN0_Y + k] = normals_a[face_stride_a + f0] + f[AN0_Z + k] = normals_a[2 * face_stride_a + f0] + f[AN1_X + k] = normals_a[f1] + f[AN1_Y + k] = normals_a[face_stride_a + f1] + f[AN1_Z + k] = normals_a[2 * face_stride_a + f1] + v0 = edges_a[e].origin + v1 = edges_a[twin].origin + dx = ra[v1] - ra[v0] + dy = ra[vertex_stride_a + v1] - ra[vertex_stride_a + v0] + dz = ra[2 * vertex_stride_a + v1] - ra[2 * vertex_stride_a + v0] + f[AD_X + k] = dx + f[AD_Y + k] = dy + f[AD_Z + k] = dz + f[AV0_X + k] = ra[v0] + f[AV0_Y + k] = ra[vertex_stride_a + v0] + f[AV0_Z + k] = ra[2 * vertex_stride_a + v0] + f[ATOL + k] = squared_tol * (dx * dx + dy * dy + dz * dz) + k = k + 1 + } + // The tail lanes zero: a zero direction and zero normals fail the arc + // test, so a lane past the count never wins. + zero = lanes.splat8(0.0) + lanes.store8(s, (AN0_X + na) as long, zero) + lanes.store8(s, (AN0_Y + na) as long, zero) + lanes.store8(s, (AN0_Z + na) as long, zero) + lanes.store8(s, (AN1_X + na) as long, zero) + lanes.store8(s, (AN1_Y + na) as long, zero) + lanes.store8(s, (AN1_Z + na) as long, zero) + lanes.store8(s, (AD_X + na) as long, zero) + lanes.store8(s, (AD_Y + na) as long, zero) + lanes.store8(s, (AD_Z + na) as long, zero) + lanes.store8(s, (AV0_X + na) as long, zero) + lanes.store8(s, (AV0_Y + na) as long, zero) + lanes.store8(s, (AV0_Z + na) as long, zero) + lanes.store8(s, (ATOL + na) as long, zero) + + // Every candidate of B against eight of A's a pass. This tolerance skips + // edges shorter than about a centimetre. + eps = lanes.splat8(0.0 - slop * slop) + inf = lanes.splat8(math.infinity_f32()) + one = lanes.splat8(1.0) + m = 0 + while m < nb { + e = lists[EDGES_B + m] + twin = e + 1 + f0 = edges_b[e].face + f1 = edges_b[twin].face + v0 = edges_b[e].origin + v1 = edges_b[twin].origin + cx = lanes.splat8(f[BFN_X + f0]) + cy = lanes.splat8(f[BFN_Y + f0]) + cz = lanes.splat8(f[BFN_Z + f0]) + nd_x = lanes.splat8(f[BFN_X + f1]) + nd_y = lanes.splat8(f[BFN_Y + f1]) + nd_z = lanes.splat8(f[BFN_Z + f1]) + pbx = f[BW_X + v0] + pby = f[BW_Y + v0] + pbz = f[BW_Z + v0] + dcx = lanes.splat8(f[BW_X + v1] - pbx) + dcy = lanes.splat8(f[BW_Y + v1] - pby) + dcz = lanes.splat8(f[BW_Z + v1] - pbz) + bvx = lanes.splat8(pbx) + bvy = lanes.splat8(pby) + bvz = lanes.splat8(pbz) + i = 0 + while i < na { + ix = i as long + ax = lanes.load8(s, AD_X as long + ix) + ay = lanes.load8(s, AD_Y as long + ix) + az = lanes.load8(s, AD_Z as long + ix) + n0x = lanes.load8(s, AN0_X as long + ix) + n0y = lanes.load8(s, AN0_Y as long + ix) + n0z = lanes.load8(s, AN0_Z as long + ix) + n1x = lanes.load8(s, AN1_X as long + ix) + n1y = lanes.load8(s, AN1_Y as long + ix) + n1z = lanes.load8(s, AN1_Z as long + ix) + cba = cx * ax + cy * ay + cz * az + dba = nd_x * ax + nd_y * ay + nd_z * az + adc = n0x * dcx + n0y * dcy + n0z * dcz + bdc = n1x * dcx + n1y * dcy + n1z * dcz + // The Gauss-map arcs cross, and the edges are not near parallel + // (the arc's lerp is ill conditioned when both of B's normals are + // nearly perpendicular to A's edge). + m1 = lanes.lt8(cba * dba, eps) + m2 = lanes.lt8(adc * bdc, eps) + m3 = lanes.lt8(cba * bdc, eps) + not_parallel = lanes.gt8(lanes.max8(cba * cba, dba * dba), lanes.load8(s, ATOL as long + ix)) + mask = lanes.mask8_and(lanes.mask8_and(m1, m2), lanes.mask8_and(m3, not_parallel)) + // Most of A's edges fail it: then no divide, square root or support. + if lanes.any8(mask) { + t = (-cba) / (dba - cba) + nx = cx + t * (nd_x - cx) + ny = cy + t * (nd_y - cy) + nz = cz + t * (nd_z - cz) + inv = one / lanes.sqrt8(nx * nx + ny * ny + nz * nz) + nx = inv * nx + ny = inv * ny + nz = inv * nz + support = (lanes.load8(s, AV0_X as long + ix) + bvx) * nx + (lanes.load8(s, AV0_Y as long + ix) + bvy) * ny + (lanes.load8(s, AV0_Z as long + ix) + bvz) * nz + // A lane that fails the arc test can never win. + lane_separation = -lanes.select8(mask, support, inf) + if lanes.any8(lanes.gt8(lane_separation, lanes.splat8(best_edge))) { + lanes.store8(s, LANE_SEP as long, lane_separation) + lanes.store8(s, LANE_NX as long, nx) + lanes.store8(s, LANE_NY as long, ny) + lanes.store8(s, LANE_NZ as long, nz) + // In lane order, so a tie keeps the first edge and an early + // return takes the first improving lane past the distance. + lane = 0 + while lane < 8 { + sl = f[LANE_SEP + lane] + if sl > best_edge { + best_edge = sl + res.edge = SatAxis { normal: math.vec3(f[LANE_NX + lane] as float, f[LANE_NY + lane] as float, f[LANE_NZ + lane] as float), + separation: sl as float, index_a: lists[EDGES_A + i + lane], index_b: e } + if sl > spec && early_return { + res.separated = SAT_EDGE_PAIR + return res + } + } + lane = lane + 1 + } + } + } + i = i + 8 + } + m = m + 1 + } + return res +} diff --git a/bench/RESULTS.md b/bench/RESULTS.md index ffd7410..f3fd790 100644 --- a/bench/RESULTS.md +++ b/bench/RESULTS.md @@ -1306,6 +1306,52 @@ step, and each new box is rounded (the inline, branch-free bit step of nextafterf); a tree query no longer rounds at all, the double box compared against the widened nodes. +### The reference's wide SAT, the lanes transposed, the width at run time (2026-10-20, #150, #121, #156) + +Aether 0.835.0 brought what the last section waited on: lane moves +(aether#2550), bit casts and a horizontal minimum (#2603), and a module +imported a second time at `@target("avx2")` (#2607). + +The separating axis test is now Box3D's wide one line for line +(`aephysics.sat`, after `convex_manifold_wide.inl`), in single precision +from the hulls' rows. The support search keeps the vertex index in the low +seven mantissa bits, so one horizontal minimum gives the vertex, for every +hull size. The face dots and B's frame are taken in lanes, the edge pairs +eight at a time, and the improving lanes are reduced in order. The double +recompute is gone, which is what changed the golden trace (the wave pile +now sleeps at step 320, where it slept at 326; Box3D's own sleeps at 314). +Microseconds a query, ours before (2026-10-20, the old test) / ours after / +the reference in the same run as after, AVX2: + +| pair | SAT | SAT, no early return | +|---|---|---| +| complex/complex | 1.83 / 1.32 / 0.95 | 15.6 / 7.43 / 6.15 | +| complex/rock | 1.18 / 0.89 / 0.65 | 5.63 / 3.44 / 2.75 | +| complex/cylinder | 2.33 / 1.56 / 1.06 | 15.1 / 7.34 / 6.14 | +| rock/rock | 0.61 / 0.52 / 0.39 | 2.17 / 1.53 / 1.23 | +| rock/cylinder | 1.55 / 1.06 / 0.76 | 5.92 / 3.64 / 3.05 | +| cylinder/cylinder | 2.98 / 2.02 / 1.41 | 11.6 / 8.47 / 7.62 | + +The convex pile (5,120 hulls), ms per step, the best of three alternated +pairs: collide 7.65 to 7.07, step 11.32 to 10.74. Box3D in the same +period: collide 5.81-5.88, step 8.94-9.07, so the collide pass went from +1.31x to 1.20x. What is left in the SAT is the scalar work around the +lanes (the candidate edges' inscribed-sphere tests, a face's bias) and the +slice indexing the arrays go through. + +The wide contact stages gather and scatter their bodies through 8x8 +register transposes (Box3D's b3GatherBodies, b3ScatterBodies). The second +tangent is computed where it is used, not stored. Large pyramid, -mavx2, +medians of alternated runs: step 7.69 to 6.80 ms; warm start 0.91 to 0.72, +solve 1.36 to 1.15, relax 1.49 to 1.25. The bits did not change. + +One binary now runs both widths: the solver and the SAT each have a copy +compiled for AVX2, chosen by `basics.set_simd_width` (0 asks the CPU). In +a default build (no -mavx2), large pyramid, best of four: width 4 runs +8.18 ms a step and width 8 runs 6.77 (solve 1.57 to 1.15, relax 1.79 to +1.23), the speed of the -mavx2 build. test_determinism steps every scene +at both widths in one process, bit for bit. + ## Build flags (inlining) The generated C of every Aether function is a plain `static` function, diff --git a/tests/golden/determinism_trace.txt b/tests/golden/determinism_trace.txt index c005f3c..93dd8b5 100644 --- a/tests/golden/determinism_trace.txt +++ b/tests/golden/determinism_trace.txt @@ -390,288 +390,282 @@ wave_pile 41 -4560761182026993532 wave_pile 42 -4560679810687586226 wave_pile 43 -4560683520601692278 wave_pile 44 -4560707929601800122 -wave_pile 45 -4560814486915425934 -wave_pile 46 -4561025798763819026 -wave_pile 47 -4561243026545669106 -wave_pile 48 -4561461633981134064 -wave_pile 49 -4561576406374833044 -wave_pile 50 -4561668128929685320 -wave_pile 51 -4561755952325667810 -wave_pile 52 -4561962873306589430 -wave_pile 53 -4562094022066447192 -wave_pile 54 -4562294484135213369 -wave_pile 55 -4562418353735062663 -wave_pile 56 -4562577280778071551 -wave_pile 57 -4562783000811083977 -wave_pile 58 -4562776302805511531 -wave_pile 59 -4562950973339905433 -wave_pile 60 -4563275238572997713 -wave_pile 61 -4563562372308674602 -wave_pile 62 -4563836051292762332 -wave_pile 63 -4564069974042239122 -wave_pile 64 -4564238269483561929 -wave_pile 65 -4564372513565158346 -wave_pile 66 -4564466756525178964 -wave_pile 67 -4564623270473338113 -wave_pile 68 -4565001807535143780 -wave_pile 69 -4565366765801676036 -wave_pile 70 -4565620539603627606 -wave_pile 71 -4565720729273624074 -wave_pile 72 -4565767289600229272 -wave_pile 73 -4565721465052769528 -wave_pile 74 -4565609518065491365 -wave_pile 75 -4565427378000127453 -wave_pile 76 -4565323862256829090 -wave_pile 77 -4565244895266226472 -wave_pile 78 -4565115713375489387 -wave_pile 79 -4564962238763648032 -wave_pile 80 -4564723931389380218 -wave_pile 81 -4564516963655943976 -wave_pile 82 -4564288312579018727 -wave_pile 83 -4564077796864389079 -wave_pile 84 -4563934490716266284 -wave_pile 85 -4563723953369447100 -wave_pile 86 -4563410257811319697 -wave_pile 87 -4563108947444326617 -wave_pile 88 -4562781806609697776 -wave_pile 89 -4562527018771282057 -wave_pile 90 -4562264263767140170 -wave_pile 91 -4562073313239616766 -wave_pile 92 -4561952858209983900 -wave_pile 93 -4561814910191227277 -wave_pile 94 -4561687854805571791 -wave_pile 95 -4561569398951647809 -wave_pile 96 -4561459337903087256 -wave_pile 97 -4561360557912832116 -wave_pile 98 -4561264522535023904 -wave_pile 99 -4561159867313353896 -wave_pile 100 -4561062540750736651 -wave_pile 101 -4560963094028577997 -wave_pile 102 -4560873081665402600 -wave_pile 103 -4560787923860290921 -wave_pile 104 -4560703903833723440 -wave_pile 105 -4560679854419208266 -wave_pile 106 -4560670141461396006 -wave_pile 107 -4560608979721081501 -wave_pile 108 -4560518900315643245 -wave_pile 109 -4560493897179888752 -wave_pile 110 -4560503584011693247 -wave_pile 111 -4560518168704831974 -wave_pile 112 -4560514141622601270 -wave_pile 113 -4560516751717309074 -wave_pile 114 -4560537391754846617 -wave_pile 115 -4560566883291582672 -wave_pile 116 -4560603074465234718 -wave_pile 117 -4560621699813457327 -wave_pile 118 -4560644079357921544 -wave_pile 119 -4560668492887627168 -wave_pile 120 -4560691240159996402 -wave_pile 121 -4560682240573956873 -wave_pile 122 -4560646710417473051 -wave_pile 123 -4560590889823543404 -wave_pile 124 -4560530173686748589 -wave_pile 125 -4560478671211562923 -wave_pile 126 -4560437118357052455 -wave_pile 127 -4560380614624226426 -wave_pile 128 -4560316529387596541 -wave_pile 129 -4560257288215114167 -wave_pile 130 -4560186641414480302 -wave_pile 131 -4560104465612903629 -wave_pile 132 -4560017029084645748 -wave_pile 133 -4559930926323043472 -wave_pile 134 -4559851447377391410 -wave_pile 135 -4559769235501277781 -wave_pile 136 -4559696576936621709 -wave_pile 137 -4559620359844109141 -wave_pile 138 -4559515932395435247 -wave_pile 139 -4559405921779489526 -wave_pile 140 -4559289865352713283 -wave_pile 141 -4559175586859325397 -wave_pile 142 -4559066127564622839 -wave_pile 143 -4558967870817970755 -wave_pile 144 -4558868385317388363 -wave_pile 145 -4558768166259204493 -wave_pile 146 -4558677733251088386 -wave_pile 147 -4558593293434795835 -wave_pile 148 -4558512920438579650 -wave_pile 149 -4558437522870441372 -wave_pile 150 -4558354141586221180 -wave_pile 151 -4558271649232741613 -wave_pile 152 -4558192458735683312 -wave_pile 153 -4558118829992943527 -wave_pile 154 -4558042955471181679 -wave_pile 155 -4557962598561109851 -wave_pile 156 -4557872939404367280 -wave_pile 157 -4557783959327839888 -wave_pile 158 -4557695919735020742 -wave_pile 159 -4557626477006482563 -wave_pile 160 -4557585098908663068 -wave_pile 161 -4557545922418916283 -wave_pile 162 -4557507200929880410 -wave_pile 163 -4557469112950282506 -wave_pile 164 -4557428728252305662 -wave_pile 165 -4557380480798532726 -wave_pile 166 -4557334750730034198 -wave_pile 167 -4557290397406565082 -wave_pile 168 -4557247616942164897 -wave_pile 169 -4557219049697158336 -wave_pile 170 -4557194557009827791 -wave_pile 171 -4557172123285719935 -wave_pile 172 -4557150933145556397 -wave_pile 173 -4557131226942625278 -wave_pile 174 -4557113598601252035 -wave_pile 175 -4557097546464784422 -wave_pile 176 -4557080558523750479 -wave_pile 177 -4557061804946180009 -wave_pile 178 -4557041406839384245 -wave_pile 179 -4557023026931836186 -wave_pile 180 -4557007458688543275 -wave_pile 181 -4556993132645928308 -wave_pile 182 -4556978451434670847 -wave_pile 183 -4556963200724848930 -wave_pile 184 -4556948404804299784 -wave_pile 185 -4556934645198405433 -wave_pile 186 -4556931851782187417 -wave_pile 187 -4556930328706042439 -wave_pile 188 -4556928710723211735 -wave_pile 189 -4556927126518236226 -wave_pile 190 -4556926797408112873 -wave_pile 191 -4556927439517184100 -wave_pile 192 -4556927016072355748 -wave_pile 193 -4556925200140748713 -wave_pile 194 -4556924296609722914 -wave_pile 195 -4556923683581025332 -wave_pile 196 -4556923892391620409 -wave_pile 197 -4556925105517505426 -wave_pile 198 -4556925947835889851 -wave_pile 199 -4556927912567312004 -wave_pile 200 -4556930715444180668 -wave_pile 201 -4556935258170017849 -wave_pile 202 -4556942683551148865 -wave_pile 203 -4556950478530675372 -wave_pile 204 -4556960044318637239 -wave_pile 205 -4556971348427993039 -wave_pile 206 -4556983719287246337 -wave_pile 207 -4556997322416657163 -wave_pile 208 -4557012681912476185 -wave_pile 209 -4557029466932138082 -wave_pile 210 -4557046341698568073 -wave_pile 211 -4557063453266917000 -wave_pile 212 -4557080952051966962 -wave_pile 213 -4557098948241768162 -wave_pile 214 -4557117192584448274 -wave_pile 215 -4557135534942430219 -wave_pile 216 -4557154555927582973 -wave_pile 217 -4557173964467283743 -wave_pile 218 -4557193642872323630 -wave_pile 219 -4557213555786209082 -wave_pile 220 -4557233346696509135 -wave_pile 221 -4557253074400282850 -wave_pile 222 -4557273382699787004 -wave_pile 223 -4557294073413604464 -wave_pile 224 -4557314860080136683 -wave_pile 225 -4557335959866188428 -wave_pile 226 -4557356912505506784 -wave_pile 227 -4557377805793099191 -wave_pile 228 -4557398654152132213 -wave_pile 229 -4557419449575869029 -wave_pile 230 -4557442605071223977 -wave_pile 231 -4557465266875978605 -wave_pile 232 -4557487665556278879 -wave_pile 233 -4557509803998868085 -wave_pile 234 -4557531751324967377 -wave_pile 235 -4557547770744411771 -wave_pile 236 -4557563409068434390 -wave_pile 237 -4557578686882285402 -wave_pile 238 -4557593520091547620 -wave_pile 239 -4557607942047450759 -wave_pile 240 -4557621887939468945 -wave_pile 241 -4557635398009057339 -wave_pile 242 -4557654078000595842 -wave_pile 243 -4557678840085111502 -wave_pile 244 -4557702411918414670 -wave_pile 245 -4557724865308361874 -wave_pile 246 -4557746212485965154 -wave_pile 247 -4557766452295051462 -wave_pile 248 -4557785657243486782 -wave_pile 249 -4557803824044254506 -wave_pile 250 -4557821035548985806 -wave_pile 251 -4557837257588397546 -wave_pile 252 -4557852608645132644 -wave_pile 253 -4557867018213454804 -wave_pile 254 -4557880722100804392 -wave_pile 255 -4557893743081427732 -wave_pile 256 -4557906227698670368 -wave_pile 257 -4557918026952649308 -wave_pile 258 -4557929340917822710 -wave_pile 259 -4557940265689794854 -wave_pile 260 -4557951108736888538 -wave_pile 261 -4557961867849147560 -wave_pile 262 -4557972481809648796 -wave_pile 263 -4557983001983326420 -wave_pile 264 -4557993420352578982 -wave_pile 265 -4558003733717301196 -wave_pile 266 -4558013948749230878 -wave_pile 267 -4558024010863832138 -wave_pile 268 -4558033927224185080 -wave_pile 269 -4558043712659458760 -wave_pile 270 -4558053381086122532 -wave_pile 271 -4558062955310028680 -wave_pile 272 -4558072457747273258 -wave_pile 273 -4558081802758220530 -wave_pile 274 -4558085175898645880 -wave_pile 275 -4558091204220986826 -wave_pile 276 -4558096887833064496 -wave_pile 277 -4558102412140024880 -wave_pile 278 -4558107621270055016 -wave_pile 279 -4558112472522628298 -wave_pile 280 -4558116788321934638 -wave_pile 281 -4558120441495125584 -wave_pile 282 -4558123379977063872 -wave_pile 283 -4558125786454034900 -wave_pile 284 -4558127838788717442 -wave_pile 285 -4558129627372625426 -wave_pile 286 -4558131210223167886 -wave_pile 287 -4558132600211424862 -wave_pile 288 -4558134143569433580 -wave_pile 289 -4558135659813650720 -wave_pile 290 -4558137065570226618 -wave_pile 291 -4558138441170132730 -wave_pile 292 -4558140350102285768 -wave_pile 293 -4558141567055137348 -wave_pile 294 -4558142399832265506 -wave_pile 295 -4558143075298799858 -wave_pile 296 -4558143019006755352 -wave_pile 297 -4558142930195722996 -wave_pile 298 -4558142988090735028 -wave_pile 299 -4558142974149967902 -wave_pile 300 -4558142970364541780 -wave_pile 301 -4558142976820610284 -wave_pile 302 -4558142973575942476 -wave_pile 303 -4558142974443271348 -wave_pile 304 -4558142974710699690 -wave_pile 305 -4558142974373075914 -wave_pile 306 -4558142974528844296 -wave_pile 307 -4558142974532457494 -wave_pile 308 -4558142974512073750 -wave_pile 309 -4558142974524364072 -wave_pile 310 -4558142974519717712 -wave_pile 311 -4558142974521410276 -wave_pile 312 -4558142974522108296 -wave_pile 313 -4558142974521670564 -wave_pile 314 -4558142974522038570 -wave_pile 315 -4558142974522018606 -wave_pile 316 -4558142974522016742 -wave_pile 317 -4558142974522062048 -wave_pile 318 -4558142974522053608 -wave_pile 319 -4558142974522060064 -wave_pile 320 -4558142974522063598 -wave_pile 321 -4558142974522063094 -wave_pile 322 -4558142974522064726 -wave_pile 323 -4558142974522065082 -wave_pile 324 -4558142974522065624 -wave_pile 325 -4558142974522066346 -wave_pile 326 -4558142974522066740 +wave_pile 45 -4560814486917084626 +wave_pile 46 -4561025798758990926 +wave_pile 47 -4561243026523442028 +wave_pile 48 -4561461633995377438 +wave_pile 49 -4561576406418842408 +wave_pile 50 -4561668128998501332 +wave_pile 51 -4561755952394406638 +wave_pile 52 -4561962873413257904 +wave_pile 53 -4562094022225201862 +wave_pile 54 -4562294483833736389 +wave_pile 55 -4562418352674314655 +wave_pile 56 -4562577279154375457 +wave_pile 57 -4562782999852925045 +wave_pile 58 -4562776302242844563 +wave_pile 59 -4562950974574071515 +wave_pile 60 -4563275241216102237 +wave_pile 61 -4563562474106518720 +wave_pile 62 -4563837833811705112 +wave_pile 63 -4564072680788595840 +wave_pile 64 -4564242051135372177 +wave_pile 65 -4564377260406189038 +wave_pile 66 -4564472643905131112 +wave_pile 67 -4564630273448757327 +wave_pile 68 -4565009498952958174 +wave_pile 69 -4565376110393556440 +wave_pile 70 -4565634544282429732 +wave_pile 71 -4565738901002335706 +wave_pile 72 -4565786608007577748 +wave_pile 73 -4565739497055016683 +wave_pile 74 -4565625940435931357 +wave_pile 75 -4565442946664116471 +wave_pile 76 -4565336681093981955 +wave_pile 77 -4565256719355324629 +wave_pile 78 -4565127511949510363 +wave_pile 79 -4564972489928935964 +wave_pile 80 -4564736089765420443 +wave_pile 81 -4564525610306698724 +wave_pile 82 -4564295113079877307 +wave_pile 83 -4564084151528587063 +wave_pile 84 -4563936290682319380 +wave_pile 85 -4563724898482777306 +wave_pile 86 -4563410154651488410 +wave_pile 87 -4563112993357116641 +wave_pile 88 -4562783135520870497 +wave_pile 89 -4562526620728485287 +wave_pile 90 -4562262650330304010 +wave_pile 91 -4562071119360488179 +wave_pile 92 -4561954832334548108 +wave_pile 93 -4561817387115612309 +wave_pile 94 -4561690604059130689 +wave_pile 95 -4561575806380721433 +wave_pile 96 -4561469046733115314 +wave_pile 97 -4561375763877443431 +wave_pile 98 -4561282958462261707 +wave_pile 99 -4561185160043618496 +wave_pile 100 -4561099028876638311 +wave_pile 101 -4561007095162237044 +wave_pile 102 -4560920728534519564 +wave_pile 103 -4560833608498479305 +wave_pile 104 -4560748105960388675 +wave_pile 105 -4560718777341817383 +wave_pile 106 -4560682946066409788 +wave_pile 107 -4560608674243466473 +wave_pile 108 -4560526218111747865 +wave_pile 109 -4560490810379193183 +wave_pile 110 -4560491353399292224 +wave_pile 111 -4560503008454891444 +wave_pile 112 -4560492740593170120 +wave_pile 113 -4560492269266974270 +wave_pile 114 -4560511989699630295 +wave_pile 115 -4560532182303803959 +wave_pile 116 -4560566399089243176 +wave_pile 117 -4560583147802785310 +wave_pile 118 -4560602447269957814 +wave_pile 119 -4560622904720818343 +wave_pile 120 -4560634507143851813 +wave_pile 121 -4560611102383725554 +wave_pile 122 -4560570400368650960 +wave_pile 123 -4560504494459205846 +wave_pile 124 -4560440987924959286 +wave_pile 125 -4560389464018273495 +wave_pile 126 -4560338656661679181 +wave_pile 127 -4560278807745952593 +wave_pile 128 -4560215946808554269 +wave_pile 129 -4560154086348284364 +wave_pile 130 -4560077167763513746 +wave_pile 131 -4559990260490208583 +wave_pile 132 -4559899308371332183 +wave_pile 133 -4559810040068240984 +wave_pile 134 -4559726284740034945 +wave_pile 135 -4559643057685963775 +wave_pile 136 -4559571589916814722 +wave_pile 137 -4559494421426361346 +wave_pile 138 -4559390764418318885 +wave_pile 139 -4559271756515378057 +wave_pile 140 -4559148739049271372 +wave_pile 141 -4559028802302165977 +wave_pile 142 -4558919531356037471 +wave_pile 143 -4558824999597723247 +wave_pile 144 -4558726503917137029 +wave_pile 145 -4558626445969074272 +wave_pile 146 -4558536876369800433 +wave_pile 147 -4558454574264311997 +wave_pile 148 -4558375682606625255 +wave_pile 149 -4558298438605213424 +wave_pile 150 -4558213366678036991 +wave_pile 151 -4558131122796954255 +wave_pile 152 -4558050297553069341 +wave_pile 153 -4557973951910180996 +wave_pile 154 -4557895463889269750 +wave_pile 155 -4557812645299849766 +wave_pile 156 -4557722392114403655 +wave_pile 157 -4557637454479395393 +wave_pile 158 -4557594263107946888 +wave_pile 159 -4557551888686267434 +wave_pile 160 -4557510637589765597 +wave_pile 161 -4557471632914228219 +wave_pile 162 -4557433164364376138 +wave_pile 163 -4557395413722438920 +wave_pile 164 -4557351004885492584 +wave_pile 165 -4557300522657285767 +wave_pile 166 -4557253171347031828 +wave_pile 167 -4557210562482945019 +wave_pile 168 -4557168212101330940 +wave_pile 169 -4557139485559913598 +wave_pile 170 -4557115372692221078 +wave_pile 171 -4557092885548668869 +wave_pile 172 -4557071904382865663 +wave_pile 173 -4557052544207786416 +wave_pile 174 -4557035086928029988 +wave_pile 175 -4557020153994150123 +wave_pile 176 -4557008153426803923 +wave_pile 177 -4556997610229566222 +wave_pile 178 -4556984271752517250 +wave_pile 179 -4556967940330571454 +wave_pile 180 -4556949227489763649 +wave_pile 181 -4556932066355681142 +wave_pile 182 -4556917417875616489 +wave_pile 183 -4556904316343262809 +wave_pile 184 -4556891157423208514 +wave_pile 185 -4556877080537763481 +wave_pile 186 -4556873163436436622 +wave_pile 187 -4556870720670642715 +wave_pile 188 -4556869609844269773 +wave_pile 189 -4556869860646518560 +wave_pile 190 -4556870328485848976 +wave_pile 191 -4556871568676036507 +wave_pile 192 -4556872216275026490 +wave_pile 193 -4556872595969488276 +wave_pile 194 -4556873977819338146 +wave_pile 195 -4556876320226846904 +wave_pile 196 -4556879985521869753 +wave_pile 197 -4556884116782510743 +wave_pile 198 -4556887584603127608 +wave_pile 199 -4556891880326450086 +wave_pile 200 -4556896711889183660 +wave_pile 201 -4556903771185536903 +wave_pile 202 -4556913826854054131 +wave_pile 203 -4556924173088886257 +wave_pile 204 -4556936141613560912 +wave_pile 205 -4556949572465058105 +wave_pile 206 -4556963704489786383 +wave_pile 207 -4556978983682552886 +wave_pile 208 -4556995462881546939 +wave_pile 209 -4557014206985091074 +wave_pile 210 -4557033675989494592 +wave_pile 211 -4557052773235060481 +wave_pile 212 -4557071729520830167 +wave_pile 213 -4557091459383226512 +wave_pile 214 -4557112179913074088 +wave_pile 215 -4557134698001808295 +wave_pile 216 -4557157481407237976 +wave_pile 217 -4557180450364100428 +wave_pile 218 -4557204274673697849 +wave_pile 219 -4557228298620067318 +wave_pile 220 -4557252071043035244 +wave_pile 221 -4557275847488296049 +wave_pile 222 -4557299623193473063 +wave_pile 223 -4557323325113404602 +wave_pile 224 -4557346962724684949 +wave_pile 225 -4557367800081520175 +wave_pile 226 -4557388071054750637 +wave_pile 227 -4557408327909399566 +wave_pile 228 -4557422920770162646 +wave_pile 229 -4557437289271480579 +wave_pile 230 -4557453951992861527 +wave_pile 231 -4557470041935959683 +wave_pile 232 -4557485757813271465 +wave_pile 233 -4557500788227555250 +wave_pile 234 -4557515366006469759 +wave_pile 235 -4557529440529671688 +wave_pile 236 -4557543063660794687 +wave_pile 237 -4557556231009640205 +wave_pile 238 -4557568984074334611 +wave_pile 239 -4557581318241871239 +wave_pile 240 -4557593255994604142 +wave_pile 241 -4557604811582191550 +wave_pile 242 -4557615990003616070 +wave_pile 243 -4557626586532630540 +wave_pile 244 -4557636660771445770 +wave_pile 245 -4557649706553959400 +wave_pile 246 -4557667928293522972 +wave_pile 247 -4557685266426275360 +wave_pile 248 -4557701734046961692 +wave_pile 249 -4557717624374192016 +wave_pile 250 -4557732689474602994 +wave_pile 251 -4557747097911664706 +wave_pile 252 -4557760858562974958 +wave_pile 253 -4557773995499435642 +wave_pile 254 -4557786535103101090 +wave_pile 255 -4557798495910966494 +wave_pile 256 -4557809911128295454 +wave_pile 257 -4557820815569487310 +wave_pile 258 -4557831254715474738 +wave_pile 259 -4557841348392150706 +wave_pile 260 -4557851328999790742 +wave_pile 261 -4557861199501328794 +wave_pile 262 -4557870915507723386 +wave_pile 263 -4557880531356773642 +wave_pile 264 -4557890062748605786 +wave_pile 265 -4557899503794309302 +wave_pile 266 -4557908104618018622 +wave_pile 267 -4557911049782510286 +wave_pile 268 -4557916564937990714 +wave_pile 269 -4557921837204218774 +wave_pile 270 -4557926910852792370 +wave_pile 271 -4557931695899117938 +wave_pile 272 -4557936153941175930 +wave_pile 273 -4557940111408024270 +wave_pile 274 -4557943435850698370 +wave_pile 275 -4557946068334681202 +wave_pile 276 -4557948205238821286 +wave_pile 277 -4557950022766406898 +wave_pile 278 -4557951604236307730 +wave_pile 279 -4557952994225933614 +wave_pile 280 -4557954227941089762 +wave_pile 281 -4557955567744306090 +wave_pile 282 -4557956922524145978 +wave_pile 283 -4557958177763385210 +wave_pile 284 -4557959418407841162 +wave_pile 285 -4557960976982057570 +wave_pile 286 -4557961990817252834 +wave_pile 287 -4557962694193467630 +wave_pile 288 -4557963261357500962 +wave_pile 289 -4557963831636793698 +wave_pile 290 -4557963808430838106 +wave_pile 291 -4557963714185548094 +wave_pile 292 -4557963744550230154 +wave_pile 293 -4557963747261949822 +wave_pile 294 -4557963737450839778 +wave_pile 295 -4557963743316057738 +wave_pile 296 -4557963741444708174 +wave_pile 297 -4557963741267783606 +wave_pile 298 -4557963741763365798 +wave_pile 299 -4557963741438759902 +wave_pile 300 -4557963741510108914 +wave_pile 301 -4557963741531598698 +wave_pile 302 -4557963741512246002 +wave_pile 303 -4557963741520800382 +wave_pile 304 -4557963741517166442 +wave_pile 305 -4557963741516252882 +wave_pile 306 -4557963741516889538 +wave_pile 307 -4557963741516186622 +wave_pile 308 -4557963741516226138 +wave_pile 309 -4557963741516191614 +wave_pile 310 -4557963741516103450 +wave_pile 311 -4557963741516110838 +wave_pile 312 -4557963741516091942 +wave_pile 313 -4557963741516082158 +wave_pile 314 -4557963741516080598 +wave_pile 315 -4557963741516075606 +wave_pile 316 -4557963741516073154 +wave_pile 317 -4557963741516071358 +wave_pile 318 -4557963741516070566 +wave_pile 319 -4557963741516069890 +wave_pile 320 -4557963741516069282 query_spawn 1 -4559988040411345615 query_spawn 2 -4559260839560390653 query_spawn 3 -4559918509271973339 diff --git a/tests/test_collision.ae b/tests/test_collision.ae index f7cdd62..6ba045a 100644 --- a/tests/test_collision.ae +++ b/tests/test_collision.ae @@ -172,11 +172,11 @@ main() { test_ray_aabb() test_large_world_manifold() test_large_world_aabb() - // The manifold module's scratch, seven blocks made on its first use and - // kept (each worker's speculative distance, the clip polygons, the - // points, the edge rows and lists, the support search's lanes), is all - // that stays. - ensure("every other counted allocation was freed (${basics.alloc_count() - before})", basics.alloc_count() == before + 7) + // The scratch made on first use and kept is all that stays: the manifold + // module's five blocks (each worker's speculative distance, the clip + // polygons, the points, the edge contact's point) and the separating + // axis test's arrays (of the copy the SIMD width runs). + ensure("every other counted allocation was freed (${basics.alloc_count() - before})", basics.alloc_count() == before + 6) println("collision: ${checks} checks") if failures == 0 { diff --git a/tests/test_manifold.ae b/tests/test_manifold.ae index 092a825..c2bf58a 100644 --- a/tests/test_manifold.ae +++ b/tests/test_manifold.ae @@ -708,7 +708,8 @@ brute_edge_separation(a: *HullData, b: *HullData, xf: Transform) -> float { points_b = hull.points(b) edges_b = hull.edges(b) tol = manifold.PARALLEL_EDGE_TOL * manifold.PARALLEL_EDGE_TOL - eps = 0.0 - 0.0001 + // The reference's arc tolerance (e77352c), -LINEAR_SLOP squared. + eps = 0.0 - math.LINEAR_SLOP * math.LINEAR_SLOP best = 0.0 - math.MAX_FLOAT i = 0 while i < a.edge_count { @@ -995,11 +996,11 @@ main() { test_hull_capsule_edge_deep() test_sphere_hull_seam() test_rounded_pairs() - // The module's scratch stays allocated: each worker's speculative - // distance, the two clip polygons, the unreduced points, the edge axes - // the separating-axis test gathers A's edges into, its two hulls' - // candidate edge lists, and the support search's lanes. - ensure("every other counted allocation was freed (${basics.alloc_count() - before})", basics.alloc_count() == before + 7) + // The modules' scratch stays allocated: each worker's speculative + // distance, the two clip polygons, the unreduced points, the edge + // contact's point, and the separating axis test's arrays (of the copy the + // SIMD width runs; nothing here reserves the other). + ensure("every other counted allocation was freed (${basics.alloc_count() - before})", basics.alloc_count() == before + 6) println("manifold: ${checks} checks") if failures == 0 {