Ignore the focus loss that the close's own hide of the window sends, so the exit summary keeps the session's frames.

The review of the window close change found that hiding the active window in gfx_window_win.c exit_start makes
Windows send WM_ACTIVATEAPP(FALSE), which wnd_proc passed on to gfx_window_focus_changed(0). In play, perf.c
perf_focus_lost then asked for the session's reports, and the present thread, which runs until rt_exit_steps reaches
prof_write, wrote "frametime: at focus loss" for the whole session at its next present, with the stutter map and the
memory ranking, and started a new period. "frametime: exit" then covered only the few frames since the hide, or was
missing when none came, so a normal close no longer left the exit summary the play-test's "Normal close" item reads.

wnd_proc now returns at a focus loss once g_exit_started is set. exit_start sets it on the window thread before
ShowWindow, and the message comes on the same thread inside that call, so the check has no race. A gained focus is
still passed on.

Not reproduced before the change (it needs a play run). tests/exit_order_check.py checks statically that
WM_ACTIVATEAPP returns before gfx_window_focus_changed for a loss once g_exit_started is set and that exit_start sets
the flag before the hide; the check fails on the code before this change and passes after it. The compile check of
gfx_window_win.c passes with no new -Wall warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 18:20:58 -05:00
parent b23acff301
commit a135c19450
2 changed files with 14 additions and 0 deletions

View File

@@ -589,6 +589,10 @@ static LRESULT CALLBACK wnd_proc(HWND h, UINT m, WPARAM w, LPARAM l) {
}
case WM_ACTIVATEAPP: {
int in = w != 0;
// The end's own hide (exit_start, which sets g_exit_started first on this thread) deactivates the window: no
// player's focus loss. Taken as one, the present thread's next present wrote the session's report for the whole
// run and started a new period (perf.c perf_focus_lost), so "frametime: exit" covered only the frames since.
if (!in && __atomic_load_n(&g_exit_started, __ATOMIC_ACQUIRE)) return 0;
gfx_window_focus_changed(in);
if (in && g_fs_pending) {
g_fs_pending = 0;

View File

@@ -172,6 +172,16 @@ def static_checks():
print(f'gfx_window_win.c: WM_CLOSE and WM_ENDSESSION start the end on its own thread and hide the window: '
f'{"ok" if ok else "FAIL"}', flush=True)
bad += not ok
# the hide's focus loss is the end's, not a player's: it must not reach perf.c perf_focus_lost, whose session report
# would take the exit summary's frames
act = proc[proc.index('case WM_ACTIVATEAPP:'):proc.index('case WM_ENDSESSION:')]
start = function(win, 'static PlatThread *exit_start(')
guard, focus = act.find('g_exit_started'), act.find('gfx_window_focus_changed(')
ok = (0 <= guard < focus and 'return 0;' in act[guard:focus] and '!in' in act[:focus]
and 0 <= start.find('g_exit_started') < start.find('ShowWindow(h, SW_HIDE)'))
print(f'gfx_window_win.c: a focus loss once the end has begun (the hide) does not reach gfx_window_focus_changed: '
f'{"ok" if ok else "FAIL"}', flush=True)
bad += not ok
steps = function(gp, 'void rt_exit_steps(unsigned what)')
pos = calls_in_order(steps, ['rtss_limit_restore', 'vk_pipeline_save_exit'])
ok = None not in pos and pos[0] < pos[1]