diff --git a/context/glass-cockpit.md b/context/glass-cockpit.md index 67fdf83..373bae7 100644 --- a/context/glass-cockpit.md +++ b/context/glass-cockpit.md @@ -63,6 +63,21 @@ version skew (a stale steam exe shipped 2 days behind), tripled build time, and ([[reconstruction-gotchas]]); everything links always, so this class of trap no longer varies by build flavor. +## The pre-mission WAITING SCREEN (2026-07-22) [T2] +Every pre-run state (seat wait, roster full, WaitingForEgg, LoadingMission, WaitingForLaunch) +paints an animated overlay -- Consolas green text + a 12-segment spinner phased on +GetTickCount/80 (`BTWaitScreenPaintDC`, L4VIDEO.cpp) -- so a joining player never sees "Not +Responding" or a frozen frame. Two paint paths: PRE-DEVICE (relay seat wait, L4NET +`RelayWaitTick`) paints the window DC directly (nothing else presents -- safe); ONCE THE +DEVICE EXISTS (`DPLRenderer::ExecuteIdle`) it must NOT paint after Present (the black D3D +frame alternating with GDI = the load-time flicker, user-reported) -- and the backbuffer +refuses `GetDC` (D3DERR_INVALIDCALL without D3DPRESENTFLAG_LOCKABLE_BACKBUFFER, which we +won't force game-wide), so the overlay GDI-paints into a cached OFFSCREEN PLAIN surface +(`mWaitOverlaySurface`, GetDC always legal there) and `StretchRect`s onto the backbuffer +BEFORE Present. D3DPOOL_DEFAULT: released at every device-Reset site. One-shot boot log +`[waitscreen] paint path:` says which path is live. Verified: 14 rapid PrintWindow frames +all carry the text (was alternating black). + ## Plan of record (2026-07-17) Working branch `glass-cockpit` (from master `e2c21c4`). Steps: (1) gates+scaffolding [THIS], diff --git a/engine/MUNGA_L4/L4VIDEO.cpp b/engine/MUNGA_L4/L4VIDEO.cpp index 3a7a05a..7056c61 100644 --- a/engine/MUNGA_L4/L4VIDEO.cpp +++ b/engine/MUNGA_L4/L4VIDEO.cpp @@ -8750,6 +8750,11 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte } if (hr == D3DERR_DEVICELOST) { + if (mWaitOverlaySurface != NULL) + { + mWaitOverlaySurface->Release(); // D3DPOOL_DEFAULT: pre-Reset + mWaitOverlaySurface = NULL; + } int bbCount = mPresentParams.BackBufferCount; int bbWidth = mPresentParams.BackBufferWidth; int bbHeight = mPresentParams.BackBufferHeight; @@ -8779,18 +8784,10 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte // after each ExecuteIdle Present during WaitingForEgg (the black idle frame). // A 12-segment spinner phases on the tick clock; text centered above it. // -void BTWaitScreenPaint(const char *line1, const char *line2) +static void BTWaitScreenPaintDC(HDC dc, int w, int h, + const char *line1, const char *line2) { - extern void *BTResolveMainWindow(); - HWND wnd = (HWND)BTResolveMainWindow(); - if (wnd == NULL) - return; - HDC dc = GetDC(wnd); - if (dc == NULL) - return; - RECT rc; - GetClientRect(wnd, &rc); - int w = rc.right - rc.left, h = rc.bottom - rc.top; + RECT rc = { 0, 0, w, h }; FillRect(dc, &rc, (HBRUSH)GetStockObject(BLACK_BRUSH)); static HFONT s_font = NULL, s_fontDim = NULL; @@ -8829,6 +8826,21 @@ void BTWaitScreenPaint(const char *line1, const char *line2) SelectObject(dc, old_pen); DeleteObject(pen); } +} + +void BTWaitScreenPaint(const char *line1, const char *line2) +{ + extern void *BTResolveMainWindow(); + HWND wnd = (HWND)BTResolveMainWindow(); + if (wnd == NULL) + return; + HDC dc = GetDC(wnd); + if (dc == NULL) + return; + RECT rc; + GetClientRect(wnd, &rc); + BTWaitScreenPaintDC(dc, rc.right - rc.left, rc.bottom - rc.top, + line1, line2); ReleaseDC(wnd, dc); } @@ -8842,10 +8854,14 @@ void DPLRenderer::ExecuteIdle() hr = mDevice->EndScene(); - // WaitingForEgg is a black frame -- overlay the waiting screen (GDI, - // repainted after every Present; join/join_lan UX fix 2026-07-22). + // The waiting screen (join/join_lan UX fix 2026-07-22; FLICKER fix + // 2026-07-22): paint the text + spinner INTO the backbuffer via the + // surface's GDI handle BEFORE Present, so the presented frame already + // carries the overlay -- the old paint-AFTER-Present alternated a black + // D3D frame with the GDI redraw and visibly flickered during loads. + // GetDC-on-backbuffer failure (unusual formats) falls back to the old + // window-DC paint. { - HRESULT present_hr = mDevice->Present(NULL, NULL, NULL, NULL); const char *wait_line1 = "WAITING FOR MISSION ASSIGNMENT"; const char *wait_line2 = "the operator starts the session from the console"; switch (application ? application->GetApplicationState() : -1) @@ -8860,9 +8876,75 @@ void DPLRenderer::ExecuteIdle() wait_line2 = "waiting for the operator to launch"; break; } - BTWaitScreenPaint(wait_line1, wait_line2); + // The backbuffer refuses GetDC on this driver (D3DERR_INVALIDCALL + // without D3DPRESENTFLAG_LOCKABLE_BACKBUFFER, which we won't force on + // the whole game). Standard workaround: GDI-paint into a cached + // OFFSCREEN PLAIN surface (GetDC always works there) and StretchRect + // it onto the backbuffer pre-Present. The cache is D3DPOOL_DEFAULT, + // released on device loss below. + Logical painted_in_backbuffer = False; + IDirect3DSurface9 *backbuffer = NULL; + if (SUCCEEDED(mDevice->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, + &backbuffer)) && backbuffer != NULL) + { + D3DSURFACE_DESC desc; + if (SUCCEEDED(backbuffer->GetDesc(&desc))) + { + if (mWaitOverlaySurface != NULL) + { + D3DSURFACE_DESC have; + if (FAILED(mWaitOverlaySurface->GetDesc(&have)) + || have.Width != desc.Width + || have.Height != desc.Height) + { + mWaitOverlaySurface->Release(); + mWaitOverlaySurface = NULL; + } + } + if (mWaitOverlaySurface == NULL) + { + mDevice->CreateOffscreenPlainSurface(desc.Width, + desc.Height, D3DFMT_X8R8G8B8, D3DPOOL_DEFAULT, + &mWaitOverlaySurface, NULL); + } + HDC surface_dc = NULL; + if (mWaitOverlaySurface != NULL + && SUCCEEDED(mWaitOverlaySurface->GetDC(&surface_dc)) + && surface_dc != NULL) + { + BTWaitScreenPaintDC(surface_dc, (int)desc.Width, + (int)desc.Height, wait_line1, wait_line2); + mWaitOverlaySurface->ReleaseDC(surface_dc); + if (SUCCEEDED(mDevice->StretchRect(mWaitOverlaySurface, + NULL, backbuffer, NULL, D3DTEXF_NONE))) + { + painted_in_backbuffer = True; + } + } + } + backbuffer->Release(); + } + HRESULT present_hr = mDevice->Present(NULL, NULL, NULL, NULL); + static int s_wait_path_logged = 0; + if (!s_wait_path_logged) + { + s_wait_path_logged = 1; + DEBUG_STREAM << "[waitscreen] paint path: " + << (painted_in_backbuffer ? "BACKBUFFER (flicker-free)" + : "window-DC FALLBACK (GetDC failed)") + << std::endl << std::flush; + } + if (!painted_in_backbuffer) + { + BTWaitScreenPaint(wait_line1, wait_line2); + } if (present_hr == D3DERR_DEVICELOST) { + if (mWaitOverlaySurface != NULL) + { + mWaitOverlaySurface->Release(); // D3DPOOL_DEFAULT: must go pre-Reset + mWaitOverlaySurface = NULL; + } int bbCount = mPresentParams.BackBufferCount; int bbWidth = mPresentParams.BackBufferWidth; int bbHeight = mPresentParams.BackBufferHeight; diff --git a/engine/MUNGA_L4/L4VIDEO.h b/engine/MUNGA_L4/L4VIDEO.h index 5f3cf8b..6ed5999 100644 --- a/engine/MUNGA_L4/L4VIDEO.h +++ b/engine/MUNGA_L4/L4VIDEO.h @@ -449,6 +449,10 @@ private: // LPDIRECT3DDEVICE9 mDevice; D3DPRESENT_PARAMETERS mPresentParams; + // wait-screen overlay cache (ExecuteIdle GDI paint -> StretchRect; the + // backbuffer refuses GetDC on some drivers). D3DPOOL_DEFAULT -- released + // at every device Reset site. + IDirect3DSurface9 *mWaitOverlaySurface = NULL; unsigned int x_size; unsigned int y_size; SChainOf mRenderables;