The map draws on every step of the rate wheel
The renderer walks a sixteen-step rate wheel: one step per full pass over the gauge list, shifted right each pass and reset at the bottom. A gauge redraws only on the step its configured rate names, so the map - on one step - waited a whole turn of the wheel however cheap its redraw was. With the frame budget fixed the wheel turns about fifty times a second and one-in-sixteen would be tolerable. It is still the wrong shape for the map: the thing a pilot reads to navigate should not be the display that updates least often, and RP412MAPRATE says how many of the sixteen steps it draws on. Sixteen by default, one for the old data-driven behaviour. Each extra step costs one gauge's redraw against a pass that runs ninety of them, which measured as nothing. The write has to be QUALIFIED, and that is worth recording because it cost hours. GPS's constructor takes its rate as a parameter also called 'rate', which shadows the inherited Gauge::rate for the whole body - so a bare assignment sets the parameter and leaves the member holding whatever the gauge data asked for. oldRate is not shadowed, so it took the value, and the pair then disagreed: rate=2000, old=ffff. That looked exactly like something writing the member from outside, and there is no such writer - Gauge touches rate in three places, none of which can produce that pair. A hardware write-watch on the member settled it by reporting an address on the STACK. Also here, the terrain-arrival work on the map background. It draws one placement into the cached picture when the static bounds are unchanged, and rebuilds the whole thing only when they move - the bounds set the scale, and the scale is what everything already on the picture was drawn at. It is honest to say this fires rarely: the logs show terrain arriving in one burst at mission load, not streaming in as you drive, so the incremental path is mostly insurance. What it does close is real, though - departures now order a rebuild. Nothing listened for those before, and they had been swept up by the rebuild the next ARRIVAL ordered, which on a track whose terrain all arrives at load is a rebuild that never comes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -210,6 +210,14 @@ namespace
|
||||
"# rate.\n"
|
||||
"RP412GAUGESLICE=2\n"
|
||||
"\n"
|
||||
"# How many times the map redraws per turn of the gauge rate wheel, 1 to\n"
|
||||
"# 16. The renderer gives each gauge one step of a sixteen-step wheel and\n"
|
||||
"# a gauge redraws only on its own step, so a map left on one step waits a\n"
|
||||
"# whole turn. 16 = redraw on every step (default); 1 = whatever the gauge\n"
|
||||
"# data asks for, which is how it behaved before this existed. Each step\n"
|
||||
"# costs one map redraw against a pass that runs ninety gauges.\n"
|
||||
"RP412MAPRATE=16\n"
|
||||
"\n"
|
||||
"# 1 = log how many times a second every cockpit display is actually\n"
|
||||
"# refreshed, to rpl4.log. Watching the screen cannot tell a display that\n"
|
||||
"# has stopped refreshing from one whose picture simply is not changing.\n"
|
||||
|
||||
+241
-45
@@ -1,4 +1,5 @@
|
||||
#include "rpl4.h"
|
||||
|
||||
#pragma hdrstop
|
||||
|
||||
#define PRELOAD_ART
|
||||
@@ -1951,7 +1952,75 @@ GPS::GPS(
|
||||
//-----------------------------------------------------------
|
||||
background = new Video8BitBuffered(width, height);
|
||||
Register_Object(background);
|
||||
|
||||
//
|
||||
//-----------------------------------------------------------------
|
||||
// RP412MAPRATE - how many of the sixteen rate steps the map redraws
|
||||
// on.
|
||||
//
|
||||
// The gauge renderer walks a 16-bit rate wheel: one bit per full
|
||||
// pass over every active gauge, shifted right each pass and reset at
|
||||
// the bottom (GAUGREND.cpp). A gauge redraws only on the step its
|
||||
// configured rate names, so a map on one bit redraws once per
|
||||
// SIXTEEN passes - and its update period is sixteen passes however
|
||||
// cheap the redraw is.
|
||||
//
|
||||
// That was fine when a pass was quick. Racing, the background loop
|
||||
// only gets what is left of the frame after the 3D, passes fall to
|
||||
// about five a second, and sixteen of them is over three seconds
|
||||
// between map updates - measured, on a 60 fps display with the 3D
|
||||
// perfectly smooth. Nothing is slow here; the map is just waiting
|
||||
// its turn on a wheel built for a machine that came round faster.
|
||||
//
|
||||
// Drawing on more of the steps costs one gauge's redraw per step,
|
||||
// against a pass that runs ninety of them. It does not make the
|
||||
// wheel turn faster - it stops the map needing a whole turn.
|
||||
//-----------------------------------------------------------------
|
||||
//
|
||||
{
|
||||
static int updates = -1;
|
||||
if (updates < 0)
|
||||
{
|
||||
const char *setting = getenv("RP412MAPRATE");
|
||||
updates = (setting != NULL) ? atoi(setting) : 16;
|
||||
// 1..16, and only the powers of two divide the wheel evenly
|
||||
if (updates > 16) updates = 16;
|
||||
if (updates < 1) updates = 1;
|
||||
}
|
||||
if (updates > 1)
|
||||
{
|
||||
GaugeRate mask = 0;
|
||||
for (int step = 0; step < 16; step += (16 / updates))
|
||||
{
|
||||
mask |= (GaugeRate)(0x8000 >> step);
|
||||
}
|
||||
//
|
||||
// QUALIFIED, both of them. This constructor's first
|
||||
// parameter is also called 'rate', so a bare assignment
|
||||
// here writes the PARAMETER and leaves the member holding
|
||||
// whatever the gauge data asked for - which is what it did,
|
||||
// silently, and cost a long hunt for a writer that did not
|
||||
// exist. oldRate is not shadowed, which is why it took the
|
||||
// value and the pair disagreed.
|
||||
//
|
||||
// Gauge::Disable(False) restores rate from oldRate when the
|
||||
// mode system enables a gauge, so both have to carry it or
|
||||
// the first activation puts the old rate back.
|
||||
//
|
||||
Gauge::rate = mask;
|
||||
Gauge::oldRate = mask;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
needsStaticUpdate = True;
|
||||
//
|
||||
// Nothing drawn yet, so there is no picture to add a placement to and
|
||||
// no scale it would be added at. The first pass builds both.
|
||||
//
|
||||
backgroundBuilt = False;
|
||||
builtMinX = builtMinY = builtMinZ = (Scalar) 0;
|
||||
builtMaxX = builtMaxY = builtMaxZ = (Scalar) 0;
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
@@ -1989,6 +2058,95 @@ void
|
||||
|
||||
void
|
||||
GPS::NotifyOfNewInterestingEntity(Entity *entity)
|
||||
{
|
||||
Check(this);
|
||||
Check(entity);
|
||||
if (!entity->IsDerivedFrom(*Terrain::GetClassDerivations()))
|
||||
{
|
||||
Check_Fpu();
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// A rebuild is already owed, or there is no picture to add to yet.
|
||||
//
|
||||
if (needsStaticUpdate || !backgroundBuilt)
|
||||
{
|
||||
needsStaticUpdate = True;
|
||||
Check_Fpu();
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Terrain arrives as you DRIVE - the interest system hands each piece
|
||||
// over as it comes into range, not all of it at load - and this used
|
||||
// to order a rebuild of the entire map for every one of them. A
|
||||
// rebuild redraws every placement on the track, so the cost of one
|
||||
// arriving piece was the whole track, and the map went seconds
|
||||
// between updates on the tracks that draw the most: Tour De Mars at
|
||||
// 351 placements and Ares' Armpits at 320, against 80-140 for a
|
||||
// typical one. It is also not interruptible - the gauge loop checks
|
||||
// its time slice BETWEEN gauges - so a rebuild stalled every other
|
||||
// display with it.
|
||||
//
|
||||
// But the bounds are what set the scale, and the scale is what the
|
||||
// whole cached picture was drawn at. An arrival that leaves the
|
||||
// bounds alone leaves every placement already on the map exactly
|
||||
// where it belongs, and the only thing missing from the picture is
|
||||
// the new one. Draw that, and nothing else.
|
||||
//
|
||||
// Only a piece that moves an EDGE of the track changes the scale, and
|
||||
// then the picture really is wrong everywhere and has to be redrawn.
|
||||
// That is rare, and it gets rarer as the track fills in.
|
||||
//
|
||||
// RPL4GaugeRenderer::NotifyOfNewInterestingEntity adds the entity to
|
||||
// staticEntities before chaining here, so these bounds already
|
||||
// account for the arrival being judged.
|
||||
//
|
||||
Scalar
|
||||
minX, minY, minZ,
|
||||
maxX, maxY, maxZ;
|
||||
|
||||
Check(renderer);
|
||||
renderer->GetStaticBounds(
|
||||
&minX, &minY, &minZ,
|
||||
&maxX, &maxY, &maxZ
|
||||
);
|
||||
|
||||
if (minX != builtMinX || minY != builtMinY || minZ != builtMinZ ||
|
||||
maxX != builtMaxX || maxY != builtMaxY || maxZ != builtMaxZ)
|
||||
{
|
||||
needsStaticUpdate = True;
|
||||
}
|
||||
else if (DrawStaticEntity(entity))
|
||||
{
|
||||
//
|
||||
// Only when something was actually drawn. Terrain without a
|
||||
// GaugeImage never reaches the map - on these tracks that is
|
||||
// most of it, 256 of Tour De Mars' 607 - and blitting the
|
||||
// background again for one of those is pure cost.
|
||||
//
|
||||
needsScreenUpdate = True;
|
||||
}
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// NotifyOfBecomingUninterestingEntity
|
||||
//#############################################################################
|
||||
//
|
||||
// The map showed only currently-interesting terrain before any of this,
|
||||
// and it still does. A placement cannot be un-drawn from a composited
|
||||
// picture, so a departure is the one case left that costs a full rebuild.
|
||||
//
|
||||
// Nothing used to listen for this at all: departures were swept up by the
|
||||
// rebuild that the very next ARRIVAL ordered, which is no longer ordered.
|
||||
// Without this the map would keep showing terrain that had gone until
|
||||
// something moved the bounds.
|
||||
//
|
||||
void
|
||||
GPS::NotifyOfBecomingUninterestingEntity(Entity *entity)
|
||||
{
|
||||
Check(this);
|
||||
Check(entity);
|
||||
@@ -2104,61 +2262,34 @@ void
|
||||
ChainIteratorOf<Entity*>
|
||||
i(staticEntityList.GetInstanceList());
|
||||
|
||||
Check(renderer);
|
||||
L4Warehouse
|
||||
*warehouse = (L4Warehouse *) renderer->warehousePointer;
|
||||
Check(warehouse);
|
||||
|
||||
Entity
|
||||
*entity;
|
||||
L4GaugeImage
|
||||
*gauge_image;
|
||||
|
||||
AffineMatrix
|
||||
worldToView,
|
||||
localToView;
|
||||
|
||||
Vector3D
|
||||
scaling_vector;
|
||||
//------------------------------------
|
||||
// Scale the display
|
||||
//------------------------------------
|
||||
|
||||
Verify(!Small_Enough(pixelsPerMeter));
|
||||
|
||||
scaling_vector.x = pixelsPerMeter;
|
||||
scaling_vector.y = pixelsPerMeter;
|
||||
scaling_vector.z = pixelsPerMeter;
|
||||
|
||||
worldToView.BuildIdentity();
|
||||
worldToView *= centeringOffset; // translation
|
||||
worldToView *= scaling_vector;
|
||||
int
|
||||
drawn = 0;
|
||||
|
||||
while ((entity=i.ReadAndNext()) != NULL)
|
||||
{
|
||||
Check(entity);
|
||||
//-------------------------------------
|
||||
// Draw image
|
||||
//-------------------------------------
|
||||
gauge_image = warehouse->
|
||||
gaugeImageBin.GetIfAlreadyExists(entity->GetResourceID());
|
||||
if (gauge_image != NULL)
|
||||
{
|
||||
Check(gauge_image);
|
||||
|
||||
localToView.Multiply(entity->localToWorld, worldToView);
|
||||
|
||||
gauge_image->Draw(
|
||||
LODIndex, // value set by creator
|
||||
metersPerPixel,
|
||||
&backgroundView,
|
||||
0, // default color
|
||||
(AffineMatrix &) localToView
|
||||
);
|
||||
|
||||
warehouse->gaugeImageBin.Release(entity->GetResourceID());
|
||||
}
|
||||
DrawStaticEntity(entity);
|
||||
}
|
||||
|
||||
//
|
||||
// The picture now matches these bounds, and they are what the scale
|
||||
// everything on it was drawn at came from. Remember them: an arrival
|
||||
// that leaves them alone can be added to this picture rather than
|
||||
// replacing it.
|
||||
//
|
||||
builtMinX = minX;
|
||||
builtMinY = minY;
|
||||
builtMinZ = minZ;
|
||||
builtMaxX = maxX;
|
||||
builtMaxY = maxY;
|
||||
builtMaxZ = maxZ;
|
||||
backgroundBuilt = True;
|
||||
|
||||
//-----------------------------------------------------------
|
||||
// Redraw new background, restart moving entity display
|
||||
//-----------------------------------------------------------
|
||||
@@ -2166,6 +2297,71 @@ void
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//
|
||||
//#############################################################################
|
||||
// DrawStaticEntity
|
||||
//#############################################################################
|
||||
//
|
||||
// One placement onto the cached background, at the scale that background
|
||||
// was built at. Shared by the full rebuild above and by the single-arrival
|
||||
// path in NotifyOfNewInterestingEntity, so the two cannot drift into
|
||||
// drawing the same track two different ways.
|
||||
//
|
||||
Logical
|
||||
GPS::DrawStaticEntity(Entity *entity)
|
||||
{
|
||||
Check(this);
|
||||
Check(entity);
|
||||
Check(background);
|
||||
Check(renderer);
|
||||
|
||||
L4Warehouse
|
||||
*warehouse = (L4Warehouse *) renderer->warehousePointer;
|
||||
Check(warehouse);
|
||||
|
||||
L4GaugeImage
|
||||
*gauge_image =
|
||||
warehouse->gaugeImageBin.GetIfAlreadyExists(entity->GetResourceID());
|
||||
if (gauge_image == NULL)
|
||||
{
|
||||
return False; // nothing of it appears on the map
|
||||
}
|
||||
Check(gauge_image);
|
||||
|
||||
L4BytePort
|
||||
backgroundPort(background, "background", 0);
|
||||
GraphicsView
|
||||
backgroundView(&backgroundPort);
|
||||
backgroundView.SetOrigin(width>>1, height>>1);
|
||||
|
||||
Vector3D
|
||||
scaling_vector;
|
||||
scaling_vector.x = pixelsPerMeter;
|
||||
scaling_vector.y = pixelsPerMeter;
|
||||
scaling_vector.z = pixelsPerMeter;
|
||||
|
||||
AffineMatrix
|
||||
worldToView,
|
||||
localToView;
|
||||
|
||||
worldToView.BuildIdentity();
|
||||
worldToView *= centeringOffset; // translation
|
||||
worldToView *= scaling_vector;
|
||||
|
||||
localToView.Multiply(entity->localToWorld, worldToView);
|
||||
|
||||
gauge_image->Draw(
|
||||
LODIndex, // value set by creator
|
||||
metersPerPixel,
|
||||
&backgroundView,
|
||||
0, // default color
|
||||
(AffineMatrix &) localToView
|
||||
);
|
||||
|
||||
warehouse->gaugeImageBin.Release(entity->GetResourceID());
|
||||
return True;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
GPS::Execute()
|
||||
|
||||
+27
-1
@@ -330,12 +330,27 @@ public:
|
||||
BecameActive(); // virtual function in 'GaugeBase'
|
||||
void
|
||||
NotifyOfNewInterestingEntity(Entity *entity);
|
||||
//
|
||||
// Terrain that leaves interest range is dropped from the map, and
|
||||
// there is no way to un-draw one placement from a composited picture
|
||||
// - so this is the one case that still costs a full rebuild.
|
||||
//
|
||||
void
|
||||
NotifyOfBecomingUninterestingEntity(Entity *entity);
|
||||
void
|
||||
Execute();
|
||||
|
||||
protected:
|
||||
void
|
||||
UpdateStaticEntities();
|
||||
//
|
||||
// Draw one placement into the cached background, at the scale that
|
||||
// background was built at. False if the entity has no GaugeImage and
|
||||
// so never appears on the map at all - which on the big tracks is
|
||||
// most of the terrain in them.
|
||||
//
|
||||
Logical
|
||||
DrawStaticEntity(Entity *entity);
|
||||
|
||||
enum
|
||||
{
|
||||
@@ -344,7 +359,18 @@ protected:
|
||||
|
||||
Logical
|
||||
needsStaticUpdate,
|
||||
needsScreenUpdate;
|
||||
needsScreenUpdate,
|
||||
// is there a picture worth adding a single placement to?
|
||||
backgroundBuilt;
|
||||
//
|
||||
// The static bounds the background was last built for. They decide
|
||||
// the scale, so terrain arriving inside them can be drawn onto the
|
||||
// picture instead of forcing a new one - see
|
||||
// GPS::NotifyOfNewInterestingEntity.
|
||||
//
|
||||
Scalar
|
||||
builtMinX, builtMinY, builtMinZ,
|
||||
builtMaxX, builtMaxY, builtMaxZ;
|
||||
Scalar
|
||||
LODIndex,
|
||||
metersPerPixel,
|
||||
|
||||
Reference in New Issue
Block a user