Cyd asked for an analysis of the networking stack and what would make the simulation feel better over the internet. The analysis found something more urgent than latency: the transport has been losing data silently since the arcade, and nothing in the game could see it happen. Every send result was discarded - L4NET, the console, all of it. On the 1ms arcade LAN the socket buffer never filled, so it never mattered. Over the internet it matters twice. A peer stalled in its own 10-30 second mission load stops reading, its window closes, and our nonblocking send starts answering would-block, which threw the message away; or worse, answering a PARTIAL count, and since framing on that stream is recovered purely from each message's length prefix, the bytes that never followed sheared it for good. Both are reachable in an ordinary race, because every race has a load in it. So sends go through a bounded per-connection queue now. What the wire will not take is kept, byte-exact, and retried at three flush points - before the render (the present blocks on vsync, and this frame's state should be travelling while it does), at the top of the receive pump, and before a connect sequence. Nothing is ever dropped from the middle: these are reliable ordered messages carrying entity creation, damage and race control, so a queue that overflows its 256K declares the connection dead and lets the disconnect path run rather than quietly desyncing the stream. RP412NETSENDQ=0 restores the old behaviour and still logs what it would have lost, which is the honest way to A/B it. On Steam the same queue finally surfaces k_EResultLimitExceeded, which the old code collapsed into -1 and discarded - that was backpressure, unlogged. The receive side gained the check the release build never had. The length prefix is untrusted input; Verify() compiles away in release, so a corrupt one went to memmove as a negative, or copied 4096 bytes of assembled packet into a 1600-byte stack buffer, or named a size the pad could never complete and wedged the connection forever. It is now validated against the same bounds the sender works to, and a stream that fails them is dropped like any other lost peer. And a fry that never ends: drop zones are map entities dealt round-robin at load, ownership transfer is not implemented, so a leaver's pads stay in the DropZones group. The respawn request dispatched to one goes to a host that is gone - dropped at the send, the 'no host N in the table' path - and the two-second retry re-dispatches to the same dead owner forever. The pad scan now skips zones whose owner has left, and re-validates one assigned earlier before reusing it. The rest is measurement, because the symptoms this work exists to chase are all reported in prose and none of them are in any log. Sixteen logs from the six-player night contain zero player-facing latency lines. A race now ends with a NetLog summary: per remote pod, how many updates arrived and how evenly (median and p95 out of a log2 histogram), the widest gap, how many gaps were long enough to mean a quiet sender versus short enough to mean OUR loop stalled, how often its motion snapped instead of blending, and how far arriving updates moved it. Per peer, whether the clock alignment ever had to step mid-race - which is the input for deciding if it needs slewing, rather than guessing. The mission t0 tick goes in the log too, alongside the console's per-pod RunMission send ticks, because nothing has ever measured how far apart the machines actually start; the clockwork doors inherit that skew directly. RP412NETSTATS adds the transport's own view - per connection: messages, bytes, wire writes, partials, refusals, how much sat queued - and on Steam the first read this codebase has ever taken of GetConnectionRealTime Status. Ping, quality, pending and unacked bytes, and one route description per connection at teardown. The API was vendored and never called; there was no RTT number anywhere in the game. Finally, rpl4opt -spoolstats reads any recording offline. The data was already in every spool ever made and nothing read it that way: the recorder restamps each packet with local arrival time while the update records inside keep the sender's sim-grid stamp, so the difference is clock offset plus one-way delay, and the same running-minimum estimator the game runs live separates them. It prints delay above the per-host minimum, and decomposes each entity's gaps into sender pacing versus delivery jitter - which no live counter can do. It lives in the game exe rather than RPL4TOOL because the tool is deliberately not /Zp1 and would misread every struct in the file. Verified on the two-pod loopback harness: mesh up, egg fed, 60s raced, stopped on command, scores collected, and both summaries reading exactly what a pair of PARKED pods should read - heartbeat cadence, one snap per heartbeat, sub-quarter-metre corrections, no clock steps. The t0 ticks and the netclock offsets agree with each other to the two seconds the pods launched apart. The latency tier is deliberately NOT here. TCP_NODELAY, the Steam NoNagle flag, per-frame coalescing and the pre-sim receive drain are all scoped and all wait on this build's numbers, because the point of shipping measurement first is to find out whether the thing we would fix is the thing that hurts. Nagle is still on. Interest management is still inert. The wire format is untouched, so this build and the last one still race each other. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1162 lines
29 KiB
C++
1162 lines
29 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;
|
|
|
|
//
|
|
// Race totals for the RP412NETLOG summary: how often a window
|
|
// close actually moved the estimate (>5 ms), and the worst such
|
|
// step. This is the decision input for whether a bounded slew is
|
|
// ever needed - a step mid-race means SDR rerouted or a clock
|
|
// drifted, and today it lands as one hard jump.
|
|
//
|
|
unsigned long raceStepCount;
|
|
long raceStepWorstMs;
|
|
};
|
|
|
|
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;
|
|
free_slot->raceStepCount = 0;
|
|
free_slot->raceStepWorstMs = 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;
|
|
}
|
|
|
|
//
|
|
// Race-end summary, one line per peer (RP412NETLOG). The offset's
|
|
// absolute value only says how far apart the two processes launched
|
|
// (clocks are ms-since-process-start); the movement counters are the
|
|
// health signal - a mid-race step means SDR rerouted or a clock
|
|
// drifted, and today each one lands as a hard jump in every replicant
|
|
// from that peer. This is the decision input for the deferred bounded
|
|
// slew (plan item D2).
|
|
//
|
|
void NetClock_ReportRaceStats()
|
|
{
|
|
for (int i = 0; i < netClockMaxPeers; ++i)
|
|
{
|
|
PeerClock *peer = &gPeerClocks[i];
|
|
if (peer->inUse && peer->settled)
|
|
{
|
|
DEBUG_STREAM << "NetLog: peer " << peer->host
|
|
<< ": netclock offset " << peer->offsetTicks
|
|
<< " ms, window steps >5ms " << peer->raceStepCount
|
|
<< ", worst " << peer->raceStepWorstMs << " ms\n" << std::flush;
|
|
}
|
|
}
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
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;
|
|
long moved_abs = (moved < 0) ? -moved : moved;
|
|
if (moved_abs > 5)
|
|
{
|
|
// race totals for the NetLog summary (see PeerClock)
|
|
peer->raceStepCount++;
|
|
if (moved_abs > peer->raceStepWorstMs)
|
|
{
|
|
peer->raceStepWorstMs = moved_abs;
|
|
}
|
|
}
|
|
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");
|
|
|
|
//
|
|
// 50 Hz is the default: a 20 ms step, exact on the millisecond
|
|
// clock, and the rate whose settled hover ride height measured
|
|
// closest to the frame-coupled physics the game has always run.
|
|
// Proven before it was defaulted - a scripted lap with a crash,
|
|
// a burn and two respawns runs bit-identical at 30, 60 and 144
|
|
// fps, and identical runs reproduce exactly. 0 restores the
|
|
// original frame-coupled behaviour, where the frame rate is
|
|
// part of the physics.
|
|
//
|
|
int rate = (setting != NULL) ? atoi(setting) : 50;
|
|
|
|
//
|
|
// 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)
|
|
{
|
|
//
|
|
// Where this step STARTED, for drawing. Taken here rather than
|
|
// in BeginStep so it covers replicants too, and taken after any
|
|
// BeginStep teleport (a VTV's scheduled respawn) so a jump
|
|
// stays a cut instead of becoming a slide.
|
|
//
|
|
SnapshotRenderOrigin();
|
|
Perform(step);
|
|
++gPhysicsStepsTaken;
|
|
lastPerformance += step;
|
|
behind -= step;
|
|
++taken;
|
|
}
|
|
|
|
//
|
|
// How far past the last completed step the frame being drawn falls.
|
|
// Entity::PerformAndWatch computes this again from the FRAME's till
|
|
// after its interleave, because there this function is called once
|
|
// per step and sees no leftover at all.
|
|
//
|
|
{
|
|
Scalar fraction = behind / step;
|
|
if (fraction < (Scalar) 0) fraction = (Scalar) 0;
|
|
if (fraction > (Scalar) 1) fraction = (Scalar) 1;
|
|
SetRenderStepFraction(fraction);
|
|
}
|
|
|
|
//
|
|
// 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::SnapshotRenderOrigin()
|
|
{
|
|
// nothing by default - only an Entity has an origin to snapshot
|
|
}
|
|
|
|
void
|
|
Simulation::SetRenderStepFraction(Scalar)
|
|
{
|
|
// 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
|