Close the game's own console and bring its window to the front (#208, #521)

A game started from a frontend (Playnite, RetroBat, ES-DE) or a shortcut got a console beside it, and its window
opened behind other windows, out of reach without a mouse. Once the log file is open, the console is freed when no
other process shares it; a terminal launch keeps its output, and show_console = 1 keeps the console. The window is
raised at start, with AttachThreadInput when Windows refuses the foreground. rt_fatal writes only to the log, and a
console owned by the game closed with it anyway, so no error text is lost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Valiantsin DzerakhandClaude Opus 5.5 committed 2026-11-03 20:09:23 +03:00
1 parent 3422952e85
commit d2fbe0a2b1
7 files changed
+52 -7

No files matched your search

+3 -3
View File
@@ -51,7 +51,7 @@ static const char *const known[] = {
"REPLAY_CHECK", "RESOLVE_CHECK", "RESTART_WAIT", "ROMFS", "ROMFS_INDEX", "ROMFS_MMAP", "ROMFS_MODS", "ROMFS_PATCHES",
"ROMFS_PROOF_DIR", "ROMFS_VERIFY", "RSTICK_DEADZONE", "RUMBLE", "RUMBLE_CURVE", "RUMBLE_STRENGTH", "RUMBLE_TRIGGERS", "SHADER_CACHE",
"SHADER_MEMO", "SHADOW_RANGE", "SHADOW_RES", "SHADOW_SCALE", "SHARED_FONTS", "SHARPNESS", "SHOTS", "SHOTS_DIR",
"SHOTS_FULL", "SPEED_PROBE", "SPIN", "SPIN_HANDOFF_US", "SPLIT_DRAWS", "SPLIT_US", "STICK_DEADZONE", "STICK_SENS",
"SHOTS_FULL", "SHOW_CONSOLE", "SPEED_PROBE", "SPIN", "SPIN_HANDOFF_US", "SPLIT_DRAWS", "SPLIT_US", "STICK_DEADZONE", "STICK_SENS",
"SWKBD_ANSWER",
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "TXD_TRANSPOSE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
"VK_PRESENT", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VOLUME", "VRAM_HEADROOM", "VS_OUTS",
@@ -96,7 +96,7 @@ static const struct { const char *key, *section; } sections[] = {
{"PUSH_SKIP", "performance"}, {"SPLIT_DRAWS", "performance"}, {"SPLIT_US", "performance"}, {"VK_VALIDATION", "performance"},
{"VK_DYN_VERTEX", "performance"}, {"VK_DYN_A2C", "performance"}, {"VK_SUBGROUP_PIN", "performance"},
{"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"PASS_CRUMBS", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"},
{"WB_FENCE_LIMIT", "diagnostics"}, {"WB_EXACT", "diagnostics"},
{"WB_FENCE_LIMIT", "diagnostics"}, {"WB_EXACT", "diagnostics"}, {"SHOW_CONSOLE", "diagnostics"},
};
// The settings the menu shows: the value in effect when the key is unset, and what the key accepts. The only place a
@@ -166,7 +166,7 @@ static const CfgMeta metas[] = {
{"VK_DYN_A2C", "", "0|1", RESTART}, {"VK_SUBGROUP_PIN", "1", "0|1", RESTART},
// diagnostics
{"TRACE", "", NULL, RESTART}, {"FLIGHT", "0", "0|1", RESTART}, {"FLIGHT_SPIKE_MS", "100", "0..60000", RESTART},
{"WB_FENCE_LIMIT", "65536", "0..1073741824", RESTART}, {"WB_EXACT", "1", "0|1", RESTART},
{"SHOW_CONSOLE", "0", "0|1", RESTART}, {"WB_FENCE_LIMIT", "65536", "0..1073741824", RESTART}, {"WB_EXACT", "1", "0|1", RESTART},
{"PASS_CRUMBS", "", "0|1", RESTART}, // vk_crumbs.c pass marks; unset: VK_AMD_buffer_marker only
{"INPUT_RECORD", "", NULL, RESTART},
};
+23 -4
View File
@@ -279,6 +279,24 @@ static LRESULT CALLBACK wnd_proc(HWND h, UINT m, WPARAM w, LPARAM l) {
return DefWindowProcW(h, m, w, l);
}
// Brings the game window to the front. Windows refuses SetForegroundWindow to a process that was not the last to get
// input, which is the case for a game started by a frontend; attaching this thread's input queue to the foreground
// window's thread for the call is the documented way around it. The window is not left topmost.
static void raise_window(HWND h) {
HWND fg = GetForegroundWindow();
DWORD me = GetCurrentThreadId(), other = fg ? GetWindowThreadProcessId(fg, NULL) : 0;
int attached = other && other != me && AttachThreadInput(me, other, TRUE);
ShowWindow(h, IsIconic(h) ? SW_RESTORE : SW_SHOW);
BringWindowToTop(h);
SetForegroundWindow(h);
SetActiveWindow(h);
if (attached) AttachThreadInput(me, other, FALSE);
if (GetForegroundWindow() != h) { // still refused: a topmost toggle puts it above the others without keeping it there
SetWindowPos(h, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
SetWindowPos(h, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
}
}
static void window_main(void *arg) {
(void)arg;
HINSTANCE inst = GetModuleHandleW(NULL);
@@ -293,10 +311,10 @@ static void window_main(void *arg) {
int cw, ch, ow, oh;
cfg_window_size(&cw, &ch);
outer_size(NULL, cw, ch, &ow, &oh);
// The window opens behind the user's windows without taking focus: it shows the game's blank loading frames for a
// while, and must not cover what the player works on meanwhile. The game still counts itself in focus until the
// first WM_ACTIVATEAPP, so loading goes on, and keyboard input waits for the foreground (input_win.c). Its taskbar
// button flashes until it is brought up, and TOTK_FULLSCREEN waits for that first activation.
// The window opens behind the user's windows first and is then raised (raise_window): a game started by a frontend
// has no other way to reach it. The game counts itself in focus until the first WM_ACTIVATEAPP, so loading goes on,
// and keyboard input waits for the foreground (input_win.c). If Windows still refuses the foreground, the taskbar
// button flashes, and TOTK_FULLSCREEN waits for the first activation.
// TOTK_BACKGROUND_WINDOW=1 (test runs): no flashing, never fullscreen at start, and F11 keeps the window's place.
int background = g_background = cfg_get("TOTK_BACKGROUND_WINDOW") && atoi(cfg_get("TOTK_BACKGROUND_WINDOW"));
g_hwnd = CreateWindowExW(0, wc.lpszClassName, L"TotK Recomp", WND_STYLE, CW_USEDEFAULT, CW_USEDEFAULT, ow, oh, NULL,
@@ -315,6 +333,7 @@ static void window_main(void *arg) {
outer_size(g_hwnd, cw, ch, &ow, &oh);
SetWindowPos(g_hwnd, HWND_BOTTOM, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
if (!background) {
raise_window(g_hwnd); // a frontend's player may have no way to reach a window left behind
FLASHWINFO fw = {.cbSize = sizeof fw, .hwnd = g_hwnd, .dwFlags = FLASHW_TRAY | FLASHW_TIMERNOFG};
FlashWindowEx(&fw);
g_fs_pending = cfg_fullscreen();
+4
View File
@@ -546,6 +546,10 @@ int main(int argc, char **argv) {
if (cfg.seconds && mode) return usage(argv[0]);
log_open(!mode);
if (!mode) {
// A game launched by a frontend or a shortcut gets a console of its own that only gets in the way. The log is in
// a file by now; with no log file, or with show_console = 1, the console stays.
if (rt_log_path() && !cfg_bool("TOTK_SHOW_CONSOLE") && plat_console_release_if_owned())
rt_log("console: owned by the game alone, released (show_console = 1 keeps it)");
cfg.boot = cfg.seconds > 0; // --seconds starts the game like --boot, with its checkpoints
rt_boot();
return 0;
+2
View File
@@ -1262,6 +1262,8 @@ int plat_os_version(char *out, size_t cap) {
int plat_is_wine(void) { return 0; }
int plat_console_release_if_owned(void) { return 0; }
int plat_stderr_path(char *out, size_t cap) {
struct stat st;
if (fstat(2, &st) || !S_ISREG(st.st_mode)) return 0;
+5
View File
@@ -378,6 +378,11 @@ int plat_is_wine(void); // 1 when the Windows
// The file the process's stderr (the log) goes to, as a UTF-8 path; 0 when stderr is not a file (a console, a pipe).
int plat_stderr_path(char *out, size_t cap);
// Detaches the process from its console when no other process shares it (a frontend or a shortcut started us and
// Windows made the console for us); a terminal the user started us from keeps its output. Returns 1 when detached.
// Linux: 0.
int plat_console_release_if_owned(void);
// ---- desktop: the in-game menu's "Copy system info" and "Open log folder"
int plat_clipboard_set(const char *utf8); // replaces the clipboard's text; 1 on success
int plat_open_folder(const char *path); // shows the folder in the file manager; 1 on success
+11
View File
@@ -1303,6 +1303,17 @@ int plat_stderr_path(char *out, size_t cap) {
return WideCharToMultiByte(CP_UTF8, 0, p, -1, out, (int)cap, NULL, NULL) != 0;
}
// GetConsoleProcessList counts this process alone only when the console was created for it. The window is hidden first
// and then the console freed: under Windows Terminal as the default terminal, freeing is what closes the terminal
// window that opened beside the game.
int plat_console_release_if_owned(void) {
DWORD pids[2];
if (!GetConsoleWindow() || GetConsoleProcessList(pids, 2) != 1) return 0;
ShowWindow(GetConsoleWindow(), SW_HIDE);
FreeConsole();
return 1;
}
// ---- desktop
int plat_clipboard_set(const char *utf8) {
+4
View File
@@ -325,6 +325,10 @@
# cheat_0123abcd = 1
[diagnostics]
# Keep the console window of a game started from a frontend (Playnite, RetroBat, ES-DE) or a shortcut. Default 0: the
# console that Windows opened for the game alone is closed once the log is open. A game started from a terminal keeps
# printing there either way; the log is logs\totk.log.
# show_console = 0
# Extra lines in the log, comma separated: svc, host, ipc, fs, gfx, perf, sched, audio, present or all. The menu's
# Performance log sets perf (frame timings). Default: none.
# trace = perf