Files
RP412/MUNGA/MOVER.h
T
CydandClaude Opus 5 8d372ec067 The tick has a period, and the period is the evidence
Rejecting the sender-stall theory was right: a gap of over a second is a
death pause, not a tick. The remaining symptom is described precisely - a
RHYTHMIC tick as a pod moves past the camera - and rhythm is the clue.
A fixed period points at a cadence in our own code, because the network
has no period.

There are at least four candidates and they are only distinguishable by
their interval: the 20ms physics step, the 30ms update rate, the sawtooth
in the dead reckoner blend (percent climbs from 0.29 to 0.87 across each
update interval as lastPerformance approaches nextUpdate, then resets),
and the twenty-step quaternion renormalisation in Mover::BeginStep, which
falls at 0.4s - a few times a second.

So measure the interval rather than guess among them. Per frame, take the
angle the traced pod subtends at the eye, flag the frames whose angular
step is far above the running mean, and report the time BETWEEN those
events. Angle rather than distance because a pod crossing the view moves
far across the screen while barely changing range, which is the geometry
the tick was reported in.

Both samples come from one frame, marked by the eye's own frame counter
rather than assumed from draw order, and the renderer reports on the same
pod the mover trace describes - MoverTracedEntity now exposes that latch.
Two traces about two different pods, or two different frames, would
compare nothing; that mistake has already cost this investigation three
wrong answers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 09:01:55 -05:00

520 lines
11 KiB
C++

