Build 2's first two items, L4 and L5, picked ahead of the rest of the latency tier because the field data argues for them: a five-pod race on 4.12.233 kept time well - send interval median and p95 both ~32 ms - but every pod saw gaps of one to two seconds, and corrections as large as 2702 metres against a mean under a metre. That is not a timing problem, it is a pod flying most of the way across the map on stale velocity and being yanked back through whatever it passed. L5, the extrapolation clamp. Both dead reckoners carried lastPerformance - lastUpdate into the projection unbounded once past the expected update. They now stop at kMaximumExtrapolationSeconds, one second, so a replicant coasts and then parks. A pod that stops and snaps once reads as the dropped connection it is; a pod sliding confidently through scenery reads as a broken game, and is far more expensive to collide with. It matters more in the accelerated reckoner, which also carries a*t*t/2 and so grows the error as the SQUARE of the silence. RP412NETCOAST sets the seconds, 0 restores the unbounded coast for an A/B on one connection. MUST MATCH across a race, same class as RP412NETPREDICT - it moves replicants, so it decides where they collide. L4, the stale-record guard. Simulation now remembers the sender's stamp on the last record it accepted, and the record walk skips one stamped earlier - such a record winds lastUpdate backwards and has the reckoner extrapolate from a position the sender has already left. A regression larger than five seconds is not a late record but a different stream (a rejoin, a restart, a clock that was set), so that resyncs instead. The counter NetRaceStats::staleCount has been sitting there reading zero with a comment saying "until the stale guard ships"; it is now fed. The guard is asked by the caller rather than done inside ReadUpdateRecord, because that is virtual and not every override chains to the base. Verified: clean Release build; two-pod loopback race green, both pods scoring, and stale 0 on both - which is the reading that says the guard does not fire on an ordered stream. The clamp is NOT demonstrated by that run: the harness parks its pods, so there is no velocity to coast on. Pod B did see a 9.167 s gap in it, which on a moving pod is the shape of the field's kilometre corrections. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
494 lines
13 KiB
C++
494 lines
13 KiB
C++
#pragma once
|
|
|
|
#include "state.h"
|
|
#include "receiver.h"
|
|
#include "time.h"
|
|
#include "resource.h"
|
|
#include "hostid.h"
|
|
|
|
//##########################################################################
|
|
//########################### Net clock ##############################
|
|
//##########################################################################
|
|
//
|
|
// Aligning a peer's clock with ours, so a replicant is dead-reckoned from
|
|
// when its update was SENT rather than when it happened to arrive.
|
|
//
|
|
// Every update record carries the sender's own timestamp. The receiver
|
|
// used to throw it away and stamp lastUpdate with its own Now() - the
|
|
// original code says so: "HACK - should be based upon message->timeStamp".
|
|
// The dead reckoner then extrapolates over (lastPerformance - lastUpdate),
|
|
// so starting that clock at ARRIVAL rather than at SEND leaves every
|
|
// remote vehicle exactly one network latency behind where it should be.
|
|
// On the 1 ms arcade LAN that was invisible. Over Steam Datagram Relay it
|
|
// is a constant 50-150 ms of positional lag - a bias, not jitter.
|
|
//
|
|
// The timestamp cannot be used raw: two machines' clocks share no epoch,
|
|
// both being QueryPerformanceCounter since their own boot. So we estimate
|
|
// the offset per peer. Each arriving record gives
|
|
//
|
|
// sample = ourNow - theirStamp = trueOffset + oneWayLatency
|
|
//
|
|
// and since latency is never negative, the SMALLEST sample seen is the
|
|
// closest to the true offset. Taking a minimum over a short rolling
|
|
// window tracks crystal drift and re-adapts when the route changes,
|
|
// instead of being pinned forever by one lucky packet.
|
|
//
|
|
// RP412NETCLOCK=0 turns the whole thing off and restores the arrival-time
|
|
// behaviour, so a test machine can A/B it without a rebuild.
|
|
//
|
|
void NetClock_BeginUpdate(HostID sender); // around one message's records
|
|
void NetClock_EndUpdate();
|
|
void NetClock_Reset(); // forget every peer (new mission)
|
|
void NetClock_ReportRaceStats(); // RP412NETLOG race-end peer lines
|
|
|
|
class Simulation__SharedData;
|
|
class Simulation__IndexData;
|
|
struct Simulation__IndexEntry;
|
|
class Simulation__AttributeIndexSet;
|
|
class MemoryStream;
|
|
|
|
//##########################################################################
|
|
//################# Simulation::UpdateRecord #########################
|
|
//##########################################################################
|
|
|
|
struct Simulation__UpdateRecord
|
|
{
|
|
public:
|
|
size_t recordLength;
|
|
Word subsystemID;
|
|
Word recordID;
|
|
Time timeStamp;
|
|
Enumeration simulationState;
|
|
};
|
|
|
|
//##########################################################################
|
|
//####################### Simulation #################################
|
|
//##########################################################################
|
|
|
|
class Simulation:
|
|
public Receiver
|
|
{
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Shared Data Support
|
|
//
|
|
public:
|
|
typedef Simulation__SharedData SharedData;
|
|
SharedData*
|
|
GetSharedData();
|
|
|
|
static Derivation *GetClassDerivations();
|
|
|
|
static SharedData DefaultData;
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Construction and Destruction Support
|
|
//
|
|
protected:
|
|
Simulation(
|
|
ClassID class_ID,
|
|
SharedData &shared_data
|
|
);
|
|
|
|
public:
|
|
~Simulation();
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Attribute Support
|
|
//
|
|
public:
|
|
typedef Enumeration AttributeID;
|
|
typedef Simulation__IndexData IndexData;
|
|
typedef Simulation__IndexEntry IndexEntry;
|
|
typedef Simulation__AttributeIndexSet AttributeIndexSet;
|
|
typedef int Simulation::*AttributePointer;
|
|
|
|
enum {
|
|
AnyAttributeID = 0,
|
|
SimulationStateAttributeID,
|
|
NextAttributeID
|
|
};
|
|
|
|
static const AttributePointer NullAttribute;
|
|
|
|
void*
|
|
GetAttributePointer(AttributeID attribute);
|
|
void*
|
|
GetAttributePointer(const char* attribute_name);
|
|
|
|
private:
|
|
static const IndexEntry AttributePointers[];
|
|
|
|
protected:
|
|
//static AttributeIndexSet AttributeIndex
|
|
static AttributeIndexSet& GetAttributeIndex();
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Simulation Support
|
|
//
|
|
public:
|
|
typedef void
|
|
(Simulation::*Performance)(Scalar time_slice);
|
|
typedef void
|
|
(Simulation::*Encore)();
|
|
typedef Simulation__UpdateRecord UpdateRecord;
|
|
|
|
void
|
|
SetPerformance(Performance performance)
|
|
{Check(this); activePerformance = performance;}
|
|
void
|
|
Perform(Scalar time_slice)
|
|
{
|
|
Check(this);
|
|
(this->*activePerformance)(time_slice);
|
|
}
|
|
|
|
virtual void
|
|
PerformAndWatch(
|
|
const Time& till,
|
|
MemoryStream *update_stream
|
|
);
|
|
|
|
//
|
|
// The two halves of PerformAndWatch, so an ENTITY can interleave its
|
|
// subsystems' physics with its own, step by step, and still run the
|
|
// watchers and the update stream once per frame. PerformTo advances
|
|
// the simulation to the given time - in fixed steps when
|
|
// RP412PHYSICSHZ names a rate, in one variable slice otherwise.
|
|
//
|
|
void
|
|
PerformTo(const Time& till);
|
|
void
|
|
WatchAndWrite(MemoryStream *update_stream);
|
|
|
|
//
|
|
// Called by the entity interleave at the TOP of every fixed step,
|
|
// before any subsystem adds its forces for that step. Per-frame set-up
|
|
// work - clearing a force accumulator, deriving local velocity from
|
|
// world state - belongs here when the fixed step is on, because "once
|
|
// per frame" is a wall-clock cadence and the whole point is that wall
|
|
// clock no longer reaches the physics. Default: nothing.
|
|
//
|
|
virtual void
|
|
BeginStep();
|
|
|
|
//
|
|
// Render interpolation hooks, called by PerformTo around the fixed
|
|
// step. They live HERE rather than on the entity interleave because a
|
|
// REPLICANT never runs that interleave - it reaches PerformTo through
|
|
// Simulation::PerformAndWatch instead - and a replicant is exactly what
|
|
// every remote pod is. Hanging the snapshot off BeginStep left the
|
|
// watched car uninterpolated while the camera watching it was smooth,
|
|
// which is most of the way to nowhere.
|
|
//
|
|
// Defaults do nothing; Entity overrides them because it owns the
|
|
// origin. See Entity::GetRenderToWorld.
|
|
//
|
|
virtual void
|
|
SnapshotRenderOrigin();
|
|
virtual void
|
|
SetRenderStepFraction(Scalar fraction);
|
|
|
|
//
|
|
// The fixed step in seconds, 0 when frame-coupled. Global on purpose:
|
|
// a mixed-rate simulation would be a worse bug than either mode.
|
|
//
|
|
static Scalar
|
|
FixedStep();
|
|
|
|
void
|
|
DoNothingOnce(Scalar time_slice);
|
|
void
|
|
DoNothing(Scalar time_slice);
|
|
|
|
void
|
|
SetLastPerformance(const Time& when)
|
|
{Check(this); Check(&when); lastPerformance = when;}
|
|
const Time&
|
|
GetLastPerformance() const
|
|
{Check(this); return lastPerformance;}
|
|
void
|
|
RequestEncore(Encore encore);
|
|
|
|
//
|
|
// True if this record should be applied, False if it is an older
|
|
// one arriving late. Ask BEFORE ReadUpdateRecord: it is the caller
|
|
// that has to skip the record, because ReadUpdateRecord is virtual
|
|
// and not every override chains to the base.
|
|
//
|
|
Logical
|
|
AcceptUpdateStamp(const Time &stamp);
|
|
|
|
virtual void
|
|
ReadUpdateRecord(UpdateRecord *message);
|
|
virtual void
|
|
WriteUpdateRecord(
|
|
UpdateRecord *message,
|
|
int update_model
|
|
);
|
|
void
|
|
WriteSimulationUpdate(MemoryStream *update_stream);
|
|
|
|
enum {
|
|
DefaultUpdateModelBit=0,
|
|
NextUpdateModelBit
|
|
};
|
|
|
|
enum {
|
|
DefaultUpdateModelFlag = 1<<DefaultUpdateModelBit
|
|
};
|
|
|
|
void
|
|
ForceUpdate(Word model=DefaultUpdateModelFlag)
|
|
{Check(this); updateModel |= model;}
|
|
|
|
protected:
|
|
Time
|
|
lastPerformance;
|
|
Time
|
|
lastUpdate;
|
|
|
|
//
|
|
// The sender's own stamp on the last record we accepted, for the
|
|
// stale guard in AcceptUpdateStamp. Kept in the sender's clock, not
|
|
// ours - it is only ever compared against another stamp from the
|
|
// same sender, so no offset is needed and none is applied.
|
|
//
|
|
Time
|
|
lastSenderStamp;
|
|
Logical
|
|
lastSenderStampValid;
|
|
Word
|
|
updateModel;
|
|
|
|
Performance
|
|
activePerformance;
|
|
|
|
//##########################################################################
|
|
// Flag Support
|
|
//
|
|
public:
|
|
enum {
|
|
DelayWatchersBit,
|
|
DontExecuteBit,
|
|
NextBit
|
|
};
|
|
enum {
|
|
DelayWatchersFlag = 1<<DelayWatchersBit,
|
|
DontExecuteFlag = 1<<DontExecuteBit
|
|
};
|
|
|
|
LWord simulationFlags;
|
|
|
|
void
|
|
SetWatcherDelay()
|
|
{Check(this); simulationFlags |= DelayWatchersFlag;}
|
|
void
|
|
ClearWatcherDelay()
|
|
{Check(this); simulationFlags &= ~DelayWatchersFlag;}
|
|
Logical
|
|
AreWatchersDelayed()
|
|
{Check(this); return (simulationFlags & DelayWatchersFlag) != 0;}
|
|
|
|
void
|
|
NeverExecute()
|
|
{Check(this); simulationFlags |= DontExecuteFlag;}
|
|
void
|
|
ExecuteOnUpdate()
|
|
{Check(this); simulationFlags |= DontExecuteFlag;}
|
|
void
|
|
AlwaysExecute()
|
|
{Check(this); simulationFlags &= ~DontExecuteFlag;}
|
|
Logical
|
|
IsReplicantExecutable()
|
|
{
|
|
Check(this);
|
|
return
|
|
(simulationFlags&DontExecuteFlag) == 0
|
|
|| lastUpdate >= lastPerformance;
|
|
}
|
|
Logical
|
|
IsNonReplicantExecutable()
|
|
{Check(this); return (simulationFlags&DontExecuteFlag) == 0;}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// State support
|
|
//
|
|
public:
|
|
enum {
|
|
DefaultState = 0,
|
|
StateCount
|
|
};
|
|
|
|
unsigned
|
|
GetSimulationState()
|
|
{Check(this); return simulationState.GetState();}
|
|
unsigned
|
|
GetOldSimulationState()
|
|
{Check(this); return simulationState.GetOldState();}
|
|
void
|
|
SetSimulationState(unsigned new_state)
|
|
{Check(this); simulationState.SetState(new_state);}
|
|
|
|
StateIndicator
|
|
simulationState;
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Watcher Support
|
|
//
|
|
public:
|
|
void
|
|
AddAudioWatcher(Component *watcher)
|
|
{Check(&audioWatcherSocket);audioWatcherSocket.Add(watcher);}
|
|
void
|
|
AddVideoWatcher(Component *watcher)
|
|
{Check(&videoWatcherSocket);videoWatcherSocket.Add(watcher);}
|
|
void
|
|
AddGaugeWatcher(Component *watcher)
|
|
{Check(&gaugeWatcherSocket);gaugeWatcherSocket.Add(watcher);}
|
|
|
|
void
|
|
AddEffectWatcher(Component *watcher)
|
|
{Check(&effectWatcherSocket); effectWatcherSocket.Add(watcher);}
|
|
|
|
void
|
|
ExecuteWatchers();
|
|
|
|
private:
|
|
SChainOf<Component*>
|
|
audioWatcherSocket;
|
|
SChainOf<Component*>
|
|
videoWatcherSocket;
|
|
SChainOf<Component*>
|
|
gaugeWatcherSocket;
|
|
SChainOf<Component*>
|
|
effectWatcherSocket;
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Test Support
|
|
//
|
|
public:
|
|
Logical
|
|
TestInstance() const;
|
|
static Logical
|
|
TestClass();
|
|
};
|
|
|
|
//##########################################################################
|
|
//################### Simulation::IndexEntry #########################
|
|
//##########################################################################
|
|
|
|
struct Simulation__IndexEntry
|
|
{
|
|
Enumeration
|
|
entryID;
|
|
const char *
|
|
entryName;
|
|
Simulation::AttributePointer
|
|
entryAddress;
|
|
};
|
|
|
|
#define ATTRIBUTE_ENTRY(class,name,attribute)\
|
|
{\
|
|
class::name##AttributeID,\
|
|
#name,\
|
|
(Simulation::AttributePointer) &class::attribute\
|
|
}
|
|
|
|
//##########################################################################
|
|
//################# Simulation::AttributeIndexSet ####################
|
|
//##########################################################################
|
|
|
|
class Simulation__AttributeIndexSet:
|
|
public Receiver::InheritanceSet
|
|
{
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Construction and Destruction
|
|
//
|
|
public:
|
|
Simulation__AttributeIndexSet(
|
|
Simulation::AttributeID count,
|
|
const Simulation::IndexEntry index_table[],
|
|
const Simulation::AttributeIndexSet &inheritance
|
|
)
|
|
{Build(count, index_table, &inheritance);}
|
|
|
|
Simulation__AttributeIndexSet(
|
|
Simulation::AttributeID count,
|
|
const Simulation::IndexEntry index_table[]
|
|
)
|
|
{Build(count, index_table, NULL);}
|
|
|
|
Simulation__AttributeIndexSet()
|
|
{attributeIndex = NULL; entryCount = 0;}
|
|
|
|
~Simulation__AttributeIndexSet();
|
|
|
|
protected:
|
|
void
|
|
Build(
|
|
Simulation::AttributeID count,
|
|
const Simulation::IndexEntry index_table[],
|
|
const Simulation::AttributeIndexSet *inheritance
|
|
);
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// AttributeIndexSet Functionality
|
|
//
|
|
protected:
|
|
Simulation::IndexEntry
|
|
*attributeIndex;
|
|
|
|
public:
|
|
Simulation::AttributePointer
|
|
Find(Simulation::AttributeID attribute) const
|
|
{
|
|
Check(this);
|
|
Verify(attribute > 0);
|
|
if (attribute<=entryCount)
|
|
return attributeIndex[attribute-1].entryAddress;
|
|
else
|
|
return Simulation::NullAttribute;
|
|
}
|
|
Simulation::AttributePointer
|
|
Find(const char* attribute_name) const;
|
|
const Simulation::IndexEntry*
|
|
FindEntry(const char* attribute_name) const;
|
|
|
|
static const Simulation::AttributeIndexSet
|
|
NullSet;
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Test Support
|
|
//
|
|
public:
|
|
static Logical
|
|
TestClass();
|
|
};
|
|
|
|
//##########################################################################
|
|
//################### Simulation::SharedData #########################
|
|
//##########################################################################
|
|
|
|
class Simulation__SharedData:
|
|
public Receiver::SharedData
|
|
{
|
|
public:
|
|
Simulation__SharedData(
|
|
Derivation* derivation,
|
|
Receiver::MessageHandlerSet &message_handlers,
|
|
Simulation::AttributeIndexSet &attribute_index,
|
|
int state_count
|
|
):
|
|
Receiver::SharedData(derivation, message_handlers),
|
|
activeAttributeIndex(&attribute_index),
|
|
stateCount(state_count)
|
|
{}
|
|
|
|
Simulation::AttributeIndexSet* activeAttributeIndex;
|
|
int stateCount;
|
|
};
|
|
|
|
inline Simulation::SharedData*
|
|
Simulation::GetSharedData()
|
|
{return Cast_Object(SharedData*,sharedData);}
|