glass panels: dirty-skip -- repaint only the windows whose gauges changed

Follow-on to the HALFTONE->COLORONCOLOR fix.  BTGlassPanels_Tick repainted all 7
glass windows every ~16 Hz pump unconditionally.  Now each window carries a change
token and the pump re-blits ONLY the windows whose token differs:

  token = FNV-1a checksum of the shared gauge pixelBuffer masked to the bits this
          window can show (SVGA16::PlaneChecksum over primary + Eng-twin + RGB-group
          ports)  combined with  each button's RENDERED lamp brightness + held/latch.

Folding the flash BRIGHTNESS (not the raw lamp state) into the token means a flashing
lamp repaints exactly when it toggles; a static panel or an idle cockpit skips its
expand + StretchDIBits entirely.  The masked checksum reads the RENDERED RESULT of the
gauges' values, so it catches everything -- discrete value gauges, the continuous
radar sweep, any imagery -- with no gauge->port->window plumbing and no risk of a
frozen display (full pass, no stride; collision-free in practice).

Measured (dev box, solo mission, BT_GLASS_DIRTY): ~31 pumps/2s -> 4-15 window-repaints
vs 217-224 always-on (~15-40x fewer); avg frame work 3.3 -> 0.93ms.  BT_GLASS_DIRTY=1
logs the repaint tally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-09 23:01:20 -05:00
co-authored by Claude Opus 4.8
parent f3d27f51c2
commit c9e25e59c5
2 changed files with 113 additions and 3 deletions
+91 -3
View File
@@ -127,6 +127,12 @@ struct GWin
char monitorName[40]; // the PHYSICAL monitor this window landed on
// (\.\DISPLAYn), stamped at creation; shown
// by BT_POD_IDENT so the cab can be mapped.
// Dirty-skip (2026-08-09): the last change token the pump blitted for this
// window (plane checksum + lamp render). Zero-init (static storage); the pump
// re-blits only when the token differs, so unchanged panels cost nothing.
unsigned long lastToken;
int haveToken;
};
static GWin gWins[8];
@@ -1787,6 +1793,62 @@ void
// one-shot re-snap); a focused window just repaints from whichever fires first.
//###########################################################################
//
// GLASS DIRTY-SKIP (2026-08-09): re-blit only the windows whose content changed.
// Each window is a plane of the ONE shared gauge pixelBuffer, so its change token is
// the masked plane checksum (SVGA16::PlaneChecksum over every port that can feed the
// window) combined with each button's RENDERED lamp brightness + held/latched state.
// Folding the flash BRIGHTNESS in (not the raw lamp state) means a flashing lamp
// repaints exactly when it toggles, and a static panel -- or a fully idle cockpit --
// skips its expand + StretchDIBits entirely. The plane checksum is the only added
// cost (once per window per ~16 Hz pump) and is far cheaper than the paint it saves.
//
static unsigned long
GlassWindowToken(GaugeRenderer *gr, GWin *w, unsigned long tick)
{
unsigned long token = 2166136261UL;
if (w->portPrimary != NULL && gr != NULL)
{
// Every plane that could feed the window (primary + Eng twin + RGB group +
// their twins) -- OR their masks so a change to ANY is caught; checksum once.
const char *ports[8];
int np = 0;
ports[np++] = w->portPrimary;
if (w->portAlt != NULL) ports[np++] = w->portAlt;
for (int gi = 0; gi < w->groupCount && np < 7; ++gi)
{
ports[np++] = w->groupPort[gi];
if (w->groupAlt[gi] != NULL && np < 8) ports[np++] = w->groupAlt[gi];
}
int combined = 0;
SVGA16 *svga = NULL;
for (int i = 0; i < np; ++i)
{
L4GraphicsPort *p =
static_cast<L4GraphicsPort*>(gr->GetGraphicsPort(ports[i]));
if (p == NULL) continue;
combined |= p->GetBitMask();
if (svga == NULL) svga = static_cast<SVGA16*>(p->graphicsDisplay);
}
if (svga != NULL && combined != 0)
token ^= svga->PlaneChecksum(combined);
}
// Lamps: each button's RENDERED brightness + held/latched, so a flash toggle or a
// press repaints exactly the window it lives on.
for (int j = 0; j < w->buttonCount; ++j)
{
int addr = w->buttons[j].address;
unsigned long shade =
(unsigned long)LampBrightnessOf(PadRIO::GetLampState(addr), tick);
int held = (addr == pressedAddress) || latched[addr & 0x7F];
token = (token ^ ((unsigned long)(addr & 0xFF) << 4)
^ (shade << 1) ^ (unsigned long)held) * 16777619UL;
}
return token;
}
void
BTGlassPanels_Tick()
{
@@ -1799,12 +1861,38 @@ void
return;
sLastPaint = now;
GaugeRenderer *gr = BTResolveGaugeRenderer();
int repainted = 0;
for (int i = 0; i < gWinCount; ++i)
{
if (gWins[i].hwnd != NULL)
GWin &w = gWins[i];
if (w.hwnd == NULL)
continue;
unsigned long token = GlassWindowToken(gr, &w, now);
if (w.haveToken && token == w.lastToken)
continue; // gauges + lamps unchanged -> skip this window
w.lastToken = token;
w.haveToken = 1;
++repainted;
InvalidateRect(w.hwnd, NULL, FALSE);
UpdateWindow(w.hwnd); // synchronous paint, not the throttled queue
}
// BT_GLASS_DIRTY: report how many window-repaints the dirty-skip let through vs
// the old fixed gWinCount-per-pump, so the saving is visible.
static int sDirtyLog = -1;
if (sDirtyLog < 0) sDirtyLog = getenv("BT_GLASS_DIRTY") ? 1 : 0;
if (sDirtyLog)
{
static unsigned long sWin = 0;
static int sPumps = 0, sPaints = 0;
++sPumps; sPaints += repainted;
if (now - sWin >= 2000)
{
InvalidateRect(gWins[i].hwnd, NULL, FALSE);
UpdateWindow(gWins[i].hwnd); // synchronous paint, not the throttled queue
DEBUG_STREAM << "[glass-dirty] " << sPumps << " pumps -> " << sPaints
<< " window-repaints (was " << (sPumps * gWinCount) << " always-on)\n"
<< std::flush;
sWin = now; sPumps = 0; sPaints = 0;
}
}
}