Open the game window on a chosen monitor: the monitor setting and --monitor pick it by index, primary, or part of its device or monitor name, and --list-monitors prints the monitors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Thiago Cabral
2026-11-08 07:23:42 -03:00
committed by chrg6
parent bb0ac8caeb
commit d0c8588e3f
8 changed files with 313 additions and 28 deletions

View File

@@ -76,6 +76,8 @@ The game's *Swap Jump Controls* option trades B and X (jump on B, put away on X)
| F10 | L3 + R3 (both stick clicks) | performance overlay: off, frame rate, full panel |
| F11 | | borderless fullscreen on or off, saved to `nx2navi.ini` |
The window opens on the monitor `NX2NAVI_MONITOR` (`monitor` in `nx2navi.ini`) names, and F11 fullscreen covers the monitor the window is on. `nx2navi.exe --list-monitors` prints each monitor's index, device name, name, desktop rectangle, primary flag and DPI without game files; `nx2navi.exe --monitor <value>` sets `NX2NAVI_MONITOR` for one run and wins over the file. The value is `auto` (default), `primary`, an index from the list (counting from 1), or part of a device name (`display3`) or monitor name; when nothing matches, the log lists the monitors and the window is placed as with `auto`. Windows only.
The Minus+Plus combo never reaches the game. In the menu the arrow keys, the D-pad or the left stick move, Enter or A selects, Q and E or L and R switch tabs, Esc or B goes back (Switch button names, as in the table above).
## Audio output

View File

@@ -192,6 +192,12 @@
# here. Changes while the game runs take effect at once.
# window_size = 1920x1080
# fullscreen = 0
# Which monitor the game window opens on, centred in its work area; fullscreen covers that monitor. auto (default): the
# system places the window. primary; a number, counting from 1 in the order `nx2navi.exe --list-monitors` prints; or part
# of a GDI device name (display3) or of the monitor's name, ignoring case. For a virtual display that may not exist yet,
# use its name: when nothing matches, the log lists the monitors and the window is placed as with auto. The command line
# `--monitor <value>` overrides this for one run. Read at start; Windows only.
# monitor = auto
# 1, the default: the game keeps running while you are in another window; the keyboard stays quiet until the window is
# in front again. 0: it pauses when its window loses focus, as it does on the Switch when Home is pressed; a load
# interrupted that way can stop for good. Read at each focus change.

View File

