From f4fef294286539108f9cb8095aab76a782260d76 Mon Sep 17 00:00:00 2001 From: Cyd Date: Wed, 5 Aug 2026 22:36:45 -0500 Subject: [PATCH] The map keeps drawing when the view gets busy Two testers reported the map and the countdown clock freezing, one of them only on larger, more complex maps, and one of them until a death. Both details point at the same place. The gauges and the cockpit displays are redrawn in whatever time is left after the 3D view. The background loop is guaranteed a single pass per frame and gets more only while time remains before the frame is due, and one pass drew exactly one gauge. So a full sweep of ninety-odd gauges needed ninety-odd passes - free when there is spare frame, but on a busy map the 3D view eats all of it, the loop drops to its one guaranteed pass, and a sweep takes ninety-odd FRAMES. Seconds. A death makes the renderer skip every static object, the budget frees up, and the backlog drains at once: the display appears to come back to life. Worse, the copy phase that follows ended after a SINGLE display, so the map - one of three - came round only every third sweep. So: draw gauges to a 2ms slice rather than one per pass, which ties the refresh rate to elapsed time instead of to how much spare frame there happened to be; and copy every display before reporting the sweep done. Measured on a deliberately starved frame budget, which reproduces the reported symptom: 0.7 sweeps/s before, 3.1 after. At a normal budget 20/s, against 18-19 before - no cost to the healthy case. RP412GAUGESLICE tunes the slice and 0 restores the old behaviour, which reproduces the 0.7 exactly. RP412GAUGEDIAG=1 logs the rate; watching the screen cannot tell a display that has stopped refreshing from one whose picture simply is not changing, which is what made this hard to see. Also fixes the constructor calling Update() three lines before it initialised mDisplayToUpdate, so the first pass indexed the D3D device and surface arrays with whatever was on the stack. Co-Authored-By: Claude Opus 5 (1M context) --- MUNGA/GAUGREND.cpp | 47 ++++++++++++++++++++- MUNGA_L4/L4VB16.cpp | 98 +++++++++++++++++++++++++++++++++++++++++-- MUNGA_L4/L4VB16.h | 11 +++++ RP_L4/RPL4ENVIRON.cpp | 16 +++++++ 4 files changed, 167 insertions(+), 5 deletions(-) diff --git a/MUNGA/GAUGREND.cpp b/MUNGA/GAUGREND.cpp index 1daa82e..4275313 100644 --- a/MUNGA/GAUGREND.cpp +++ b/MUNGA/GAUGREND.cpp @@ -21,6 +21,31 @@ BitTrace Gauge_Renderer("Gauge Renderer"); #endif +// +// How long a single background pass may spend drawing gauges, in +// milliseconds. RP412GAUGESLICE tunes it; 0 restores the original +// behaviour of exactly one gauge per pass. +// +static long + GaugeSliceMs() +{ + static long + slice = -1L; + + if (slice < 0L) + { + const char + *setting = getenv("RP412GAUGESLICE"); + + slice = (setting != NULL) ? atol(setting) : 2L; + if (slice < 0L) + { + slice = 0L; + } + } + return slice; +} + //####################################################################### // Miscellaneous utilities //####################################################################### @@ -3683,7 +3708,27 @@ Logical case background: { - result = ProcessOneActiveGauge(); + //----------------------------------------------------------- + // Draw gauges until the slice is spent, rather than exactly + // one per pass. + // + // The background loop is only guaranteed a single pass per + // frame; it gets more only while time remains before the + // frame is due. On a busy map the 3D foreground eats the + // whole budget, so a cycle of ninety-odd gauges takes + // ninety-odd frames to come round and the displays sit + // frozen for seconds. Working to a slice makes the refresh + // rate depend on elapsed time instead of on how much spare + // frame there happened to be. + //----------------------------------------------------------- + Time slice_end = Now(); + slice_end += GaugeSliceMs(); + + do + { + result = ProcessOneActiveGauge(); + } + while (result && taskMode == background && Now() < slice_end); break; } diff --git a/MUNGA_L4/L4VB16.cpp b/MUNGA_L4/L4VB16.cpp index 89c9ebb..fad4f98 100644 --- a/MUNGA_L4/L4VB16.cpp +++ b/MUNGA_L4/L4VB16.cpp @@ -4358,6 +4358,15 @@ SVGA16::SVGA16( // Split-view mode: decide before BuildWindows so the packed gauge // windows can stay hidden. //------------------------------------------------------------------ + // + // Before anything else: the constructor calls Update() below, and + // both of these are read in there. mDisplayToUpdate was only being + // set at the END of the constructor, so that first pass indexed the + // display arrays with whatever was on the stack. + // + mDisplayToUpdate = 0; + mDisplaysCopiedThisPass = 0; + splitViews = False; cockpitViewscreen = NULL; Logical explodedViews = False; @@ -4997,12 +5006,16 @@ Logical SVGA16::Update(Logical forceAll) GaugeRenderer *renderer = application->GetGaugeRenderer(); if (!valid || renderer == NULL) { + mDisplaysCopiedThisPass = 0; CLEAR_SCREEN_COPY(); return False; // Do no more! } - if (++mDisplayToUpdate >= NUMGAUGEWINDOWS) + // one display per call; the rotation steps at the end of the function + if (mDisplayToUpdate >= NUMGAUGEWINDOWS) + { mDisplayToUpdate = 0; + } //Top MFD's L4GraphicsPort *UL = static_cast(renderer->GetGraphicsPort("auxUL2")); @@ -5045,7 +5058,10 @@ Logical SVGA16::Update(Logical forceAll) lrMask |= (lrMask << 16); } else { - //No MFDs to draw, break out early + //No MFDs to draw, break out early. The sweep counter resets + //too: leaving it part-used would keep the renderer in its + //copy phase, and it never draws another gauge while there. + mDisplaysCopiedThisPass = 0; return False; } } else @@ -5057,7 +5073,8 @@ Logical SVGA16::Update(Logical forceAll) secPalette = &((SVGA16 *) secPort->graphicsDisplay)->palette[secPort->paletteID]; } else { - //No secondary, skip + //No secondary, skip - and end the sweep, as above. + mDisplaysCopiedThisPass = 0; return False; } } @@ -5227,7 +5244,80 @@ Logical SVGA16::Update(Logical forceAll) // if (end.ticks - start.ticks > 100) // end = start; - return False; // True == 'more to do' + // + //------------------------------------------------------------------ + // Step the rotation, and say "more to do" until every display has + // had its turn. + // + // This used to return False unconditionally, which told the gauge + // renderer its copy phase was over after a SINGLE display. A full + // gauge sweep - one gauge per background pass, so as many passes as + // there are active gauges - therefore refreshed one display, and the + // map, one of three, came round only every third sweep. + // + // That is invisible with frame time to spare, because the background + // loop keeps running until the frame budget is used up and gets + // through several sweeps. On a big map the 3D foreground eats the + // whole budget, the loop drops to the one pass per frame it is + // guaranteed, and the map goes seconds between refreshes - which is + // what the field reports describe, on exactly those maps. A death + // makes the renderer skip every static object, the budget frees up, + // and the backlog drains at once: the display appears to come back + // to life, which is the tell that led here. + //------------------------------------------------------------------ + // + mDisplayToUpdate++; + if (mDisplayToUpdate >= NUMGAUGEWINDOWS) + { + mDisplayToUpdate = 0; + } + + if (++mDisplaysCopiedThisPass < NUMGAUGEWINDOWS) + { + return True; // call again - there are displays waiting + } + mDisplaysCopiedThisPass = 0; + + // + // RP412GAUGEDIAG=1 reports how often the displays are actually being + // refreshed. Pixel-watching from outside cannot tell a display that + // is not refreshing from one whose picture simply is not changing, + // and that ambiguity is exactly what makes "my map froze" hard to + // pin down. This counts the real thing. + // + { + static int diagnostics = -1; + if (diagnostics < 0) + { + const char *setting = getenv("RP412GAUGEDIAG"); + diagnostics = (setting != NULL && atoi(setting) != 0) ? 1 : 0; + } + if (diagnostics) + { + static unsigned long window_start = 0; + static int sweeps = 0; + unsigned long now = GetTickCount(); + ++sweeps; + if (window_start == 0) + { + window_start = now; + } + else if (now - window_start >= 2000) + { + // tenths, by hand: whole sweeps per second rounds the + // interesting cases - a starved pipeline managing two + // thirds of a sweep a second reads as a flat "0/s". + int tenths = sweeps * 10000 / (int)(now - window_start); + DEBUG_STREAM << "GaugeDiag: " << sweeps << " display sweep(s) in " + << (now - window_start) << " ms (" + << (tenths / 10) << '.' << (tenths % 10) << "/s, " + << NUMGAUGEWINDOWS << " displays each)\n" << std::flush; + window_start = now; + sweeps = 0; + } + } + } + return False; // the sweep is complete } diff --git a/MUNGA_L4/L4VB16.h b/MUNGA_L4/L4VB16.h index 1394ef6..56eb1fe 100644 --- a/MUNGA_L4/L4VB16.h +++ b/MUNGA_L4/L4VB16.h @@ -293,6 +293,17 @@ private: int mDisplayToUpdate; + //------------------------------------------------------------------ + // How many displays this copy pass has refreshed. + // + // The gauge renderer's copy phase ends the moment Update() reports it + // has finished, and Update() reported that after ONE display - so a + // whole gauge sweep refreshed a single display, and the map, one of + // three, came round only every third sweep. Counting them out means + // one sweep refreshes all of them. + //------------------------------------------------------------------ + int mDisplaysCopiedThisPass; + //------------------------------------------------------------------ // Split-view mode (L4MFDSPLIT=1): the five channel-packed MFDs and // the rotated map render as their own desktop windows; the packed diff --git a/RP_L4/RPL4ENVIRON.cpp b/RP_L4/RPL4ENVIRON.cpp index 24b6ee6..724cea9 100644 --- a/RP_L4/RPL4ENVIRON.cpp +++ b/RP_L4/RPL4ENVIRON.cpp @@ -185,6 +185,22 @@ namespace "# default is 60; the arcade pods shipped at 25.\n" "TARGETFPS=60\n" "\n" +"# How long one background pass may spend drawing cockpit gauges, in\n" +"# milliseconds. The gauges and the MFD/map displays are redrawn in the\n" +"# time left over after the 3D view; on a big, busy map there is none\n" +"# left, and at the original one-gauge-per-pass the map and the countdown\n" +"# clock could sit frozen for seconds at a time - until something (a\n" +"# death, say) lightened the 3D view enough for the backlog to drain.\n" +"# Working to a slice ties the refresh rate to elapsed time instead. Set\n" +"# 0 for the old behaviour; raise it to favour the displays over frame\n" +"# rate.\n" +"RP412GAUGESLICE=2\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" +"#RP412GAUGEDIAG=1\n" +"\n" "# 1 = Steam networking (lobbies, FakeIP mesh). Needs the Steam client\n" "# running and steam_appid.txt beside the exe; without them the game\n" "# logs the reason and falls back to plain TCP. 0 = TCP only.\n"