From 5bbe070e9d36ed3207a02cd73fe5e4dee79a5dee Mon Sep 17 00:00:00 2001 From: arcattack Date: Sat, 25 Jul 2026 09:52:28 -0500 Subject: [PATCH] Gitea #56 (+#50): pin the windowed backbuffer so a device reset cannot silently re-size it ROOT CAUSE. mPresentParams.BackBufferWidth/Height were assigned ONLY in the fullscreen branch (L4VIDEO.cpp:3430-3434); windowed they stayed 0, which tells D3D9 to derive the backbuffer from the device window's client area at CreateDevice. Fine at startup -- but the device-lost recovery path saves and restores mPresentParams around Reset(), so it faithfully preserved those ZEROS and Reset re-derived the backbuffer from whatever the client area was at THAT moment, while gBTCockpitCanvasW/H still described the startup canvas. The cockpit layout is computed in backbuffer space and clicks are mapped client -> canvas (L4VB16.cpp BTCockpitMouseDown), so after that divergence panels are drawn for one geometry and clicks are mapped in another: buttons drift off their artwork, and at a large enough mismatch nothing is clickable at all -- which is #50 (SAURON: 'none of my MFDs clickable' while other players' worked). It needs BOTH a resize/maximise AND a device loss (alt-tab, Steam overlay, monitor sleep, UAC, RDP, driver hiccup), in that order. That is why it hits some players and never the operator, who does not resize the cockpit window -- and it matches the report exactly: the buttons 'become' misaligned rather than starting that way. FIX * windowed: record the client rect into BackBufferWidth/Height at CreateDevice. That is exactly what D3D would have derived, so startup behaviour is unchanged, but the save/restore now preserves a REAL size and Reset can never pick a different one. Deliberately NOT screenWidth/screenHeight -- those are the app's -res values (default 800x600) while the cockpit window is sized independently, so pinning to them would shrink the backbuffer. * BTVerifyCockpitCanvasAfterReset(): called at both device-lost recovery sites; compares the real backbuffer against the canvas globals, and if they ever disagree it says so and re-syncs instead of drifting silently. VERIFIED: solo cockpit run unchanged -- '[cockpit] view 900x440 canvas 1452x999', BT_SHOT still 1452x999, Heat panel renders correctly, no mismatch warning. NOTE: reverted an earlier scripted edit of this file that had corrupted an unrelated line -- L4VIDEO.cpp is CRLF and a Python join('\n') introduced a bare LF inside a string literal. Use the edit tool on this file, not line surgery. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg --- engine/MUNGA_L4/L4VIDEO.cpp | 81 +++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index 1982a4b..f560208 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -3432,6 +3432,44 @@ DPLRenderer::DPLRenderer( mPresentParams.BackBufferWidth = screenWidth; mPresentParams.BackBufferHeight = screenHeight; } + else + { + // + // PIN THE WINDOWED BACKBUFFER (Gitea #56, and #50 -- the same bug). + // + // These were left at 0 for the windowed case, which tells D3D9 to derive the + // backbuffer from the device window's client area at CreateDevice time. Fine + // at startup -- but the device-lost recovery path saves and restores + // mPresentParams around Reset(), so it faithfully preserves the ZEROS and + // Reset re-derives the backbuffer from whatever the client area is at THAT + // moment. + // + // Field consequence: a player resizes or maximises the window (harmless by + // itself -- the cockpit layout lives in backbuffer space and the hit-test maps + // client -> canvas), then the device is lost (alt-tab, Steam overlay, monitor + // sleep, UAC prompt, RDP, driver hiccup). On recovery the backbuffer silently + // becomes the NEW client size while gBTCockpitCanvasW/H still describe the + // startup canvas -- so panels are drawn for one geometry and clicks are mapped + // in another. Buttons drift off their artwork, and at a big enough mismatch + // nothing is clickable at all (#50, "none of my MFDs clickable" while other + // players' worked). It needs BOTH steps in that order, which is why the + // operator -- who never resizes the cockpit window -- has never seen it. + // + // Recording the client rect here yields exactly what D3D would have derived, + // so startup behaviour is unchanged, but the save/restore now preserves a REAL + // size and Reset can never silently pick a different one. (Do NOT use + // screenWidth/screenHeight: those are the app's -res values, default 800x600, + // while the cockpit-surround window is sized independently -- pinning to them + // would shrink the backbuffer and break the composite.) + // + RECT rc; + if (hWnd != NULL && GetClientRect(hWnd, &rc) + && rc.right > rc.left && rc.bottom > rc.top) + { + mPresentParams.BackBufferWidth = (UINT)(rc.right - rc.left); + mPresentParams.BackBufferHeight = (UINT)(rc.bottom - rc.top); + } + } HRESULT hr; @@ -8067,6 +8105,45 @@ void } } +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// BTVerifyCockpitCanvasAfterReset (Gitea #56) +// +// The cockpit-surround layout is computed in BACKBUFFER space and clicks are +// mapped client -> backbuffer through gBTCockpitCanvasW/H (L4VB16.cpp +// BTCockpitMouseDown). With the windowed backbuffer now pinned at CreateDevice +// this should always agree -- so a mismatch here means the surface was re-created +// at a different size behind our back, which is exactly the state where panels +// draw in one geometry and clicks land in another. Say so, and re-sync, rather +// than drift silently for the rest of the session. +// +static void BTVerifyCockpitCanvasAfterReset(LPDIRECT3DDEVICE9 device) +{ + extern int gBTGaugeCockpit, gBTCockpitCanvasW, gBTCockpitCanvasH; + if (device == NULL || !gBTGaugeCockpit || gBTCockpitCanvasW <= 0) + { + return; + } + IDirect3DSurface9 *back = NULL; + if (FAILED(device->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &back)) + || back == NULL) + { + return; + } + D3DSURFACE_DESC desc; + if (SUCCEEDED(back->GetDesc(&desc)) + && ((int)desc.Width != gBTCockpitCanvasW + || (int)desc.Height != gBTCockpitCanvasH)) + { + DEBUG_STREAM << "[resize] Gitea #56: backbuffer is " << desc.Width << "x" + << desc.Height << " after Reset but the cockpit canvas is " + << gBTCockpitCanvasW << "x" << gBTCockpitCanvasH + << " -- clicks would be mis-mapped; re-syncing" << std::endl; + gBTCockpitCanvasW = (int)desc.Width; + gBTCockpitCanvasH = (int)desc.Height; + } + back->Release(); +} + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Execute Method, performs the rendering of one frame // @@ -8801,6 +8878,8 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte mPresentParams.BackBufferCount = bbCount; mPresentParams.BackBufferWidth = bbWidth; mPresentParams.BackBufferHeight = bbHeight; + + BTVerifyCockpitCanvasAfterReset(mDevice); // Gitea #56 guard } ticks = HiResNowTicks(); @@ -9079,6 +9158,8 @@ void DPLRenderer::ExecuteIdle() mPresentParams.BackBufferCount = bbCount; mPresentParams.BackBufferWidth = bbWidth; mPresentParams.BackBufferHeight = bbHeight; + + BTVerifyCockpitCanvasAfterReset(mDevice); // Gitea #56 guard } } }