mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user