The simulation steps at a fixed rate
RP412PHYSICSHZ names a rate and the simulation advances in whole steps of exactly that size on every machine, whatever the display does. 0 - the default, and the shipped behaviour until the play testers have spoken - is the game as it has always run: the step is however long the last frame took, which makes the frame rate part of the physics. Measured over two seconds of free fall, a 30 fps machine's pod fell three times further than a 144 fps machine's. Two players on the same track were not in the same gravity. With a rate set, the same race is bit-identical across frame rates: 30, 60 and 144 fps produce the same trajectory to the last printed digit, and identical runs reproduce exactly - which was never true of this engine before, at any frame rate. It took three pieces, and every one was found by measuring, not by reading: - Simulation::PerformTo turns lastPerformance into the accumulator it always secretly was: whole steps while time remains, the remainder carried to the next frame. Watchers and update records stay once per frame - stepping is physics, watching is I/O. - Entity::PerformAndWatch interleaves subsystems and entity per STEP. The frame loop ran all subsystems to the frame boundary and then the entity, indistinguishable from correct at one step per frame - which is why thirty years of code never noticed - and wrong at two: the thrusters raycast twice from a vehicle that had not moved, and the hover spring fired twice on one stale height sample. The subsystems are also snapped onto their entity's step grid; each Simulation anchors its grid at its own creation time, a per-run phase no seed could pin. - Mover::BeginStep clears the force accumulator per step. It was cleared once per frame while the thrusters ADD per step, so step two of a frame integrated step one's thrust again - and how many steps a frame holds rides on wall-clock jitter, which is why identical configs measured a quarter-metre apart. The quaternion renormalise counts steps now too, for the same reason. The catch-up clamp is a quarter second of simulation whatever the rate, so a machine that cannot keep up slows down rather than seizing, and does so identically everywhere. The engine's clock counts milliseconds, so rates that do not divide 1000 - 60 among them - quietly run at the neighbouring millisecond step; the log now says so and names the exact ones. 25, 50 and 100 are exact, and all three are verified bit-identical across frame rates and across runs. Verified for a single vehicle settling under gravity and hover. Driving, collisions and the network are the next frontiers, in that order: the collision path writes the victim's state with wall-clock stamps and a hard-coded 0.1 s bounce, which single-player survives and lockstep will not. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -745,6 +745,103 @@ void
|
||||
//
|
||||
if (GetInstance() != ReplicantInstance)
|
||||
{
|
||||
//
|
||||
//----------------------------------------------------------------
|
||||
// Fixed-step: the subsystems and the entity advance TOGETHER,
|
||||
// one step at a time, because they read each other mid-flight.
|
||||
// The VTV's hover spring is computed from its thrusters'
|
||||
// measured heights, and each thruster measures from where the
|
||||
// vehicle IS - so thrusters stepped twice against a vehicle
|
||||
// that has not moved yet hand back two identical height
|
||||
// samples, and the spring fires twice on stale data. Measured,
|
||||
// that pod climbs at 30 fps and flies level at 144.
|
||||
//
|
||||
// So the step loop lives HERE, above both: everyone is walked
|
||||
// to the same sub-frame instant before anyone takes the next
|
||||
// step. Watchers and the update stream still run once per
|
||||
// frame, after the loop - stepping is physics, watching is
|
||||
// I/O, and only the first belongs inside.
|
||||
//
|
||||
// The interleave keys off the ENTITY's own clock so a
|
||||
// subsystem created mid-flight (they are made alongside their
|
||||
// owner) can never wedge the loop.
|
||||
//----------------------------------------------------------------
|
||||
//
|
||||
Scalar fixed_step = Simulation::FixedStep();
|
||||
|
||||
if (fixed_step > (Scalar) 0)
|
||||
{
|
||||
//
|
||||
// One grid for the whole vehicle. Every Simulation anchors
|
||||
// its own lastPerformance at its creation time, so an
|
||||
// entity and its subsystems were stepping on grids offset
|
||||
// by a random fraction of a step - deterministic within a
|
||||
// run, DIFFERENT between runs, because creation times ride
|
||||
// on load timing. The thrusters' measurements then landed
|
||||
// a different sub-step distance from the vehicle's
|
||||
// integration every launch, which is physics drift no seed
|
||||
// can pin. Snap the subsystems onto the entity's grid; the
|
||||
// interleave below then keeps everyone in lockstep by
|
||||
// construction, and once aligned this assignment is a
|
||||
// no-op every frame after.
|
||||
//
|
||||
for (int i=0; i<subsystemCount; ++i)
|
||||
{
|
||||
if (subsystemArray[i] &&
|
||||
subsystemArray[i]->IsNonReplicantExecutable())
|
||||
{
|
||||
subsystemArray[i]->SetLastPerformance(
|
||||
GetLastPerformance());
|
||||
}
|
||||
}
|
||||
|
||||
Time step_till = GetLastPerformance();
|
||||
step_till += fixed_step;
|
||||
|
||||
while (step_till <= till)
|
||||
{
|
||||
//
|
||||
// BeginStep on the ENTITY comes before the subsystems
|
||||
// perform: the Mover's force accumulator is cleared
|
||||
// here, and the thrusters then ADD this step's forces
|
||||
// into a clean slate. The first version left that
|
||||
// clear on the per-frame path, so a two-step frame
|
||||
// integrated step one's thrust twice - and since how
|
||||
// many steps land in a frame rides on wall-clock
|
||||
// jitter, no two runs saw the same force history.
|
||||
// Identical configs measured 0.23 apart because of it.
|
||||
//
|
||||
BeginStep();
|
||||
for (int i=0; i<subsystemCount; ++i)
|
||||
{
|
||||
if (subsystemArray[i] &&
|
||||
subsystemArray[i]->IsNonReplicantExecutable())
|
||||
{
|
||||
subsystemArray[i]->BeginStep();
|
||||
subsystemArray[i]->PerformTo(step_till);
|
||||
}
|
||||
}
|
||||
Simulation::PerformTo(step_till);
|
||||
step_till += fixed_step;
|
||||
}
|
||||
|
||||
for (int i=0; i<subsystemCount; ++i)
|
||||
{
|
||||
if (subsystemArray[i] &&
|
||||
subsystemArray[i]->IsNonReplicantExecutable())
|
||||
{
|
||||
subsystemArray[i]->WatchAndWrite(update_stream);
|
||||
}
|
||||
}
|
||||
|
||||
SET_PERFORM_ENTITY();
|
||||
Simulation::WatchAndWrite(update_stream);
|
||||
Check_Fpu();
|
||||
CLEAR_PERFORM_ENTITY();
|
||||
CLEAR_PERFORM_SUBSYSTEMS();
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i=0; i<subsystemCount; ++i)
|
||||
{
|
||||
if (subsystemArray[i])
|
||||
|
||||
Reference in New Issue
Block a user