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:
co-authored by
Claude Opus 4.8
parent
2cccf877f5
commit
c421ec4af8
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user