diff --git a/RP_L4/RPL4ENVIRON.cpp b/RP_L4/RPL4ENVIRON.cpp index f5a10cd..0f1764c 100644 --- a/RP_L4/RPL4ENVIRON.cpp +++ b/RP_L4/RPL4ENVIRON.cpp @@ -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" diff --git a/RP_L4/RPL4GAUG.cpp b/RP_L4/RPL4GAUG.cpp index 3f6b8bc..55f80f8 100644 --- a/RP_L4/RPL4GAUG.cpp +++ b/RP_L4/RPL4GAUG.cpp @@ -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 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() diff --git a/RP_L4/RPL4GAUG.h b/RP_L4/RPL4GAUG.h index e4eee11..02feaa1 100644 --- a/RP_L4/RPL4GAUG.h +++ b/RP_L4/RPL4GAUG.h @@ -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,