From f3d27f51c2f752777d16c1d310095978f9a68a16 Mon Sep 17 00:00:00 2001 From: Cyd Date: Sun, 9 Aug 2026 22:40:25 -0500 Subject: [PATCH 1/3] glass panels: drop HALFTONE stretch (the per-display-mode perf sink) Playtester (Dave, SCREECH-PC, 4K) reported ~20 fps in the exploded per-display glass panels vs ~130 fps in the cockpit surround -- same scene, same machine (solo_20260809.log; maxDraw 57-92ms in panels vs 6-31ms surround). Root cause: BlitSurface used SetStretchBltMode(HALFTONE) -- GDI's slowest, per-output-pixel resample filter -- and BTGlassPanels_Tick repaints all 7 glass windows SYNCHRONOUSLY on the main render thread every ~16 Hz. 7x HALFTONE StretchDIBits of a 640x480 surface per pump stalls the frame. Default to COLORONCOLOR (nearest); BT_GLASS_SMOOTH=1 restores HALFTONE. The MFDs are low-res pixel content, so nearest reads crisp -- arguably closer to the pod CRT than the blur. A/B on the dev box (fast, understates Dave's gain): avg work 3.3->1.4ms, maxDraw stall 12-27->7ms, ~2.2x more frames per window. Co-Authored-By: Claude Opus 4.8 --- engine/MUNGA_L4/L4GLASSWIN.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/engine/MUNGA_L4/L4GLASSWIN.cpp b/engine/MUNGA_L4/L4GLASSWIN.cpp index ded2c53..744417c 100644 --- a/engine/MUNGA_L4/L4GLASSWIN.cpp +++ b/engine/MUNGA_L4/L4GLASSWIN.cpp @@ -1180,7 +1180,16 @@ static void info.bmiHeader.biCompression = BI_RGB; const RECT &r = w->surfaceRect; - SetStretchBltMode(dc, HALFTONE); + // STRETCH MODE (perf, 2026-08-09): default COLORONCOLOR. HALFTONE runs a + // per-output-pixel resample filter; done synchronously for all 7 glass windows + // every ~16 Hz repaint (BTGlassPanels_Tick), it was the per-display-mode perf + // sink -- playtesters saw ~20 fps in the exploded panels vs ~130 fps in the + // cockpit surround (same scene, same machine). The MFDs are low-res pixel + // content, so nearest-neighbour reads crisp (and closer to the pod CRT). + // BT_GLASS_SMOOTH=1 restores HALFTONE for anyone who prefers smoothing to speed. + static int sSmooth = -1; + if (sSmooth < 0) sSmooth = getenv("BT_GLASS_SMOOTH") ? 1 : 0; + SetStretchBltMode(dc, sSmooth ? HALFTONE : COLORONCOLOR); SetBrushOrgEx(dc, 0, 0, NULL); StretchDIBits(dc, r.left, r.top, r.right - r.left, r.bottom - r.top, From c9e25e59c5346a6e9ae2d2628d3eeb514209058c Mon Sep 17 00:00:00 2001 From: Cyd Date: Sun, 9 Aug 2026 23:01:20 -0500 Subject: [PATCH 2/3] 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 --- engine/MUNGA_L4/L4GLASSWIN.cpp | 94 ++++++++++++++++++++++++++++++++-- engine/MUNGA_L4/L4VB16.cpp | 22 ++++++++ 2 files changed, 113 insertions(+), 3 deletions(-) diff --git a/engine/MUNGA_L4/L4GLASSWIN.cpp b/engine/MUNGA_L4/L4GLASSWIN.cpp index 744417c..7fb0ad2 100644 --- a/engine/MUNGA_L4/L4GLASSWIN.cpp +++ b/engine/MUNGA_L4/L4GLASSWIN.cpp @@ -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(gr->GetGraphicsPort(ports[i])); + if (p == NULL) continue; + combined |= p->GetBitMask(); + if (svga == NULL) svga = static_cast(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; } } } diff --git a/engine/MUNGA_L4/L4VB16.cpp b/engine/MUNGA_L4/L4VB16.cpp index 879b5ef..cd108c4 100644 --- a/engine/MUNGA_L4/L4VB16.cpp +++ b/engine/MUNGA_L4/L4VB16.cpp @@ -692,6 +692,28 @@ void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int pa device->DrawPrimitiveUP(D3DPT_TRIANGLEFAN, 2, quad, sizeof(InsetVert)); } +//===========================================================================// +// GLASS dirty-skip: FNV-1a over the shared pixelBuffer masked to `mask` -- the bits +// one glass window can show. The glass repaint pump compares this per window and +// re-blits only the ones whose plane changed (L4GLASSWIN BTGlassPanels_Tick). Full +// pass (no stride) so a single-word gauge change is never missed; ~640*480 cheap +// integer ops, run at most once per window per ~16 Hz pump. +//===========================================================================// +unsigned long SVGA16::PlaneChecksum(int mask) const +{ + int w = pixelBuffer.Data.Size.x; + int h = pixelBuffer.Data.Size.y; + const Word *p = pixelBuffer.Data.MapPointer; + if (p == NULL || w <= 0 || h <= 0) + return 0; + unsigned long sum = 2166136261UL; // FNV-1a offset basis + Word m = (Word)mask; + int n = w * h; + for (int i = 0; i < n; ++i) + sum = (sum ^ (unsigned long)(p[i] & m)) * 16777619UL; + return sum; +} + //===========================================================================// // GLASS per-display windows -- the CPU (no-D3D) analog of DrawDevSurface: expand // one bit-plane of the shared gauge pixelBuffer into a 32-bit BGRA image that the From 3aeb2dbbe863e93d26a1bf98cbe85143695ff9c3 Mon Sep 17 00:00:00 2001 From: Cyd Date: Sun, 9 Aug 2026 23:03:41 -0500 Subject: [PATCH 3/3] glass panels: add the PlaneChecksum header decl (completes c9e25e5) The dirty-skip commit staged the header via a lowercase path (l4vb16.h) while git tracks it as L4VB16.h on this case-insensitive FS, so the PlaneChecksum declaration was left out -- c9e25e5 as committed would not build from clean (L4VB16.cpp defines SVGA16::PlaneChecksum, L4GLASSWIN.cpp calls it, but the header never declared it). This adds the one-line decl. HEAD now builds standalone. Co-Authored-By: Claude Opus 4.8 --- engine/MUNGA_L4/L4VB16.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/engine/MUNGA_L4/L4VB16.h b/engine/MUNGA_L4/L4VB16.h index f6dc0a1..bf7d6e1 100644 --- a/engine/MUNGA_L4/L4VB16.h +++ b/engine/MUNGA_L4/L4VB16.h @@ -419,6 +419,12 @@ public: // dwords; the image is written TOP-DOWN. *outW/*outH receive the produced size. void ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotateQuadrant, unsigned long *dst, int *outW, int *outH); + + // GLASS dirty-skip (L4GLASSWIN, 2026-08-09): FNV-1a checksum of the shared + // gauge pixelBuffer, masked to the bits a given window can show. Lets the + // glass repaint pump re-blit ONLY the windows whose plane actually changed + // (idle MFDs / static panels skip; the sweeping radar keeps updating). + unsigned long PlaneChecksum(int mask) const; }; //########################################################################