Wait screen: paint into the backbuffer, not after Present (load flicker)

The pre-mission overlay painted the window DC AFTER ExecuteIdle's
Present -- every cycle flashed the black D3D frame before the GDI
redraw, visible as flicker while the game loads (user report).  The
overlay now lands IN the presented frame: the backbuffer refuses GetDC
on this driver (D3DERR_INVALIDCALL without LOCKABLE_BACKBUFFER, not
worth forcing game-wide), so the text+spinner GDI-paint goes into a
cached offscreen plain surface (GetDC always legal there) and
StretchRects onto the backbuffer before Present.  D3DPOOL_DEFAULT cache
released at every device-Reset site; window-DC path remains for the
pre-device seat wait and as the fallback; one-shot [waitscreen] boot log
says which path is live.

Verified: 14 rapid PrintWindow captures of the waiting state all carry
the text (pre-fix runs alternated black frames).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-22 16:33:41 -05:00
co-authored by Claude Opus 4.8
parent 2cccf877f5
commit c421ec4af8
3 changed files with 116 additions and 15 deletions
+15
View File
@@ -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],
+97 -15
View File
@@ -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;
+4
View File
@@ -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<HierarchicalDrawComponent *> mRenderables;