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>
1081 lines
26 KiB
C++
1081 lines
26 KiB
C++
#include "munga.h"
|
|
#pragma hdrstop
|
|
|
|
#include "simulate.h"
|
|
#include "update.h"
|
|
#include "app.h"
|
|
|
|
#if defined(TRACE_EXECUTE_WATCHERS)
|
|
static BitTrace Execute_Watchers("Execute Watchers");
|
|
#define SET_EXECUTE_WATCHERS() Execute_Watchers.Set()
|
|
#define CLEAR_EXECUTE_WATCHERS() Execute_Watchers.Clear()
|
|
#else
|
|
#define SET_EXECUTE_WATCHERS()
|
|
#define CLEAR_EXECUTE_WATCHERS()
|
|
#endif
|
|
|
|
//#############################################################################
|
|
//######################## StateIndicator ###############################
|
|
//#############################################################################
|
|
|
|
//#############################################################################
|
|
// Construction and Destruction
|
|
//
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
StateIndicator::StateIndicator():
|
|
audioWatcherSocket(NULL),
|
|
videoWatcherSocket(NULL),
|
|
gaugeWatcherSocket(NULL)
|
|
{
|
|
Check_Pointer(this);
|
|
stateCount = 0;
|
|
oldState = 0;
|
|
currentState = 0;
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
StateIndicator::StateIndicator(unsigned max_states):
|
|
audioWatcherSocket(NULL),
|
|
videoWatcherSocket(NULL),
|
|
gaugeWatcherSocket(NULL)
|
|
{
|
|
Check_Pointer(this);
|
|
stateCount = max_states;
|
|
oldState = max_states;
|
|
currentState = max_states;
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
StateIndicator::StateIndicator(const StateIndicator &state_indicator):
|
|
audioWatcherSocket(NULL),
|
|
videoWatcherSocket(NULL),
|
|
gaugeWatcherSocket(NULL)
|
|
{
|
|
Check_Pointer(this);
|
|
|
|
// Do not perform deep copy of watchers
|
|
stateCount = state_indicator.stateCount;
|
|
oldState = state_indicator.oldState;
|
|
currentState = state_indicator.currentState;
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
StateIndicator::~StateIndicator()
|
|
{
|
|
Check(this);
|
|
|
|
//
|
|
// Manual deletion of existing watchers
|
|
//
|
|
{
|
|
SChainIteratorOf<Component*> iterator(&audioWatcherSocket);
|
|
#if DEBUG_LEVEL>2
|
|
Component *component;
|
|
while ((component = iterator.ReadAndNext()) != NULL)
|
|
{
|
|
Check(component);
|
|
Dump(component->GetClassID());
|
|
}
|
|
Warn(iterator.GetSize() != 0);
|
|
#endif
|
|
iterator.DeletePlugs();
|
|
}
|
|
{
|
|
SChainIteratorOf<Component*> iterator(&videoWatcherSocket);
|
|
#if DEBUG_LEVEL>2
|
|
Component *component;
|
|
while ((component = iterator.ReadAndNext()) != NULL)
|
|
{
|
|
Check(component);
|
|
Dump(component->GetClassID());
|
|
}
|
|
Warn(iterator.GetSize() != 0);
|
|
#endif
|
|
iterator.DeletePlugs();
|
|
}
|
|
{
|
|
SChainIteratorOf<Component*> iterator(&gaugeWatcherSocket);
|
|
#if DEBUG_LEVEL>2
|
|
Component *component;
|
|
while ((component = iterator.ReadAndNext()) != NULL)
|
|
{
|
|
Check(component);
|
|
Dump(component->GetClassID());
|
|
}
|
|
Warn(iterator.GetSize() != 0);
|
|
#endif
|
|
iterator.DeletePlugs();
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//#############################################################################
|
|
// State stuff
|
|
//
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
StateIndicator&
|
|
StateIndicator::operator=(const StateIndicator &state_indicator)
|
|
{
|
|
// Do not perform assignment of watchers
|
|
stateCount = state_indicator.stateCount;
|
|
oldState = state_indicator.oldState;
|
|
currentState = state_indicator.currentState;
|
|
|
|
Check_Fpu();
|
|
return *this;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Logical
|
|
StateIndicator::operator==(const StateIndicator &state_indicator) const
|
|
{
|
|
Check_Fpu();
|
|
return
|
|
(
|
|
stateCount == state_indicator.stateCount &&
|
|
oldState == state_indicator.oldState &&
|
|
currentState == state_indicator.currentState
|
|
);
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
StateIndicator::SetState(unsigned new_state)
|
|
{
|
|
Check(this);
|
|
Verify(new_state < stateCount);
|
|
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
// See if the state really changes
|
|
//
|
|
// NOTE - the old state does change to the current state, simulating a loop
|
|
// in the state engine. If it turns out that someone is watching the
|
|
// level of the state indicator and doing their own edge detection,
|
|
// this might possibly maybe screw something up
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
oldState = currentState;
|
|
if (new_state == currentState)
|
|
{
|
|
return;
|
|
}
|
|
|
|
//
|
|
//-------------------------------------------------------------------
|
|
// If the state has changed, update the state values and then run any
|
|
// watchers
|
|
//-------------------------------------------------------------------
|
|
//
|
|
currentState = new_state;
|
|
Component *watcher;
|
|
|
|
SET_EXECUTE_WATCHERS();
|
|
|
|
// Audio
|
|
{
|
|
SChainIteratorOf<Component*> iterator(audioWatcherSocket);
|
|
Check(&iterator);
|
|
while ((watcher = iterator.ReadAndNext()) != NULL)
|
|
{
|
|
watcher->Execute();
|
|
}
|
|
}
|
|
|
|
// Video
|
|
{
|
|
SChainIteratorOf<Component*> iterator(videoWatcherSocket);
|
|
Check(&iterator);
|
|
while ((watcher = iterator.ReadAndNext()) != NULL)
|
|
{
|
|
watcher->Execute();
|
|
}
|
|
}
|
|
|
|
// Gauge
|
|
{
|
|
SChainIteratorOf<Component*> iterator(gaugeWatcherSocket);
|
|
Check(&iterator);
|
|
while ((watcher = iterator.ReadAndNext()) != NULL)
|
|
{
|
|
watcher->Execute();
|
|
}
|
|
}
|
|
|
|
CLEAR_EXECUTE_WATCHERS();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
std::ostream& operator << (std::ostream &strm, const StateIndicator &state_indicator)
|
|
{
|
|
Check(&state_indicator);
|
|
|
|
strm << "[" << state_indicator.stateCount << ",";
|
|
strm << state_indicator.oldState << ",";
|
|
strm << state_indicator.currentState << "]";
|
|
|
|
return strm;
|
|
}
|
|
|
|
//#############################################################################
|
|
// Test Support
|
|
//
|
|
Logical
|
|
StateIndicator::TestInstance() const
|
|
{
|
|
return True;
|
|
}
|
|
|
|
//#############################################################################
|
|
//########################## Simulation #################################
|
|
//#############################################################################
|
|
|
|
//#############################################################################
|
|
// Virtual Data support
|
|
//
|
|
Derivation* Simulation::GetClassDerivations()
|
|
{
|
|
static Derivation classDerivations(Receiver::GetClassDerivations(), "Simulation");
|
|
return &classDerivations;
|
|
}
|
|
|
|
Simulation::SharedData
|
|
Simulation::DefaultData(
|
|
Simulation::GetClassDerivations(),
|
|
Simulation::GetMessageHandlers(),
|
|
Simulation::GetAttributeIndex(),
|
|
Simulation::StateCount
|
|
);
|
|
|
|
//#############################################################################
|
|
// Model support
|
|
//
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
//##########################################################################
|
|
// Net clock - see SIMULATE.h for why the sender's timestamp is estimated
|
|
// rather than used as it stands.
|
|
//##########################################################################
|
|
|
|
namespace
|
|
{
|
|
enum
|
|
{
|
|
netClockMaxPeers = 16,
|
|
|
|
// Samples per rolling minimum. A peer sends one record per
|
|
// simulation per frame, so at eight vehicles and 60 fps this is
|
|
// well under a second - fast enough to follow a route change,
|
|
// long enough that the minimum means something.
|
|
netClockWindow = 128,
|
|
|
|
// The furthest back we will believe a timestamp. Beyond this the
|
|
// packet is stale or the estimate is wrong, and extrapolating a
|
|
// vehicle half a second forward does more harm than the lag we
|
|
// are correcting.
|
|
netClockMaxLagTicks = 500
|
|
};
|
|
|
|
struct PeerClock
|
|
{
|
|
HostID host;
|
|
Logical inUse;
|
|
Logical settled;
|
|
long offsetTicks; // our clock - their clock
|
|
long windowMinTicks;
|
|
int windowCount;
|
|
};
|
|
|
|
PeerClock gPeerClocks[netClockMaxPeers];
|
|
HostID gUpdateSender = 0;
|
|
Logical gUpdateSenderValid = False;
|
|
|
|
Logical NetClockEnabled()
|
|
{
|
|
static int enabled = -1;
|
|
if (enabled < 0)
|
|
{
|
|
const char *setting = getenv("RP412NETCLOCK");
|
|
enabled = (setting != NULL && atoi(setting) == 0) ? 0 : 1;
|
|
if (!enabled)
|
|
{
|
|
DEBUG_STREAM << "NetClock: disabled by RP412NETCLOCK=0 - "
|
|
<< "replicants dead-reckon from arrival time\n" << std::flush;
|
|
}
|
|
}
|
|
return enabled ? True : False;
|
|
}
|
|
|
|
PeerClock *FindPeer(HostID host)
|
|
{
|
|
PeerClock *free_slot = NULL;
|
|
for (int i = 0; i < netClockMaxPeers; ++i)
|
|
{
|
|
if (gPeerClocks[i].inUse)
|
|
{
|
|
if (gPeerClocks[i].host == host)
|
|
{
|
|
return &gPeerClocks[i];
|
|
}
|
|
}
|
|
else if (free_slot == NULL)
|
|
{
|
|
free_slot = &gPeerClocks[i];
|
|
}
|
|
}
|
|
if (free_slot != NULL)
|
|
{
|
|
free_slot->inUse = True;
|
|
free_slot->host = host;
|
|
free_slot->settled = False;
|
|
free_slot->offsetTicks = 0;
|
|
free_slot->windowMinTicks = 0;
|
|
free_slot->windowCount = 0;
|
|
}
|
|
return free_slot;
|
|
}
|
|
}
|
|
|
|
void NetClock_BeginUpdate(HostID sender)
|
|
{
|
|
gUpdateSender = sender;
|
|
gUpdateSenderValid = True;
|
|
}
|
|
|
|
void NetClock_EndUpdate()
|
|
{
|
|
gUpdateSenderValid = False;
|
|
}
|
|
|
|
void NetClock_Reset()
|
|
{
|
|
memset(gPeerClocks, 0, sizeof(gPeerClocks));
|
|
gUpdateSenderValid = False;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Simulation::ReadUpdateRecord(UpdateRecord *message)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(message);
|
|
|
|
//
|
|
//------------------------------------------------------------------
|
|
// When this update arrived is not when it was taken. Put lastUpdate
|
|
// at the sender's sampling moment, expressed in our clock, so the
|
|
// dead reckoner extrapolates over the network latency instead of
|
|
// starting from scratch once it has already elapsed.
|
|
//------------------------------------------------------------------
|
|
//
|
|
long now_ticks = Now().ticks;
|
|
long local_ticks = now_ticks;
|
|
|
|
PeerClock *peer = gUpdateSenderValid && NetClockEnabled()
|
|
? FindPeer(gUpdateSender) : NULL;
|
|
if (peer != NULL)
|
|
{
|
|
//
|
|
// sample = trueOffset + oneWayLatency, so the running minimum
|
|
// converges on the offset from above.
|
|
//
|
|
long sample = now_ticks - message->timeStamp.ticks;
|
|
|
|
if (!peer->settled)
|
|
{
|
|
peer->settled = True;
|
|
peer->offsetTicks = sample;
|
|
peer->windowMinTicks = sample;
|
|
peer->windowCount = 0;
|
|
DEBUG_STREAM << "NetClock: host " << peer->host
|
|
<< " first sample, offset " << sample << " ms\n" << std::flush;
|
|
}
|
|
else
|
|
{
|
|
if (sample < peer->windowMinTicks)
|
|
{
|
|
peer->windowMinTicks = sample;
|
|
}
|
|
if (sample < peer->offsetTicks)
|
|
{
|
|
peer->offsetTicks = sample; // a shorter path: believe it now
|
|
}
|
|
if (++peer->windowCount >= netClockWindow)
|
|
{
|
|
//
|
|
// Close the window: adopt its minimum even if it is
|
|
// LARGER than the running estimate, which is how the
|
|
// figure follows clock drift and a route that got
|
|
// slower rather than staying pinned to one old packet.
|
|
//
|
|
long moved = peer->windowMinTicks - peer->offsetTicks;
|
|
if (moved > 50 || moved < -50)
|
|
{
|
|
DEBUG_STREAM << "NetClock: host " << peer->host
|
|
<< " offset " << peer->offsetTicks << " -> "
|
|
<< peer->windowMinTicks << " ms\n" << std::flush;
|
|
}
|
|
peer->offsetTicks = peer->windowMinTicks;
|
|
peer->windowMinTicks = sample;
|
|
peer->windowCount = 0;
|
|
}
|
|
}
|
|
|
|
local_ticks = message->timeStamp.ticks + peer->offsetTicks;
|
|
|
|
//
|
|
// Never ahead of our own clock, and never further back than we
|
|
// are willing to extrapolate.
|
|
//
|
|
if (local_ticks > now_ticks)
|
|
{
|
|
local_ticks = now_ticks;
|
|
}
|
|
else if (now_ticks - local_ticks > netClockMaxLagTicks)
|
|
{
|
|
local_ticks = now_ticks - netClockMaxLagTicks;
|
|
}
|
|
}
|
|
|
|
lastUpdate.ticks = local_ticks;
|
|
SetSimulationState(message->simulationState);
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Simulation::WriteUpdateRecord(
|
|
UpdateRecord *message,
|
|
int update_model
|
|
)
|
|
{
|
|
Check(this);
|
|
Check_Pointer(message);
|
|
|
|
message->timeStamp = lastPerformance;
|
|
message->simulationState = GetSimulationState();
|
|
message->recordLength = sizeof(*message);
|
|
message->recordID = (Word)update_model;
|
|
lastUpdate = lastPerformance;
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Simulation::WriteSimulationUpdate(MemoryStream *update_stream)
|
|
{
|
|
Check(this);
|
|
Check(update_stream);
|
|
|
|
//
|
|
//-----------------
|
|
// Write the update
|
|
//-----------------
|
|
//
|
|
int bit=0;
|
|
int update_model = updateModel;
|
|
updateModel = 0;
|
|
while (update_model)
|
|
{
|
|
|
|
if (update_model & 1)
|
|
{
|
|
UpdateRecord* update = (UpdateRecord*)update_stream->GetPointer();
|
|
WriteUpdateRecord(update, bit);
|
|
update_stream->AdvancePointer(update->recordLength);
|
|
}
|
|
update_model >>= 1;
|
|
++bit;
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//#############################################################################
|
|
// Construction and Destruction
|
|
//
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Simulation::Simulation(
|
|
Simulation::ClassID class_ID,
|
|
Simulation::SharedData &virtual_data
|
|
):
|
|
Receiver(class_ID, virtual_data),
|
|
simulationState(GetSharedData()->stateCount),
|
|
audioWatcherSocket(NULL),
|
|
videoWatcherSocket(NULL),
|
|
gaugeWatcherSocket(NULL),
|
|
effectWatcherSocket(NULL)
|
|
{
|
|
Check_Pointer(this);
|
|
|
|
SetSimulationState(DefaultState);
|
|
lastPerformance = Now();
|
|
lastUpdate = lastPerformance;
|
|
activePerformance = &Simulation::DoNothingOnce;
|
|
updateModel = 0;
|
|
simulationFlags = 0;
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Simulation::~Simulation()
|
|
{
|
|
Check(this);
|
|
|
|
//
|
|
// Watchers should be deleted by renderers by now
|
|
//
|
|
#if DEBUG_LEVEL>0
|
|
{
|
|
SChainIteratorOf<Component*> iterator(&audioWatcherSocket);
|
|
Verify(iterator.GetSize() == 0);
|
|
}
|
|
{
|
|
SChainIteratorOf<Component*> iterator(&videoWatcherSocket);
|
|
Verify(iterator.GetSize() == 0);
|
|
}
|
|
{
|
|
SChainIteratorOf<Component*> iterator(&gaugeWatcherSocket);
|
|
Verify(iterator.GetSize() == 0);
|
|
}
|
|
#endif
|
|
Check_Fpu();
|
|
}
|
|
|
|
//#############################################################################
|
|
// Attribute Support
|
|
//
|
|
const Simulation::AttributePointer
|
|
Simulation::NullAttribute = NULL;
|
|
|
|
const Simulation::IndexEntry
|
|
Simulation::AttributePointers[]=
|
|
{
|
|
{
|
|
Simulation::SimulationStateAttributeID,
|
|
"SimulationState",
|
|
(Simulation::AttributePointer)&Simulation::simulationState
|
|
}
|
|
};
|
|
|
|
Simulation::AttributeIndexSet& Simulation::GetAttributeIndex()
|
|
{
|
|
static Simulation::AttributeIndexSet attributeIndex(ELEMENTS(Simulation::AttributePointers),
|
|
Simulation::AttributePointers
|
|
);
|
|
return attributeIndex;
|
|
}
|
|
|
|
void*
|
|
Simulation::GetAttributePointer(Simulation::AttributeID attribute)
|
|
{
|
|
Check(this);
|
|
|
|
AttributePointer attr =
|
|
GetSharedData()->activeAttributeIndex->Find(attribute);
|
|
Check_Fpu();
|
|
if (attr == NullAttribute)
|
|
{
|
|
return NULL;
|
|
}
|
|
else
|
|
{
|
|
return &(this->*attr);
|
|
}
|
|
}
|
|
|
|
void*
|
|
Simulation::GetAttributePointer(const char* attribute_name)
|
|
{
|
|
Check(this);
|
|
|
|
AttributePointer attr =
|
|
GetSharedData()->activeAttributeIndex->Find(attribute_name);
|
|
Check_Fpu();
|
|
if (attr == NullAttribute)
|
|
{
|
|
return NULL;
|
|
}
|
|
else
|
|
{
|
|
return &(this->*attr);
|
|
}
|
|
}
|
|
|
|
//#############################################################################
|
|
// RP412PHYSICSHZ - the size of one simulation step, as a rate in hertz.
|
|
//
|
|
// The engine simulates TO a timestamp: every entity keeps a lastPerformance
|
|
// marking how far it has been simulated, and PerformAndWatch hands Perform()
|
|
// the difference. That difference used to be however long the last frame
|
|
// took, which made the frame rate part of the physics - explicitly so, since
|
|
// Mover scales its bounce and penetration thresholds by delta_t.
|
|
//
|
|
// Advancing lastPerformance in fixed steps instead makes it the accumulator
|
|
// a fixed-step loop needs, and every Perform() in the game gets an identical
|
|
// dt without one of them being touched.
|
|
//
|
|
// 0 restores the old behaviour for comparison. The RATE is a game-feel
|
|
// decision, not a technical one: thirty years of handling constants were
|
|
// tuned against the DOS build's 40 ms steps, and RP412 has been running
|
|
// ~18 ms variable ones, so the feel has already drifted. Whatever is chosen
|
|
// here becomes the canonical physics for pods and PCs alike.
|
|
//#############################################################################
|
|
|
|
static Scalar
|
|
FixedPhysicsStep()
|
|
{
|
|
static Scalar
|
|
step = (Scalar) -1;
|
|
|
|
if (step < (Scalar) 0)
|
|
{
|
|
const char
|
|
*setting = getenv("RP412PHYSICSHZ");
|
|
|
|
//
|
|
// OFF until it is proven. The accumulator below is correct in
|
|
// isolation but measured WORSE than the frame-coupled path it
|
|
// replaces - at 30 fps the pod climbs, at 144 it barely moves -
|
|
// so something else is still rate-dependent and feeding it. Not
|
|
// a default until the trace says two frame rates agree.
|
|
//
|
|
int rate = (setting != NULL) ? atoi(setting) : 0;
|
|
|
|
//
|
|
// Guard the arithmetic rather than the taste: a rate below the
|
|
// frame rate is a legitimate choice (the pods ran at 25), but a
|
|
// step of zero or a negative one is not a choice at all.
|
|
//
|
|
if (rate < 0)
|
|
{
|
|
rate = 0;
|
|
}
|
|
if (rate > 1000)
|
|
{
|
|
rate = 1000;
|
|
}
|
|
step = (rate > 0) ? ((Scalar) 1 / (Scalar) rate) : (Scalar) 0;
|
|
|
|
DEBUG_STREAM << "Physics: ";
|
|
if (rate > 0)
|
|
{
|
|
DEBUG_STREAM << "fixed step, " << rate << " Hz";
|
|
|
|
//
|
|
// The engine's clock counts MILLISECONDS, so a step is
|
|
// really round(1000/rate) ms. A rate that does not divide
|
|
// 1000 evenly therefore runs at a neighbouring rate wearing
|
|
// this one's name - 60 asks for 16.67 ms and gets 17, which
|
|
// is 58.8 Hz. Say so, and name the rates that mean what
|
|
// they say.
|
|
//
|
|
if ((1000 % rate) != 0)
|
|
{
|
|
int step_ms = (1000 + rate / 2) / rate;
|
|
DEBUG_STREAM << " - NOT millisecond-exact, steps will run "
|
|
<< step_ms << " ms (" << (1000.0f / (float) step_ms)
|
|
<< " Hz). 25, 50 and 100 are exact";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DEBUG_STREAM << "frame-coupled (RP412PHYSICSHZ=0)";
|
|
}
|
|
DEBUG_STREAM << "\n" << std::flush;
|
|
}
|
|
return step;
|
|
}
|
|
|
|
//
|
|
// How far behind one frame may catch up: a quarter second of simulation,
|
|
// whatever the rate - enough to ride out a texture load or an alt-tab,
|
|
// short of letting a stalled machine spiral. Counted in steps because the
|
|
// loop is, so 6 steps at 25 Hz, 12 at 50, 25 at 100.
|
|
//
|
|
static int
|
|
MaximumCatchUpSteps(Scalar step)
|
|
{
|
|
int steps = (int)((Scalar) 0.25 / step);
|
|
return (steps < 4) ? 4 : steps;
|
|
}
|
|
|
|
// how many fixed steps the whole simulation has taken - the trace prints it,
|
|
// so 'is the step actually fixed' is answered by measurement not by reading
|
|
long gPhysicsStepsTaken = 0;
|
|
|
|
//#############################################################################
|
|
// Simulation Support
|
|
//
|
|
Scalar
|
|
Simulation::FixedStep()
|
|
{
|
|
return FixedPhysicsStep();
|
|
}
|
|
|
|
void
|
|
Simulation::PerformTo(const Time& till)
|
|
{
|
|
Check(this);
|
|
Check(&till);
|
|
|
|
Scalar step = FixedPhysicsStep();
|
|
|
|
if (step > (Scalar) 0)
|
|
{
|
|
//
|
|
//------------------------------------------------------------------
|
|
// Fixed step. The simulation advances in whole steps of the same
|
|
// size on every machine, and whatever is left over waits for the
|
|
// next frame - lastPerformance is the accumulator, and always was.
|
|
//
|
|
// Before this, the slice was simply however long the last frame
|
|
// took, so a 30 fps machine integrated gravity in 33 ms steps and
|
|
// a 144 fps machine in 7 ms ones. Nothing in any Perform()
|
|
// changes: it is handed a dt it can rely on instead of one that
|
|
// depended on the graphics card.
|
|
//
|
|
// NOTE the caller decides the interleaving. An entity's spring
|
|
// forces are computed from its subsystems (the VTV reads its
|
|
// thrusters' measured heights), so the subsystems and the entity
|
|
// must advance TOGETHER, one step at a time -
|
|
// Entity::PerformAndWatch owns that loop and hands everyone the
|
|
// same sub-frame 'till'. Stepping a subsystem all the way to the
|
|
// frame boundary before its owner moves at all is how the first
|
|
// attempt at this produced a pod that climbed at 30 fps and flew
|
|
// level at 144: two spring impulses from one stale height sample.
|
|
//------------------------------------------------------------------
|
|
//
|
|
Scalar behind = till - lastPerformance;
|
|
int taken = 0;
|
|
int max_steps = MaximumCatchUpSteps(step);
|
|
|
|
while (behind >= step && taken < max_steps)
|
|
{
|
|
Perform(step);
|
|
++gPhysicsStepsTaken;
|
|
lastPerformance += step;
|
|
behind -= step;
|
|
++taken;
|
|
}
|
|
|
|
//
|
|
// A machine that cannot keep up must not try to buy back the whole
|
|
// backlog next frame - that costs more time, which makes a bigger
|
|
// backlog. Drop what could not be run and carry on: the game slows
|
|
// down rather than seizing, and it does so identically everywhere.
|
|
//
|
|
if (taken >= max_steps && behind >= step)
|
|
{
|
|
lastPerformance = till;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Scalar slice = till - lastPerformance;
|
|
lastPerformance = till;
|
|
|
|
Perform(slice);
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
void
|
|
Simulation::BeginStep()
|
|
{
|
|
// nothing by default - see the header
|
|
}
|
|
|
|
void
|
|
Simulation::WatchAndWrite(MemoryStream *update_stream)
|
|
{
|
|
Check(this);
|
|
|
|
if (!AreWatchersDelayed())
|
|
{
|
|
ExecuteWatchers();
|
|
}
|
|
WriteSimulationUpdate(update_stream);
|
|
Check_Fpu();
|
|
}
|
|
|
|
void
|
|
Simulation::PerformAndWatch(
|
|
const Time& till,
|
|
MemoryStream *update_stream
|
|
)
|
|
{
|
|
Check(this);
|
|
Check(&till);
|
|
|
|
PerformTo(till);
|
|
WatchAndWrite(update_stream);
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Simulation::DoNothingOnce(Scalar)
|
|
{
|
|
NeverExecute();
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Simulation::DoNothing(Scalar)
|
|
{
|
|
Check_Fpu();
|
|
}
|
|
|
|
//#############################################################################
|
|
// Watcher Support
|
|
//
|
|
void
|
|
Simulation::ExecuteWatchers()
|
|
{
|
|
SET_EXECUTE_WATCHERS();
|
|
|
|
Component *watcher;
|
|
|
|
// Audio
|
|
{
|
|
SChainIteratorOf<Component*> iterator(audioWatcherSocket);
|
|
while ((watcher = iterator.ReadAndNext()) != NULL)
|
|
{
|
|
watcher->Execute();
|
|
}
|
|
}
|
|
|
|
// Video
|
|
{
|
|
SChainIteratorOf<Component*> iterator(videoWatcherSocket);
|
|
while ((watcher = iterator.ReadAndNext()) != NULL)
|
|
{
|
|
watcher->Execute();
|
|
}
|
|
}
|
|
|
|
// Gauge
|
|
{
|
|
SChainIteratorOf<Component*> iterator(gaugeWatcherSocket);
|
|
while ((watcher = iterator.ReadAndNext()) != NULL)
|
|
{
|
|
watcher->Execute();
|
|
}
|
|
}
|
|
|
|
// Effect
|
|
{
|
|
SChainIteratorOf<Component*> iterator(effectWatcherSocket);
|
|
while ((watcher = iterator.ReadAndNext()) != NULL)
|
|
{
|
|
watcher->Execute();
|
|
}
|
|
}
|
|
CLEAR_EXECUTE_WATCHERS();
|
|
}
|
|
|
|
//#############################################################################
|
|
// Test Support
|
|
//
|
|
|
|
Logical
|
|
Simulation::TestInstance() const
|
|
{
|
|
return IsDerivedFrom(*GetClassDerivations());
|
|
}
|
|
|
|
//#############################################################################
|
|
//################### Simulation::AttributeIndexSet #####################
|
|
//#############################################################################
|
|
|
|
const Simulation::AttributeIndexSet
|
|
Simulation::AttributeIndexSet::NullSet;
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Simulation__AttributeIndexSet::~Simulation__AttributeIndexSet()
|
|
{
|
|
if (attributeIndex)
|
|
{
|
|
Unregister_Pointer(attributeIndex);
|
|
delete[] attributeIndex;
|
|
}
|
|
};
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
Simulation::AttributeIndexSet::Build(
|
|
Simulation::AttributeID count,
|
|
const Simulation::IndexEntry index_table[],
|
|
const Simulation::AttributeIndexSet *inheritance
|
|
)
|
|
{
|
|
//
|
|
//-------------------------------------------------------
|
|
// Find out the highest message type we have to deal with
|
|
//-------------------------------------------------------
|
|
//
|
|
Check(this);
|
|
Check_Pointer(index_table);
|
|
entryCount = 0;
|
|
Simulation::AttributeID i;
|
|
for (i=0; i<count; ++i)
|
|
{
|
|
if (index_table[i].entryID > entryCount)
|
|
{
|
|
entryCount = index_table[i].entryID;
|
|
}
|
|
}
|
|
if (inheritance)
|
|
{
|
|
Check(inheritance);
|
|
if (entryCount<inheritance->entryCount)
|
|
{
|
|
entryCount = inheritance->entryCount;
|
|
}
|
|
#if DEBUG_LEVEL>0
|
|
else if (entryCount > inheritance->entryCount)
|
|
{
|
|
i = inheritance->entryCount+1;
|
|
goto Check_Table;
|
|
}
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
Verify(entryCount == count);
|
|
#if DEBUG_LEVEL>0
|
|
i = 1;
|
|
Check_Table:
|
|
while (i <= entryCount)
|
|
{
|
|
int j;
|
|
for (j=0; j<count; ++j)
|
|
{
|
|
if (index_table[j].entryID == i)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
if (j == count)
|
|
{
|
|
break;
|
|
}
|
|
++i;
|
|
}
|
|
Verify(i > count);
|
|
#endif
|
|
}
|
|
|
|
//
|
|
//-----------------------------------------------------------------------
|
|
// Allocate the memory for the new handler set, and copy the inherited
|
|
// handlers to the new table. We are guaranteed to have enough space for
|
|
// the inherited table
|
|
//-----------------------------------------------------------------------
|
|
//
|
|
attributeIndex = new Simulation::IndexEntry[entryCount];
|
|
Check_Pointer(attributeIndex);
|
|
Register_Pointer(attributeIndex);
|
|
i = 0;
|
|
if (inheritance)
|
|
{
|
|
for (; i<inheritance->entryCount; ++i)
|
|
{
|
|
attributeIndex[i] = inheritance->attributeIndex[i];
|
|
}
|
|
}
|
|
|
|
//
|
|
//----------------------------------------------------------------------
|
|
// Step through the new table supplied, placing each handler in the slot
|
|
// determined by the message type
|
|
//----------------------------------------------------------------------
|
|
//
|
|
for (i=0; i<count; ++i)
|
|
{
|
|
Verify(!inheritance || index_table[i].entryID > inheritance->entryCount);
|
|
attributeIndex[index_table[i].entryID-1] = index_table[i];
|
|
}
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
Simulation::AttributePointer
|
|
Simulation::AttributeIndexSet::Find(const char* attribute_name) const
|
|
{
|
|
Check(this);
|
|
Check_Pointer(attribute_name);
|
|
|
|
for (int attribute=0; attribute<entryCount; ++attribute)
|
|
{
|
|
if (!strcmp(attribute_name, attributeIndex[attribute].entryName))
|
|
{
|
|
Check_Fpu();
|
|
return attributeIndex[attribute].entryAddress;
|
|
}
|
|
}
|
|
Check_Fpu();
|
|
return Simulation::NullAttribute;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
const Simulation::IndexEntry*
|
|
Simulation::AttributeIndexSet::FindEntry(const char* attribute_name) const
|
|
{
|
|
Check(this);
|
|
Check_Pointer(attribute_name);
|
|
|
|
for (int attribute=0; attribute<entryCount; ++attribute)
|
|
{
|
|
if (!strcmp(attribute_name, attributeIndex[attribute].entryName))
|
|
{
|
|
Check_Fpu();
|
|
return &attributeIndex[attribute];
|
|
}
|
|
}
|
|
Check_Fpu();
|
|
return NULL;
|
|
}
|
|
|
|
void
|
|
Simulation::RequestEncore(Encore encore)
|
|
{
|
|
Check(this);
|
|
SetWatcherDelay();
|
|
|
|
Check(application);
|
|
UpdateManager *updater = application->GetUpdateManager();
|
|
Check(updater);
|
|
updater->RequestEncore(this, encore);
|
|
}
|
|
|
|
#if defined(TEST_CLASS) && 0
|
|
#include "model.tcp"
|
|
#endif
|