@@ -47,7 +47,7 @@ static const char *const known[] = {
"HDR_EXPOSURE", "HDR_FLARE", "HDR_GAMMA", "HDR_GAME_NITS", "HDR_GRADE", "HDR_HIGHLIGHT_SAT", "HDR_HIGHLIGHTS", "HDR_HUE",
"HDR_PEAK_NITS", "HDR_SATURATION", "HDR_SHADOWS", "HDR_TONEMAP", "HDR_UI_NITS", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD",
"BIG_BUILD_FREE_MB", "BIG_BUILD_MAX_MB", "LIB_PREBUILD_BIG", "LIB_PREBUILD_BIG_BG", "LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LOCALE", "LOD_GUARD", "LSTICK_DEADZONE", "MEMORY", "MENU_LANG", "MENU_SOUND_VOLUME", "MENU_TEST",
"MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MOTION", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "MOVIE_HOST", "MOVIE_PACE",
"MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MONITOR", "MOTION", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "MOVIE_HOST", "MOVIE_PACE",
"MVOBJ_ALL", "MVOBJ_BANKS", "MVOBJ_EQUAL", "MVOBJ_EXACT", "MVOBJ_EXEC", "MVOBJ_NOC4", "MVOBJ_ONLY", "MVOBJ_TIMES", "MVPREV_CHECK", "MVPREV_GROW", "MVPREV_SKIP", "MVPREV_TRUST", "MV_CHECK",
"MV_SURVEY",
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PACE_HZ", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_RES_FIXED", "PATCH_SCENE", "PATCH_WIDE_ASPECT", "PATCH_WIDE_HUD", "PATCH_WIDE_MOVIE", "PATCH_WIDE_OUT", "PATCH_WIDE_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPELINE_CACHE_MAX_MB", "PIPE_BINARIES", "PIPE_BINARIES_MAX_MB", "PIPE_FEEDBACK", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT", "POWER_FULL", "TEX_SAME", "PRESENT_HELD", "HELD_SYNC",
@@ -74,7 +74,7 @@ static const struct { const char *key, *section; } sections[] = {
{"FPS_LIMIT", "frame rate"}, {"FPS60", "frame rate"}, {"RTSS", "frame rate"},
{"REALTIME", "frame rate"}, {"CLOTH_REALTIME", "frame rate"}, {"HIGHFPS_FIX", "frame rate"},
{"FULLSCREEN", "display"}, {"WINDOW_SIZE", "display"}, {"PERF_OVERLAY", "display"}, {"VK_PRESENT", "display"},
{"GPU_INDEX", "display"}, {"OUTPUT_RES", "display"}, {"GAME_ASPECT", "display"}, {"GAME_RES", "display"}, {"RENDER_SCALE", "display"}, {"UPSCALER", "display"},
{"GPU_INDEX", "display"}, {"MONITOR", "display"}, {"OUTPUT_RES", "display"}, {"GAME_ASPECT", "display"}, {"GAME_RES", "display"}, {"RENDER_SCALE", "display"}, {"UPSCALER", "display"},
{"SHARPNESS", "display"}, {"ASPECT", "display"}, {"FOV", "display"}, {"ANISO", "display"},
{"COLOR_PRESET", "display"},
{"COLOR_BRIGHTNESS", "display"}, {"COLOR_CONTRAST", "display"}, {"COLOR_SATURATION", "display"},
@@ -127,6 +127,7 @@ static const CfgMeta metas[] = {
// Screen
{"FULLSCREEN", "0", "0|1", LIVE}, {"WINDOW_SIZE", "1920x1080", NULL, LIVE},
{"VK_PRESENT", "mailbox", "mailbox|fifo|immediate", LIVE}, {"GPU_INDEX", "", NULL, RESTART},
{"MONITOR", "auto", NULL, RESTART}, // auto|primary|<1-based index>|<part of a device or monitor name> (gfx_window_win.c chosen_monitor)
{"PERF_OVERLAY", "off", "off|fps|full", LIVE},
{"OUTPUT_RES", "1920x1080", NULL, RESTART}, {"RENDER_SCALE", "game", "game|50|67|75|100|125|150|200", RESTART},
{"UPSCALER", "fsr", "fsr|bilinear", LIVE}, {"SHARPNESS", "80", "0..100", LIVE},

View File

@@ -153,6 +153,8 @@ int probe_cfg(void) {
"meta: prefix and case are accepted");
m = cfg_meta("RUMBLE_STRENGTH");
CHECK(m && EQ(m->def, "130") && EQ(m->values, "0..300"), "meta: rumble strength 130 in 0..300");
m = cfg_meta("MONITOR");
CHECK(m && EQ(m->def, "auto") && !m->values && !m->live, "meta: monitor is auto by default, free text, read at start");
m = cfg_meta("PERF_OVERLAY");
CHECK(m && EQ(m->def, "off") && EQ(m->values, "off|fps|full") && m->live, "meta: perf_overlay is live, off by default");
m = cfg_meta("AUDIO_DEVICE");

View File

@@ -265,6 +265,9 @@ static void open_window(void *arg) {
note_applied();
refresh_rate_update();
if (cfg_get("NX2NAVI_BACKGROUND_WINDOW")) rt_log("gfx: NX2NAVI_BACKGROUND_WINDOW has no effect with SDL windows");
const char *monitor = cfg_get("NX2NAVI_MONITOR"); // open_window runs once, so this is logged once
if (monitor && *monitor && strcasecmp(monitor, "auto"))
rt_log("gfx: NX2NAVI_MONITOR=%s has no effect with SDL windows: monitor selection is Win32-only (gfx_window_win.c)", monitor);
if (gfx_cfg_fullscreen()) SDL_SetWindowFullscreen(g_win, true);
SDL_SyncWindow(g_win);
input_sdl_window(g_win);
@@ -277,6 +280,12 @@ int wsys_open(void) {
return open_ok;
}
// --list-monitors (main.c) lists Win32 monitors only; see open_window for the setting.
int list_monitors(void) {
fprintf(stderr, "list-monitors: monitor selection is Win32-only; this build uses SDL windows\n");
return 1;
}
typedef struct { VkInstance inst; VkSurfaceKHR surface; } SurfaceReq;
static void make_surface(void *arg) {

View File

@@ -1,5 +1,5 @@
// The Win32 back end of the window (gfx_wsys.h): the window and its message loop on a thread of their own, F11
// fullscreen, the settings NX2NAVI_WINDOW_SIZE and NX2NAVI_FULLSCREEN, the monitor's refresh rate, focus, the Vulkan surface.
// fullscreen, the settings NX2NAVI_WINDOW_SIZE, NX2NAVI_FULLSCREEN and NX2NAVI_MONITOR, the monitor's refresh rate, focus, the Vulkan surface.
// The swapchain and the present are gfx_present.c's. The in-game menu's keyboard and mouse come from the window
// procedure (ui/menu_win.cpp), and so do the game's (input_win.c).
//
@@ -62,25 +62,33 @@ static int is_fullscreen(HWND h) { return !(GetWindowLongW(h, GWL_STYLE) & WS_OV
unsigned gfx_window_dpi(void) { return (unsigned)plat_cas32(&g_dpi, 0, 0); }
// The outer size of a framed window whose client area is cw x ch. The frame is measured at the window's DPI, or the
// system DPI before the window exists: plain AdjustWindowRect always takes the system DPI, which is wrong on a monitor
// at another display scale.
static void outer_size(HWND h, int cw, int ch, int *ow, int *oh) {
// The outer size of a framed window whose client area is cw x ch, with the frame measured at dpi.
static void outer_size_at(int cw, int ch, UINT dpi, int *ow, int *oh) {
RECT r = {0, 0, cw, ch};
AdjustWindowRectExForDpi(&r, WND_STYLE, FALSE, 0, h ? GetDpiForWindow(h) : GetDpiForSystem());
AdjustWindowRectExForDpi(&r, WND_STYLE, FALSE, 0, dpi);
*ow = r.right - r.left;
*oh = r.bottom - r.top;
}
// The client size cw x ch scaled down, in its own proportions, so the whole window fits the primary monitor's work
// area, where CW_USEDEFAULT opens it: the default 1920x1080 on a 1080p laptop put the bottom of the picture (the HUD)
// under the taskbar and past the screen. The setting itself stays; the menu's size list offers what fits.
static void fit_work_area(int *cw, int *ch) {
RECT work;
// The same at the window's DPI, or the system DPI before the window exists: plain AdjustWindowRect always takes the
// system DPI, which is wrong on a monitor at another display scale.
static void outer_size(HWND h, int cw, int ch, int *ow, int *oh) {
outer_size_at(cw, ch, h ? GetDpiForWindow(h) : GetDpiForSystem(), ow, oh);
}
// The client size cw x ch scaled down, in its own proportions, so the whole window fits a work area: the chosen
// monitor's (work, with the frame measured at dpi) or, with work NULL, the primary monitor's, where CW_USEDEFAULT opens
// it. The default 1920x1080 on a 1080p laptop put the bottom of the picture (the HUD) under the taskbar and past the
// screen. The setting itself stays; the menu's size list offers what fits.
static void fit_work_area(int *cw, int *ch, const RECT *work, UINT dpi) {
RECT prim;
int ow, oh;
if (!SystemParametersInfoW(SPI_GETWORKAREA, 0, &work, 0)) return;
outer_size(NULL, *cw, *ch, &ow, &oh);
int aw = (work.right - work.left) - (ow - *cw), ah = (work.bottom - work.top) - (oh - *ch);
if (!work) {
if (!SystemParametersInfoW(SPI_GETWORKAREA, 0, &prim, 0)) return;
work = &prim;
}
outer_size_at(*cw, *ch, dpi, &ow, &oh);
int aw = (work->right - work->left) - (ow - *cw), ah = (work->bottom - work->top) - (oh - *ch);
if (aw <= 0 || ah <= 0 || (*cw <= aw && *ch <= ah)) return;
double k = (double)aw / *cw < (double)ah / *ch ? (double)aw / *cw : (double)ah / *ch;
int w = (int)(*cw * k) & ~1, hh = (int)(*ch * k) & ~1;
@@ -222,10 +230,235 @@ static void refresh_rate_update(HWND h, int force) {
gfx_window_set_refresh(monitor_rate_hz(mon));
}
// Before the window exists (widescreen.c wide_init at start), the primary monitor: the one a fullscreen window opens on.
// ---- monitor choice (NX2NAVI_MONITOR, --monitor, --list-monitors)
// The monitors in EnumDisplayMonitors order, which is the index the setting counts in (1-based) and --list-monitors
// prints. The order follows the display driver's adapter and output order, so it holds while the same monitors stay
// attached and enabled, but a monitor that appears or goes away (a virtual display) can move the ones after it: the GDI
// device name or the monitor's name pins one monitor more firmly than its index.
#define MON_MAX 32
typedef struct {
HMONITOR h;
RECT rc, work; // the monitor's rectangle and its work area (without the taskbar), desktop pixels
int primary;
UINT dpi; // effective DPI of the monitor, 96 at 100% display scale
wchar_t device[CCHDEVICENAME]; // GDI device name, such as \\.\DISPLAY3
wchar_t name[64]; // the monitor's friendly name from the display configuration; empty when it has none
} MonInfo;
typedef struct { MonInfo m[MON_MAX]; int n; } MonList;
static void to_utf8(const wchar_t *w, char *out, size_t n) {
if (!WideCharToMultiByte(CP_UTF8, 0, w, -1, out, (int)n, NULL, NULL)) out[0] = 0;
}
// GetDpiForMonitor lives in shcore.dll, which the build does not link; 0 when it is missing.
static UINT monitor_dpi(HMONITOR mon) {
typedef HRESULT(WINAPI * GetDpiForMonitorFn)(HMONITOR, int, UINT *, UINT *);
HMODULE lib = LoadLibraryExW(L"shcore.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);
GetDpiForMonitorFn fn = lib ? (GetDpiForMonitorFn)(void *)GetProcAddress(lib, "GetDpiForMonitor") : NULL;
UINT x = 0, y = 0;
if (!fn || fn(mon, 0 /* MDT_EFFECTIVE_DPI */, &x, &y) != S_OK) x = 0;
return x;
}
static BOOL CALLBACK enum_monitor(HMONITOR h, HDC dc, LPRECT rc, LPARAM lp) {
(void)dc, (void)rc;
MonList *l = (MonList *)lp;
MONITORINFOEXW mi = {.cbSize = sizeof mi};
if (l->n >= MON_MAX) return FALSE;
if (!GetMonitorInfoW(h, (MONITORINFO *)&mi)) return TRUE;
MonInfo *m = &l->m[l->n++];
*m = (MonInfo){.h = h, .rc = mi.rcMonitor, .work = mi.rcWork, .primary = (mi.dwFlags & MONITORINFOF_PRIMARY) != 0,
.dpi = monitor_dpi(h)};
wcsncpy(m->device, mi.szDevice, CCHDEVICENAME - 1);
return TRUE;
}
// Fills MonInfo.name: an active display path ties a GDI device (the source) to the monitor attached to it (the target),
// and the target knows the monitor's name. Cloned monitors share a source, and the first path with a name wins.
static void fill_monitor_names(MonList *l) {
DISPLAYCONFIG_PATH_INFO *paths = NULL;
DISPLAYCONFIG_MODE_INFO *modes = NULL;
for (int attempt = 0; attempt < 3; attempt++) { // the topology can change between the size query and the query
UINT32 np = 0, nm = 0;
if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &np, &nm) != ERROR_SUCCESS || !np || !nm) break;
free(paths);
free(modes);
paths = calloc(np, sizeof *paths);
modes = calloc(nm, sizeof *modes);
if (!paths || !modes) break;
LONG rc = QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &np, paths, &nm, modes, NULL);
if (rc == ERROR_INSUFFICIENT_BUFFER) continue;
if (rc != ERROR_SUCCESS) break;
for (UINT32 i = 0; i < np; i++) {
DISPLAYCONFIG_SOURCE_DEVICE_NAME src = {.header = {.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME,
.size = sizeof src,
.adapterId = paths[i].sourceInfo.adapterId,
.id = paths[i].sourceInfo.id}};
DISPLAYCONFIG_TARGET_DEVICE_NAME tgt = {.header = {.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME,
.size = sizeof tgt,
.adapterId = paths[i].targetInfo.adapterId,
.id = paths[i].targetInfo.id}};
if (DisplayConfigGetDeviceInfo(&src.header) != ERROR_SUCCESS ||
DisplayConfigGetDeviceInfo(&tgt.header) != ERROR_SUCCESS)
continue;
for (int k = 0; k < l->n; k++)
if (!l->m[k].name[0] && !_wcsicmp(src.viewGdiDeviceName, l->m[k].device))
wcsncpy(l->m[k].name, tgt.monitorFriendlyDeviceName, (sizeof l->m[k].name / sizeof(wchar_t)) - 1);
}
break;
}
free(paths);
free(modes);
}
static int enum_monitors(MonList *l) {
*l = (MonList){0};
EnumDisplayMonitors(NULL, NULL, enum_monitor, (LPARAM)l);
fill_monitor_names(l);
return l->n;
}
// "2 \\.\DISPLAY3 "Name" 1920x1080 at 2560,0 primary dpi 144 (150%)": one monitor, for the log and --list-monitors.
static void monitor_line(const MonList *l, int i, char *out, size_t cap) {
const MonInfo *m = &l->m[i];
char dev[CCHDEVICENAME * 4], name[64 * 4];
to_utf8(m->device, dev, sizeof dev);
to_utf8(m->name, name, sizeof name);
snprintf(out, cap, "%d %s \"%s\" %ldx%ld at %ld,%ld%s dpi %u (%u%%)", i + 1, dev, name, m->rc.right - m->rc.left,
m->rc.bottom - m->rc.top, m->rc.left, m->rc.top, m->primary ? " primary" : "", m->dpi,
m->dpi * 100 / USER_DEFAULT_SCREEN_DPI);
}
static int monitor_unset(const char *v) { return !v || !*v || !_stricmp(v, "auto"); }
// Whether text contains needle_lower, which is already lower-cased; the comparison ignores case.
static int contains_nocase(const wchar_t *text, const wchar_t *needle_lower) {
wchar_t buf[CCHDEVICENAME + 64];
if (!*needle_lower) return 0;
wcsncpy(buf, text, (sizeof buf / sizeof *buf) - 1);
buf[(sizeof buf / sizeof *buf) - 1] = 0;
CharLowerBuffW(buf, (DWORD)wcslen(buf));
return wcsstr(buf, needle_lower) != NULL;
}
// Which monitor of the list a NX2NAVI_MONITOR value (not empty, not auto) names, as a 0-based index; -1 when none.
// Rules, in this order: "primary"; all digits, a 1-based index; any other text, a case-insensitive substring of a
// monitor's GDI device name or friendly name (the first monitor in list order when several match). why gets the
// rule that matched, or the reason nothing did.
static int resolve_monitor(const MonList *l, const char *want, char *why, size_t cap) {
if (!_stricmp(want, "primary")) {
for (int i = 0; i < l->n; i++)
if (l->m[i].primary) {
snprintf(why, cap, "the primary monitor");
return i;
}
snprintf(why, cap, "no monitor is marked primary");
return -1;
}
if (want[strspn(want, "0123456789")] == 0) {
long idx = strtol(want, NULL, 10);
if (idx >= 1 && idx <= l->n) {
snprintf(why, cap, "index %ld of %d", idx, l->n);
return (int)idx - 1;
}
snprintf(why, cap, "index %s is outside 1..%d", want, l->n);
return -1;
}
wchar_t needle[CCHDEVICENAME + 64];
if (!MultiByteToWideChar(CP_UTF8, 0, want, -1, needle, (int)(sizeof needle / sizeof *needle))) {
snprintf(why, cap, "the text is not valid UTF-8 or is too long");
return -1;
}
CharLowerBuffW(needle, (DWORD)wcslen(needle));
int first = -1, hits = 0;
for (int i = 0; i < l->n; i++)
if (contains_nocase(l->m[i].device, needle) || contains_nocase(l->m[i].name, needle)) {
if (first < 0) first = i;
hits++;
}
if (first < 0) {
snprintf(why, cap, "no device name or monitor name contains it");
return -1;
}
if (hits > 1) snprintf(why, cap, "device or monitor name contains it; the first of %d matches", hits);
else snprintf(why, cap, "device or monitor name contains it");
return first;
}
// The monitor NX2NAVI_MONITOR picks, copied to *out; 0 when it is unset or auto (the system places the window), or when
// it matches nothing now (the monitors present are logged, and the default placement applies). log: say what was decided
// and why. The window thread asks with log set; the size query below, which widescreen.c makes before the window
// exists, stays quiet.
static int chosen_monitor(MonInfo *out, int log) {
const char *want = cfg_get("NX2NAVI_MONITOR");
if (monitor_unset(want)) return 0;
MonList l;
char why[256], line[512];
int n = enum_monitors(&l), pick = -1;
if (n) pick = resolve_monitor(&l, want, why, sizeof why);
else snprintf(why, sizeof why, "no monitor was found");
if (pick < 0) {
if (log) {
rt_log("gfx: NX2NAVI_MONITOR=%s picks no monitor (%s); the window gets the default placement. Monitors present:",
want, why);
for (int i = 0; i < n; i++) {
monitor_line(&l, i, line, sizeof line);
rt_log("gfx: %s", line);
}
}
return 0;
}
if (log) {
monitor_line(&l, pick, line, sizeof line);
rt_log("gfx: NX2NAVI_MONITOR=%s picks monitor %s (%s)", want, line, why);
}
*out = l.m[pick];
return 1;
}
// --list-monitors (main.c): one line per monitor, then what the current NX2NAVI_MONITOR would pick. No game files.
int list_monitors(void) {
MonList l;
char line[512], why[256];
int n = enum_monitors(&l);
if (!n) {
fprintf(stderr, "list-monitors: no monitor found\n");
return 1;
}
printf("index device name desktop rectangle primary dpi\n");
for (int i = 0; i < n; i++) {
monitor_line(&l, i, line, sizeof line);
printf("%s\n", line);
}
const char *want = cfg_get("NX2NAVI_MONITOR");
if (monitor_unset(want)) {
printf("NX2NAVI_MONITOR is not set (auto): the system places the window\n");
} else {
int pick = resolve_monitor(&l, want, why, sizeof why);
if (pick >= 0) printf("NX2NAVI_MONITOR=%s picks monitor %d (%s)\n", want, pick + 1, why);
else printf("NX2NAVI_MONITOR=%s picks no monitor (%s)\n", want, why);
}
return 0;
}
// The top-left that centres an ow x oh window in work; a window larger than the work area keeps its corner at the
// work area's corner, so its title bar stays reachable.
static void center_in(const RECT *work, int ow, int oh, int *x, int *y) {
int ww = work->right - work->left, wh = work->bottom - work->top;
*x = work->left + (ww > ow ? (ww - ow) / 2 : 0);
*y = work->top + (wh > oh ? (wh - oh) / 2 : 0);
}
// Before the window exists (widescreen.c wide_init at start), the monitor a fullscreen window opens on: the one
// NX2NAVI_MONITOR picks (window_main places the window there), else the primary monitor.
int gfx_window_monitor_size(int *w, int *h) {
MONITORINFO mi = {sizeof mi};
HMONITOR mon = g_hwnd ? MonitorFromWindow(g_hwnd, MONITOR_DEFAULTTOPRIMARY) : MonitorFromPoint((POINT){0, 0}, MONITOR_DEFAULTTOPRIMARY);
MonInfo chosen;
HMONITOR mon = g_hwnd ? MonitorFromWindow(g_hwnd, MONITOR_DEFAULTTOPRIMARY)
: chosen_monitor(&chosen, 0) ? chosen.h
: MonitorFromPoint((POINT){0, 0}, MONITOR_DEFAULTTOPRIMARY);
if (!GetMonitorInfoW(mon, &mi)) return 0;
*w = mi.rcMonitor.right - mi.rcMonitor.left;
*h = mi.rcMonitor.bottom - mi.rcMonitor.top;
@@ -412,10 +645,19 @@ static void window_main(void *arg) {
GetSystemMetrics(SM_CYSMICON), LR_SHARED),
.hCursor = pointer_cursor(), .lpszClassName = L"NX2naviRecompWindow"};
RegisterClassExW(&wc);
int cw, ch, ow, oh;
int cw, ch, ow, oh, x = CW_USEDEFAULT, y = CW_USEDEFAULT;
cfg_window_size(&cw, &ch);
fit_work_area(&cw, &ch);
// NX2NAVI_MONITOR: the window is created centred in the chosen monitor's work area, with its frame measured at that
// monitor's DPI, so it opens there at the scale it will have. Without a choice the system places it. Either way
// the size is scaled down first if it does not fit the work area it opens in.
MonInfo mon;
int on_mon = chosen_monitor(&mon, 1);
fit_work_area(&cw, &ch, on_mon ? &mon.work : NULL, on_mon && mon.dpi ? mon.dpi : GetDpiForSystem());
outer_size(NULL, cw, ch, &ow, &oh);
if (on_mon) {
outer_size_at(cw, ch, mon.dpi ? mon.dpi : GetDpiForSystem(), &ow, &oh);
center_in(&mon.work, ow, oh, &x, &y);
}
// 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
@@ -424,8 +666,8 @@ static void window_main(void *arg) {
// for F11. If that fails, g_fs_pending retries at the first activation (wnd_proc, WM_ACTIVATEAPP).
// NX2NAVI_BACKGROUND_WINDOW=1 (test runs): no flashing, never fullscreen at start, and F11 keeps the window's place.
int background = g_background = cfg_get("NX2NAVI_BACKGROUND_WINDOW") && atoi(cfg_get("NX2NAVI_BACKGROUND_WINDOW"));
g_hwnd = CreateWindowExW(0, wc.lpszClassName, L"NX2navi", WND_STYLE, CW_USEDEFAULT, CW_USEDEFAULT, ow, oh, NULL,
NULL, inst, NULL);
g_hwnd = CreateWindowExW(0, wc.lpszClassName, L"NX2navi", WND_STYLE, x, y, ow, oh, NULL, NULL,
inst, NULL);
if (!g_hwnd) {
rt_log("gfx: CreateWindowEx failed (%lu)", GetLastError());
g_wnd_failed = 1;
@@ -435,11 +677,21 @@ static void window_main(void *arg) {
// Detaching the game's window leaves the software keyboard alone: its edit box sits in a window of its own
// (swkbd_win.c) and gets the IME it needs.
ImmAssociateContext(g_hwnd, NULL);
// The frame was measured at the system DPI; the window may have opened on a monitor at another scale.
// The frame was measured at the system DPI (or the chosen monitor's); the window may have opened on a monitor at
// another scale. When the size changes on a chosen monitor, the window is centred again in its work area;
// otherwise it keeps the place it was created at.
int nw, nh, nx = 0, ny = 0;
UINT flags = SWP_NOACTIVATE | SWP_SHOWWINDOW;
plat_xchg32(&g_dpi, (int32_t)GetDpiForWindow(g_hwnd));
outer_size(g_hwnd, cw, ch, &ow, &oh);
int fs = !background && cfg_fullscreen() && fullscreen_before_show(g_hwnd, ow, oh);
SetWindowPos(g_hwnd, HWND_BOTTOM, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW | (fs ? SWP_NOSIZE : 0));
outer_size(g_hwnd, cw, ch, &nw, &nh);
if (on_mon && (nw != ow || nh != oh)) center_in(&mon.work, nw, nh, &nx, &ny);
else flags |= SWP_NOMOVE;
// the fullscreen setting: the window turns fullscreen on the monitor it is on before it is shown, and keeps
// that place and size (fullscreen_before_show); the windowed size stays for F11
int fs = !background && cfg_fullscreen() && fullscreen_before_show(g_hwnd, nw, nh);
SetWindowPos(g_hwnd, HWND_BOTTOM, nx, ny, nw, nh, flags | (fs ? SWP_NOSIZE | SWP_NOMOVE : 0));
if (on_mon && MonitorFromWindow(g_hwnd, MONITOR_DEFAULTTONEAREST) != mon.h)
rt_log("gfx: the window is not on the chosen monitor; F11 fullscreen covers the monitor it is on");
if (!background) {
// Pending before the raise: the activation it gets is the first WM_ACTIVATEAPP, which takes the fullscreen
// setting; set after it, fullscreen waited for the next activation and the game started in a window. Only

View File

@@ -350,6 +350,7 @@ void rt_boot(void); // boot.c
int list_audio_devices(void); // audio_mixer.c: --list-audio-devices (SDL3 on every window back end)
int probe_cfg(void); // cfg_probe.c
int probe_dpi(void); // dpi_probe_win.c (Windows only)
int list_monitors(void); // --list-monitors: gfx_window_win.c, gfx_window_sdl.c
int probe_mods(void); // mods_probe.c
void discord_init(void); // discord_rpc.c: the Rich Presence thread, game runs only
int probe_discord(void); // discord_rpc.c

View File

@@ -1,11 +1,16 @@
// nx2navi.exe entry: reserves the guest address space, parses the command line and runs one mode.
//
// nx2navi.exe [--exefs DIR] [--romfs DIR] [--profile DIR] [--audio-device VALUE] <mode>
// nx2navi.exe [--exefs DIR] [--romfs DIR] [--profile DIR] [--audio-device VALUE] [--monitor VALUE] <mode>
// --audio-device VALUE play the sound on one output device: sets NX2NAVI_AUDIO_DEVICE before the settings are read, so
// it wins over nx2navi.ini's `audio_device =`. VALUE is default (the system default device),
// a 1-based index as --list-audio-devices prints it (devices sorted by name), or part of a device name, case-insensitive
// --list-audio-devices print each playback device's index, name, channels and frequency, mark the system default and
// show which device NX2NAVI_AUDIO_DEVICE picks; needs no game files
// --monitor VALUE open the game window on one monitor: sets NX2NAVI_MONITOR before the settings are read, so it wins
// over nx2navi.ini's `monitor =`. VALUE is auto (default), primary, a 1-based index as --list-monitors
// prints it, or part of a GDI device name (DISPLAY3) or of a monitor's name, case-insensitive
// --list-monitors print each monitor's index, GDI device name, name, desktop rectangle, primary flag and DPI;
// needs no game files
// --selftest layout, dispatch table and host-function registry; needs no game files
// --probe-mods the mods folder scan, order and switches (mods.c); needs no game files
// --probe-loader load and relocate the modules, check them against the measured 1.2.1 values
@@ -480,7 +485,7 @@ static int probe_crash(int null) {
}
static int usage(const char *argv0) {
fprintf(stderr, "usage: %s [--exefs DIR] [--romfs DIR] [--profile DIR] [--audio-device VALUE] [--list-audio-devices | --selftest | --probe-loader | "
fprintf(stderr, "usage: %s [--exefs DIR] [--romfs DIR] [--profile DIR] [--audio-device VALUE] [--monitor VALUE] [--list-audio-devices | --list-monitors | --selftest | --probe-loader | "
"--probe-cfg | --probe-dpi | --probe-mods | --probe-kernel | --probe-ipc | --probe-services | --probe-fs | --probe-gfx-abi | "
"--probe-nvn | --probe-nvn-tex | --probe-window | --probe-shader | --probe-vkmem | "
"--probe-vkexec | --probe-astc | --probe-movie | --probe-crash-call | --probe-crash-null | --probe-discord | "
@@ -565,6 +570,12 @@ int main(int argc, char **argv) {
// (plat_setenv) and hand the choice back to nx2navi.ini, so "default" is the spelling for the system default.
if (++i == argc || !*argv[i]) return usage(argv[0]);
plat_setenv("NX2NAVI_AUDIO_DEVICE", argv[i]);
} else if (!strcmp(argv[i], "--monitor")) {
// The settings read NX2NAVI_MONITOR from the environment first (cfg.c raw_get), and nothing has read it yet:
// the window opens in rt_boot. An empty value would remove the variable (plat_setenv) and hand the choice back
// to nx2navi.ini, so "auto" is the spelling for "no monitor choice" on the command line.
if (++i == argc || !*argv[i]) return usage(argv[0]);
plat_setenv("NX2NAVI_MONITOR", argv[i]);
} else if (!strcmp(argv[i], "--seconds")) {
char *end;
if (++i == argc) return usage(argv[0]);
@@ -591,6 +602,7 @@ int main(int argc, char **argv) {
}
if (!strcmp(mode, "--selftest")) return selftest();
if (!strcmp(mode, "--list-audio-devices")) return list_audio_devices();
if (!strcmp(mode, "--list-monitors")) return list_monitors();
if (!strcmp(mode, "--probe-loader")) return probe_loader();
if (!strcmp(mode, "--probe-crash-call")) return probe_crash(0);
if (!strcmp(mode, "--probe-crash-null")) return probe_crash(1);