#pragma once
#include "entity.h"
#include "motion.h"
class Mover__SharedData;
class Mover;
class CollisionAssistant;
class BoundingBoxTreeNode;
class BoxedSolid;
class BoxedSolidCollision;
class BoxedSolidCollisionList;
class Normal;
class Line;
class Environment;
//##########################################################################
//#################### Mover::UpdateMessage ##########################
//##########################################################################
struct Mover__UpdateRecord:
public Entity::UpdateRecord
{
public:
Motion
localVelocity,
localAcceleration;
Vector3D
worldLinearVelocity,
worldLinearAcceleration;
};
//##########################################################################
//##################### Mover::MakeMessage ##########################
//##########################################################################
class Mover__MakeMessage:
public Entity::MakeMessage
{
public:
Motion
localVelocity,
localAcceleration;
Mover__MakeMessage(
Receiver::MessageID message_ID,
size_t length,
Entity::ClassID class_ID,
const EntityID &owner_ID,
ResourceDescription::ResourceID resource_ID,
LWord instance_flags,
const Origin &origin,
const Motion &velocity,
const Motion &acceleration
):
Entity::MakeMessage(
message_ID,
length,
class_ID,
owner_ID,
resource_ID,
instance_flags,
origin
),
localVelocity(velocity),
localAcceleration(acceleration)
{}
Mover__MakeMessage(
Receiver::MessageID message_ID,
size_t length,
const EntityID &entity_ID,
Entity::ClassID class_ID,
const EntityID &owner_ID,
ResourceDescription::ResourceID resource_ID,
LWord instance_flags,
const Origin &origin,
const Motion &velocity,
const Motion &acceleration
):
Entity::MakeMessage(
message_ID,
length,
entity_ID,
class_ID,
owner_ID,
resource_ID,
instance_flags,
origin
),
localVelocity(velocity),
localAcceleration(acceleration)
{}
};
//##########################################################################
//##################### Mover::ModelResource #########################
//##########################################################################
struct Mover__ModelResource
{
Scalar moverMass;
Vector3D
momentOfInertia,
positiveLinearDragCoefficients,
negativeLinearDragCoefficients,
angularDragCoefficients;
Scalar
frictionCoefficient,
elasticityCoefficient,
minimumBounceSpeed;
};
//##########################################################################
//############################ Mover #################################
//##########################################################################
class Mover:
public Entity
{
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shared Data support
//
public:
static Derivation *GetClassDerivations();
static SharedData DefaultData;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Attribute Support
//
public:
enum {
LocalVelocityAttributeID = Entity::NextAttributeID,
LocalAccelerationAttributeID,
WorldLinearVelocityAttributeID,
WorldLinearAccelerationAttributeID,
MoverMassAttributeID,
MomentOfInertiaAttributeID,
PositiveLinearDragCoefficientsAttributeID,
NegativeLinearDragCoefficientsAttributeID,
AngularDragCoefficientsAttributeID,
FrictionCoefficientAttributeID,
ElasticityCoefficientAttributeID,
MinimumBounceSpeedAttributeID,
NextAttributeID
};
private:
static const IndexEntry AttributePointers[];
protected:
//static AttributeIndexSet AttributeIndex
static AttributeIndexSet& GetAttributeIndex();
//
// public attribute declarations go here
//
public:
Motion
localVelocity,
localAcceleration;
Vector3D
worldLinearVelocity,
worldLinearAcceleration;
Scalar moverMass;
Vector3D
momentOfInertia,
positiveLinearDragCoefficients,
negativeLinearDragCoefficients,
angularDragCoefficients;
Scalar
frictionCoefficient,
elasticityCoefficient,
minimumBounceSpeed;
//
// virtual access
//
public:
virtual Vector3D
GetWorldLinearVelocity()
{return worldLinearVelocity;}
virtual Vector3D
GetWorldLinearAcceleration()
{return worldLinearAcceleration;}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Model Support
//
public:
enum {
StasisState = Entity::StateCount,
StateCount
};
typedef Mover__UpdateRecord UpdateRecord;
typedef Mover__MakeMessage MakeMessage;
typedef void
(Mover::*Performance)(Scalar time_slice);
void
SetPerformance(Performance performance)
{
Check(this);
activePerformance = (Simulation::Performance)performance;
}
void
UpdateWorldMotion();
void
UpdateLocalMotion();
void
ApplyWorldAccelerations(Scalar time_slice);
void
ApplyAirResistanceAndGravity(Scalar power=1.0f);
void
CalculateDrag(
Vector3D *drag,
const Vector3D &velocity,
const Vector3D &positive_CODs,
const Vector3D &negative_CODs,
Scalar power
);
void
ApplyLocalForce(
const Vector3D &force,
const Vector3D &moment
);
void
ApplyLocalAcceleration(
const Vector3D &acceleration,
const Vector3D &moment
);
Environment*
GetEnvironment()
{Check(this); return localEnvironment;}
typedef Logical (Mover::*DeadReckoner)();
void
SetDeadReckoner(DeadReckoner reckoner)
{Check(this); deadReckoner = reckoner;}
Logical
NoDeadReckoner();
Logical
LinearDeadReckoner();
Logical
AcceleratedDeadReckoner();
void
DeadReckon(Scalar time_slice);
protected:
void
WriteUpdateRecord(
Simulation::UpdateRecord *message,
int update_model
);
void
ReadUpdateRecord(Simulation::UpdateRecord *message);
void
PerformAndWatch(
const Time& till,
MemoryStream *update_stream
);
//
// Per-step set-up under fixed stepping: clears the force accumulator
// the thrusters add into, so each step integrates only its own
// forces. See the definition for the frame-jitter bug this closes.
//
void
BeginStep();
int
normalizeCount;
Environment
*localEnvironment;
DeadReckoner
deadReckoner;
Origin
projectedOrigin,
previousOrigin;
Motion
projectedVelocity,
updateAcceleration,
updateVelocity;
Time
nextUpdate;
//
// Recent gaps between replication updates for this entity, as a ring,
// and the running estimate drawn from them. The original code predicted
// the next gap from the single previous gap; see PredictUpdateInterval
// for why that stalls a step every time a packet runs late.
//
enum {UpdateIntervalSamples = 8};
Scalar
updateIntervals[UpdateIntervalSamples];
int
updateIntervalCount,
updateIntervalWrite;
//
// The interval last predicted, and how wrong the prediction before it
// proved to be once the gap it described actually elapsed. Per entity,
// so a trace reads the entity it is watching.
//
Scalar
predictedInterval,
predictionError;
//
// Arrival statistics for the window a trace reports over. A long gap
// followed by normal gaps means the sender went quiet; a long gap
// followed by a burst of near-zero ones means OUR loop stalled and the
// packets queued up behind it. The two look identical from inside the
// dead reckoner and want opposite fixes.
//
Scalar
widestGap;
int
longGapCount,
queuedGapCount;
Scalar
PredictUpdateInterval(Scalar latest);
void
ResetUpdateIntervals();
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Collision support
//
public:
virtual void
MoveCollisionVolume();
BoundingBoxTreeNode*
GetMoverCollisionRoot()
{Check(this); return containedByNode;}
BoxedSolidCollisionList*
AllocateCollisionList();
BoxedSolidCollisionList*
GetCurrentCollisions(BoxedSolidCollisionList *list=NULL);
BoxedSolid*
FindBoxedSolidHitBy(
Line *line,
Entity *except_by
);
BoxedSolidCollisionList*
CollideCenterOfMotion(
Line *line,
BoxedSolidCollisionList *list
);
void
ProcessCollisionList(
BoxedSolidCollisionList *collisions,
Scalar time_slice,
const Point3D &old_position,
Damage *damage
);
Scalar
StaticBounce(
const Point3D &old_position,
Scalar delta_t,
Scalar penetration,
const Normal &normal,
Scalar *elasticity,
Scalar bounce_min,
Scalar *friction
);
Scalar
DynamicBounce(
Mover *other,
Scalar delta_t,
Scalar penetration,
const Normal &normal,
Scalar *elasticity
);
BoxedSolid*
GetCollisionVolume()
{Check(this); return collisionVolume;}
BoxedSolid*
GetCollisionTemplate()
{Check(this); return collisionTemplate;}
int
GetCollisionVolumeCount()
{Check(this); return collisionVolumeCount;}
virtual void
StartCollisionAssistant();
protected:
int collisionVolumeCount;
BoxedSolid
*collisionVolume,
*collisionTemplate;
BoundingBoxTreeNode *containedByNode;
BoxedSolidCollisionList
*collisionLists,
*lastCollisionList;
CollisionAssistant *collisionAssistant;
virtual void
ProcessCollision(
Scalar time_slice,
BoxedSolidCollision &collision,
const Point3D &old_position,
Damage *damage
);
void
CheckAgainstBoxedSolidChain(
BoxedSolidCollisionList *collisions,
BoxedSolid *chain
);
public:
void
CheckVolumeAgainstBoxedSolidChain(
BoxedSolidCollisionList *collisions,
BoxedSolid *chain
);
protected:
BoxedSolid*
CheckLineAgainstBoxedSolidChain(
Line *line,
BoxedSolid *chain
);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Flag Support
//
public:
enum {
NoCollisionVolumeBit = Entity::NextBit,
NoCollisionTestBit,
InitialStasisBit,
NextBit
};
enum {
NoCollisionVolumeFlag = 1<<NoCollisionVolumeBit,
NoCollisionTestFlag = 1<<NoCollisionTestBit,
InitialStasisFlag = 1<<InitialStasisBit,
DefaultFlags = DynamicFlag|MasterInstance
};
void
NoCollisionVolume()
{Check(this); simulationFlags |= NoCollisionVolumeFlag;}
void
SetCollisionVolume()
{Check(this); simulationFlags &= ~NoCollisionVolumeFlag;}
Logical
IsCollisionVolume()
{Check(this); return (simulationFlags&NoCollisionVolumeFlag) == 0;}
void
NoCollisionTest()
{Check(this); simulationFlags |= NoCollisionTestFlag;}
void
SetCollisionTest()
{Check(this); simulationFlags &= ~NoCollisionTestFlag;}
Logical
IsCollisionTestable()
{Check(this); return (simulationFlags&NoCollisionTestFlag) == 0;}
Logical
IsInitialStasis()
{Check(this); return (simulationFlags&InitialStasisFlag) != 0;}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Construction and Destruction
//
public:
typedef Mover__ModelResource ModelResource;
static Mover*
Make(MakeMessage *creation_message);
//
// Warning... This function requires a properly set up strtok!!!!
//
static Logical
CreateMakeMessage(
MakeMessage *creation_message,
NotationFile *model_file,
const ResourceDirectories *directories
);
static ResourceDescription::ResourceID
CreateModelResource(
ResourceFile *resource_file,
const char* model_name,
NotationFile *model_file,
const ResourceDirectories *directories,
ModelResource* model = NULL
);
Mover(
MakeMessage *creation_message,
SharedData &virtual_data
);
~Mover();
Logical
TestInstance() const;
};
//
// The replicant the RP412CAMLOG traces are describing, so the renderer can
// report on the same pod from the other end - what its motion looks like on
// screen. Null until a replicant has stepped at least once.
//
EntityID
MoverTracedEntity();