The first cut hung the snapshot off Mover::BeginStep, which is inside Entity::PerformAndWatch's fixed-step interleave - and that interleave sits entirely inside "if (GetInstance() != ReplicantInstance)". A replicant never runs it; it reaches the step loop through Simulation::PerformAndWatch instead. Every remote pod is a replicant, so on a Live Cam the camera was being interpolated while the car it was watching still stepped. Smoother, and most of the way to nowhere - which is exactly what "still some hitching" was. So the hooks move to Simulation::PerformTo, where both paths meet: SnapshotRenderOrigin before each Perform, SetRenderStepFraction after the loop, two virtuals that do nothing by default and are overridden by Entity because Entity owns the origin. Mover::BeginStep goes back to what it was, so there is now one mechanism instead of two. Taking the snapshot inside the step loop is also strictly better placed than BeginStep was: it lands immediately before the integration, and still after any BeginStep teleport, so a VTV's scheduled respawn stays a cut. Entity::PerformAndWatch keeps computing the fraction itself after its interleave, because there PerformTo is called once per step with a till one step ahead and so sees no leftover at all - it needs the FRAME's till, which only the interleave has. Determinism re-proved, and more thoroughly than the first time. The scripted lap at 240 fps, interpolation on and off, on both the old build and this one: all four runs agree to the last decimal at the same simulation time - pos -15.06739 3.01541 388.02603 at t=15.260. The one "differing" sample in the raw comparison was the trace sampling at t=1.260 in one run and t=1.280 in the other and then realigning, not divergence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
473 lines
12 KiB
C++
473 lines
12 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)
|
|
|
|
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);
|
|
|
|
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;
|
|
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);}
|