Files
RP412/MUNGA/MOVER.h
T
CydandClaude Opus 5 9107fb4cd9 A quiet sender and a stalled loop are not the same silence
With the clamp in, spikes now occur only in windows where updates stopped
arriving. Steady state is clean - zero spikes, 250 of 250 steps blending,
the prediction within 18ms - while the one window holding a 1.236s gap
carried all four spikes. The clamp is doing exactly what it was built to
do: 0.01976 / (0.25 + 0.01976) is 0.0733, matching the logged blend floor
of 0.0732601 against the old 0.0097.

So what is left is not jitter. It is the absence of data for over a
second, and no predictor can invent motion it was never told about.

The useful question is whose silence it is, and the answer is already in
the arrival pattern. A long gap followed by ordinary 30ms gaps means the
sender went quiet - their machine or the connection. A long gap followed
by a burst of near-zero gaps means the packets were sitting in the queue
while OUR loop was busy elsewhere, and we read them all at once the
moment it came back. From inside the dead reckoner the two are
indistinguishable, and they want opposite fixes.

Record the widest gap, the count over 200ms and the count under 5ms, per
entity, and let the trace name which pattern it saw. This matters more
than it might: the camera station only recently began rastering the map,
which is exactly the kind of work that stalls a main loop.

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

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