Files
RP412/MUNGA/SIMULATE.h
T
CydandClaude Opus 5 d9149a6a8c A quiet pod coasts a second, then waits to be told
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>
2026-08-13 21:35:22 -05:00

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);}