Change a setting with the mouse only on the two framed arrows beside its value, and switch HDR without a restart.

In the settings menu a click anywhere on a row stepped its setting to the next value, and the left arrow stood left of the value's text, so it moved whenever a value of another width came up: clicking the left arrow through a list landed on the row at the second click and stepped the other way. Each stepper now has its arrows in white frames at fixed places, the left one the width of the row's widest value from the right one, and the mouse changes the value on those frames only; a click on the rest of the row selects it. On and off rows have the same arrows. An arrow at the end of its values is drawn faint and does nothing, and a button held on an arrow repeats as a held key does. A, Enter and left and right work as before.

NX2NAVI_MENU_TEST_CLICK clicks rows by their setting's name during a menu test run. On an RX 7900 XT at 3232x1352 the arrows stepped the performance overlay one value a click and stopped at both ends, and clicks on the body of four rows changed nothing.

HDR between off and the tone mapper now follows the menu. The HDR path is set up at every start (vk_hdr_armed): the swapchain colour space and metadata extensions, the tonemap splice and the settings in the support buffer, with the spliced tonemap returning the game's own SDR result while the swapchain is SDR. A change of the setting makes the swapchain again in the other format. On the same card a run that started with HDR off went to an HDR10 swapchain and back, and one that started with HDR on went to SDR and back, with the picture right in each state. The game's native HDR still waits for a restart, as the game asks about the display once, and the row says so. NX2NAVI_HDR_LIVE = 0 leaves the HDR path out of a run that starts with HDR off.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
epicmeme546
2026-11-09 23:27:02 -05:00
parent 78b3bc5a80
commit 9ae7764ab5
12 changed files with 236 additions and 69 deletions

View File

@@ -124,6 +124,11 @@ keeps per-core data without locks and relies on that, so the host models it (`ru
holds the HDR picture relative to the HUD's white; the game's FSR 1 pass loses its output clamp; the present reads
the float picture in place of the window texture and `shaders/hdr_encode.comp` writes PQ for an HDR10 swapchain.
`NX2NAVI_HDR=native` reports an HDR10 display to the game, which then renders and presents its own PQ picture.
The menu switches between off and `1` during a run: the splice, the HDR extensions and the settings in the support
buffer are there from the start whatever is set (`vk_hdr_armed`), the spliced tonemap returns the game's SDR result
while the swapchain is SDR, and a change of the setting makes the swapchain again in the other format
(`gfx_present.c` `window_settings_live`). Only `native` waits for a restart, as the game asks about the display
once. `NX2NAVI_HDR_LIVE=0` leaves the HDR path out of a run that starts with HDR off.
- ASTC textures are uploaded as they are where the device samples ASTC, and decoded by a compute shader everywhere
else (`vk_astc.c`); under 20 GiB of video memory the decoder encodes them to BC3.
- Occlusion queries never block the CPU; a result that is not ready counts as visible.

View File

@@ -78,7 +78,7 @@ The game's *Swap Jump Controls* option trades B and X (jump on B, put away on X)
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, name (on Windows its device name too), 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 monitor name (on Windows also a device name such as `display3`; on Linux the name carries the connector, such as `dp-2`); when nothing matches, the log lists the monitors and the window is placed as with `auto`. On Wayland a window cannot place itself: only fullscreen (`fullscreen = 1`) opens on the chosen monitor, and a windowed window opens where the compositor puts it.
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).
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). With the mouse, a click on a row selects it and a setting changes on the two framed arrows beside its value (held, an arrow repeats); a click anywhere else on the row changes nothing.
## Audio output

View File

@@ -343,9 +343,12 @@
# HDR output: hdr = 1 sends the picture as HDR10 through the HDR tone mapper, hdr = native as the game renders it in
# its own HDR mode (both need HDR on in Windows, "Use HDR", or a Wayland compositor with colour management; without it
# the picture stays SDR, the game's own). hdr_peak_nits is your display's peak; hdr_game_nits the picture's white
# (paper white, 203 is the HDR reference) and hdr_ui_nits the HUD's and the menu's. hdr is read at start, the rest at
# once.
# (paper white, 203 is the HDR reference) and hdr_ui_nits the HUD's and the menu's. Everything applies at once, also
# hdr between 0 and 1; only hdr = native, which the game itself takes up, waits for the next start.
# hdr = 0
# hdr_live = 0 leaves the HDR path out of a run that starts with hdr = 0, as before hdr followed the menu: switching
# HDR on then takes a restart. Only worth setting if the picture differs with it at 1.
# hdr_live = 1
# hdr_peak_nits = 1000
# hdr_game_nits = 203
# hdr_ui_nits = 203

View File

@@ -45,9 +45,10 @@ static const char *const known[] = {
"FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLUSH_SPAN", "FLUSH_UPLOAD", "FOG_CULL", "FOLIAGE_SHADOW_CULL", "FOLIAGE_SHADOW_VRS", "FOV", "FOV_PATCH", "FG_HUDLESS_SHOW", "FPS60", "FPS_LIMIT", "FRAMEGEN", "FRAMEGEN_MULTIPLIER", "FREEZE_IDLE", "FREE_CORES",
"FULLSCREEN", "FXAA", "GAME_ASPECT", "GAME_RES", "GPA", "GPA_EMPTY", "GPA_TWIN", "GPA_TWIN_MAX_KB", "GPL", "GPL_DRAW_BUILD", "GPU", "GPU_INDEX", "GPU_PASSES", "GPU_NAME", "HDR", "HDR_BLOWOUT", "HDR_CONTRAST",
"HDR_EXPOSURE", "HDR_FLARE", "HDR_GAMMA", "HDR_GAME_NITS", "HDR_GRADE", "HDR_HIGHLIGHT_SAT", "HDR_HIGHLIGHTS", "HDR_HUE",
"HDR_LIVE",
"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", "MEM_CENSUS", "MENU_LANG", "MENU_SOUND_VOLUME", "MENU_TEST",
"MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MICROPHONE", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MONITOR", "MOTION", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "MOVIE_HOST", "MOVIE_PACE",
"MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_CLICK", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MICROPHONE", "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",
@@ -79,7 +80,7 @@ static const struct { const char *key, *section; } sections[] = {
{"SHARPNESS", "display"}, {"ASPECT", "display"}, {"FOV", "display"}, {"ANISO", "display"},
{"COLOR_PRESET", "display"},
{"COLOR_BRIGHTNESS", "display"}, {"COLOR_CONTRAST", "display"}, {"COLOR_SATURATION", "display"},
{"COLOR_GAMMA", "display"}, {"HDR", "display"}, {"HDR_PEAK_NITS", "display"}, {"HDR_GAME_NITS", "display"},
{"COLOR_GAMMA", "display"}, {"HDR", "display"}, {"HDR_LIVE", "display"}, {"HDR_PEAK_NITS", "display"}, {"HDR_GAME_NITS", "display"},
{"HDR_UI_NITS", "display"}, {"HDR_GAMMA", "display"}, {"HDR_TONEMAP", "display"}, {"HDR_EXPOSURE", "display"},
{"HDR_HIGHLIGHTS", "display"}, {"HDR_SHADOWS", "display"}, {"HDR_CONTRAST", "display"}, {"HDR_SATURATION", "display"},
{"HDR_HIGHLIGHT_SAT", "display"}, {"HDR_BLOWOUT", "display"}, {"HDR_FLARE", "display"}, {"HDR_HUE", "display"},
@@ -149,9 +150,12 @@ static const CfgMeta metas[] = {
{"COLOR_PRESET", "default", "default|vivid|clarity|custom", LIVE}, {"COLOR_BRIGHTNESS", "0", "-30..30", LIVE},
{"COLOR_CONTRAST", "100", "70..150", LIVE}, {"COLOR_SATURATION", "100", "50..200", LIVE},
{"COLOR_GAMMA", "1", "0.8..1.2", LIVE},
// HDR output (vk_hdr.c): on at start (the HDR tonemap is spliced into the game's as shaders load); the rest apply at
// once. The grading sliders: 50 leaves the picture as it is, exposure in percent.
{"HDR", "0", "0|1|native", RESTART}, {"HDR_PEAK_NITS", "1000", "400..10000", LIVE}, {"HDR_GAME_NITS", "203", "48..500", LIVE},
// HDR output (vk_hdr.c): off and the tone mapper apply at once (the HDR tonemap is spliced into the game's as
// shaders load whatever is set, vk_hdr_armed); native is the game's own and is asked for once, at start, which the
// menu's row says (vk_hdr_needs_restart). HDR_LIVE = 0 leaves the HDR path out of a run that starts with HDR off.
// The grading sliders: 50 leaves the picture as it is, exposure in percent.
{"HDR", "0", "0|1|native", LIVE}, {"HDR_LIVE", "1", "0|1", RESTART},
{"HDR_PEAK_NITS", "1000", "400..10000", LIVE}, {"HDR_GAME_NITS", "203", "48..500", LIVE},
{"HDR_UI_NITS", "203", "48..500", LIVE}, {"HDR_GAMMA", "2.2", "off|2.2|2.4", LIVE},
{"HDR_TONEMAP", "renodrt", "vanilla|renodrt", LIVE}, {"HDR_EXPOSURE", "100", "0..200", LIVE},
{"HDR_HIGHLIGHTS", "50", "0..100", LIVE}, {"HDR_SHADOWS", "50", "0..100", LIVE}, {"HDR_CONTRAST", "50", "0..100", LIVE},

View File

@@ -238,7 +238,8 @@ int gfx_window_focused(void) { return g_focused; }
unsigned gfx_window_focus_messages(void) { return (unsigned)g_focus_msgs; }
// Present path (g_pl held): a settings change (cfg_version) reaches the window thread, which applies a changed size or
// fullscreen; a changed NX2NAVI_VK_PRESENT rebuilds the swapchain at this present.
// fullscreen; a changed NX2NAVI_VK_PRESENT or NX2NAVI_HDR rebuilds the swapchain at this present.
static int g_sc_hdr_wanted; // vk_hdr_wanted when the swapchain was made (create_swapchain)
static void window_settings_live(void) {
static unsigned seen;
static int started;
@@ -253,6 +254,9 @@ static void window_settings_live(void) {
if (started) plat_xchg32(&g_dirty, 1);
}
if (started && vk_fg_wanted() != vk_fg_active()) plat_xchg32(&g_dirty, 1); // frame generation switched: its own swapchain
// HDR switched in the menu: a swapchain of the other format. Compared with what the swapchain was made for, not
// with what it got: on a display without HDR10 the setting stays on and the swapchain SDR, and that is settled.
if (started && vk_hdr_wanted() != g_sc_hdr_wanted) plat_xchg32(&g_dirty, 1);
ov_operation_mode_live();
if (started) gfx_window_apply_settings();
started = 1;
@@ -362,7 +366,8 @@ static int create_swapchain(void) {
if (!nf) return plat_xchg32(&g_dirty, 1), 0;
// UNORM: the blit writes the bytes the game wrote (no sRGB encode). NX2NAVI_HDR: HDR10 when the surface offers it.
VkSurfaceFormatKHR sf = fmts[0];
int hdr = vk_hdr_surface_format(fmts, nf, &sf), found = hdr;
g_sc_hdr_wanted = vk_hdr_wanted();
int hdr = g_sc_hdr_wanted && vk_hdr_surface_format(fmts, nf, &sf), found = hdr;
for (uint32_t i = 0; i < nf && !found; i++)
if (fmts[i].format == VK_FORMAT_B8G8R8A8_UNORM && fmts[i].colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { sf = fmts[i]; found = 1; }
for (uint32_t i = 0; i < nf && !found; i++)
@@ -428,6 +433,10 @@ static int create_swapchain(void) {
drop_swapchain_locked(c);
g_sc = made;
g_sc_fmt = sf.format;
static int hdr_was = -1; // the log says when a later swapchain is of the other kind (the menu's HDR row)
if (hdr_was >= 0 && hdr_was != hdr)
rt_log("gfx: the swapchain is now %s (format %d)", hdr ? "HDR10" : "SDR", (int)sf.format);
hdr_was = hdr;
g_sc_hdr = hdr;
vk_hdr_set_active(hdr);
if (hdr) vk_hdr_metadata(g_sc, 1);

View File

@@ -65,6 +65,7 @@ int wide_available(void);
const char *const *vk_color_preset(const char *name);
int vk_hdr_wanted(void);
int vk_hdr_active(void);
int vk_hdr_needs_restart(void);
int vk_dlaa_needs_restart(void);
double frame_step_limit_hz(void);
int frame_limit_follows_display(void);
@@ -500,6 +501,7 @@ extern "C" void menu_vk_release(void) {
// NX2NAVI_MENU_TEST=<frame>[:<frames per page>]: at that presented frame the menu opens on its first tab; each tab stays
// <frames per page> frames (default 30) and its last frame's swapchain image goes to NX2NAVI_MENU_TEST_DIR (default: the
// working directory) as menu-test-<frame>-<page>.png; after the last tab the menu closes.
// NX2NAVI_MENU_TEST_CLICK=<key>:<part>[,...]: mouse clicks on the rows of the page shown (ui/option_rows.cpp click_test).
// NX2NAVI_MENU_TEST_SET=<frame>:<key>=<value>[,<key>=<value>...][;<frame>:...]: cfg_set of each key (NX2NAVI_ prefix
// optional) at that presented frame; changes live settings with no input, menu open or not. nx2navi.ini is written only by
// the pseudo-key SAVE=1 (cfg_save, as a row change does; a failed write shows the menu's save note).
@@ -859,18 +861,20 @@ static void row_upscaling(void) {
if (e.kind >= DLSS) opt_apply("NX2NAVI_TAA", e.kind == FSR3 ? "fsr3" : "dlss");
}
// "in use: off" under the HDR value when the game started with HDR on but the swapchain is not HDR10 (the display or
// Windows' HDR switch is off, vk_hdr_surface_format). The row's "after restart" comes from the setting itself (cfg.c).
// Under the HDR value: "after restart" when what is set waits for one (the game's own HDR, to or from it:
// vk_hdr_needs_restart), "in use: off" when HDR is on but the swapchain is not HDR10 (the display or Windows' HDR
// switch is off, vk_hdr_surface_format).
static const char *hdr_note(void) {
if (vk_hdr_needs_restart()) return menu_text(UI_MENU_AFTER_RESTART);
if (!vk_hdr_wanted() || vk_hdr_active() || !*gfx_window_present_mode() || !cfg_bool("NX2NAVI_HDR")) return nullptr;
static char note[96];
snprintf(note, sizeof note, menu_text(UI_MENU_PRESENT_IN_USE), menu_text(UI_MENU_FRAMEGEN_OFF));
return note;
}
// HDR (vk_hdr.c), after a restart: off, the HDR tone mapper over the game's picture or the game's native HDR. The
// brightness rows follow while it is on; the tone mapper's own rows only with the first (native HDR is the game's
// picture as it is), its grading rows only with its own curve chosen.
// HDR (vk_hdr.c): off or the HDR tone mapper over the game's picture, switched at once, or the game's native HDR,
// which the game takes up at its start. The brightness rows follow while it is on; the tone mapper's own rows only
// with the first (native HDR is the game's picture as it is), its grading rows only with its own curve chosen.
static void row_hdr(void) {
static const char *const values[] = {"0", "1", "native"};
const char *names[] = {menu_text(UI_MENU_FRAMEGEN_OFF), menu_text(UI_MENU_HDR_TONEMAPPED), menu_text(UI_MENU_HDR_NATIVE)};

View File

@@ -130,13 +130,83 @@ void opt_end(void) {
struct RowView {
const char *value; // right side; "" for none
bool is_default; // the value is the setting's default: the "default" mark, the value not highlighted
bool arrows; // a stepper: < value >, the arrows click too
bool arrows; // a stepper: [<] value [>]; the mouse changes the value on the two framed arrows only
const char *note; // under the value instead of the "default" mark; NULL for none
float col; // the width of the widest value the row can show (value_width); 0: this value's
bool end_l, end_r; // the value is the first / last one: that arrow is drawn faint and does nothing
};
// One row: draws it, handles A / click and left / right while selected. Returns 2 (A / click), -1 / +1 (left / right),
// 3 / 4 (move up / down, movable rows only) or 0; always 0 for a row the environment fixes (greyed out, still selectable
// so its description shows). movable: the top / left face buttons, Ctrl+Up / Ctrl+Down and two drawn arrows move it.
// The width of the widest of n values at the current font size: a stepper's left arrow stands that far from the right
// one whatever value shows, so it stays under the pointer while it is clicked through the values.
static float value_width(const char *const *names, int n) {
ImFont *font = ImGui::GetFont();
float fs = ImGui::GetFontSize(), w = 0;
for (int i = 0; i < n; i++)
if (names[i]) w = fmaxf(w, font->CalcTextSizeA(fs, FLT_MAX, 0, names[i]).x);
return w;
}
// One framed arrow of a stepper over [a, b]: a white frame with the arrow in its middle, filled a little while the
// pointer is on it and more while it is pressed; faint when it does nothing (dim: the row is greyed out or the value
// is at this end).
static void arrow_box(ImDrawList *dl, ImVec2 a, ImVec2 b, bool left, bool hot, bool down, bool dim, float s) {
const int al = dim ? 70 : hot ? 255 : 215;
const ImU32 white = IM_COL32(255, 255, 255, al);
if (hot && !dim) dl->AddRectFilled(a, b, IM_COL32(255, 255, 255, down ? 96 : 44), 4 * s);
dl->AddRect(a, b, white, 4 * s, 0, fmaxf(1.0f, (hot && !dim ? 2.2f : 1.6f) * s));
float cx = (a.x + b.x) * 0.5f, cy = (a.y + b.y) * 0.5f, aw = (b.y - a.y) * 0.38f, ah = aw * 0.8f;
float tip = left ? -aw * 0.55f : aw * 0.55f, back = left ? aw * 0.45f : -aw * 0.45f;
dl->AddTriangleFilled(ImVec2(cx + tip, cy), ImVec2(cx + back, cy - ah), ImVec2(cx + back, cy + ah), white);
}
// ---- test hook. NX2NAVI_MENU_TEST_CLICK=<key>:<part>[,<key>:<part>...], with NX2NAVI_MENU_TEST opening the menu on the
// page that has the rows (menu.cpp): mouse clicks on steppers by their setting's name (without NX2NAVI_), part l (the
// left arrow), r (the right arrow) or b (the row's body, on its label). One click after another, each over a dozen
// frames: the pointer goes there, the button goes down and up, and the log says what the setting is then. So a run
// checks that the arrows step a setting and that an arrow at its end and a click on the body do not, with the fonts
// and sizes of the game.
static struct ClickTest { int parsed, n, at, frame; struct { char key[56]; char part; } c[24]; } g_click;
static void click_test(const char *key, ImVec2 la, ImVec2 lb, ImVec2 ra, ImVec2 rb, ImVec2 label) {
ClickTest &t = g_click;
if (!t.parsed) {
t.parsed = 1;
const char *s = cfg_get("NX2NAVI_MENU_TEST_CLICK");
while (s && *s && t.n < 24) {
size_t n = strcspn(s, ",");
const char *colon = (const char *)memchr(s, ':', n);
if (colon) {
snprintf(t.c[t.n].key, sizeof t.c[0].key, "NX2NAVI_%.*s", (int)(colon - s), s);
t.c[t.n++].part = colon[1];
}
s += n;
if (*s == ',') s++;
}
}
if (t.at >= t.n || strcmp(key, t.c[t.at].key)) return;
const char part = t.c[t.at].part;
const ImVec2 to = part == 'l' ? ImVec2((la.x + lb.x) * 0.5f, (la.y + lb.y) * 0.5f)
: part == 'r' ? ImVec2((ra.x + rb.x) * 0.5f, (ra.y + rb.y) * 0.5f)
: label;
ImGuiIO &io = ImGui::GetIO();
const int f = t.frame++;
if (f == 0) io.AddMousePosEvent(to.x, to.y);
else if (f == 4) io.AddMouseButtonEvent(ImGuiMouseButton_Left, true);
else if (f == 7) io.AddMouseButtonEvent(ImGuiMouseButton_Left, false);
else if (f == 12) {
rt_log("menu: test hook: click on %s of %s: now %s",
part == 'l' ? "the left arrow" : part == 'r' ? "the right arrow" : "the body", key, cfg_value(key));
t.at++, t.frame = 0;
}
}
// One row: draws it, handles A / click and left / right while selected. Returns 2 (A, or a click on a row without
// arrows), -1 / +1 (left / right, or the mouse on a stepper's framed arrow), 3 / 4 (move up / down, movable rows only)
// or 0; always 0 for a row the environment fixes (greyed out, still selectable so its description shows). movable: the
// top / left face buttons, Ctrl+Up / Ctrl+Down and two drawn arrows move it.
// A stepper takes the mouse on its two framed arrows only: a click anywhere else on the row selects it and changes
// nothing. A click on the row used to step to the next value, and the left arrow stood left of the value's text, so
// it moved when a value of another width came up and the next click at the same place landed on the row instead.
static int row_text(const char *key, const char *label, const char *desc, const RowView &v, bool movable) {
const CfgMeta *m = cfg_meta(key);
const char *disabled = g_disable_next ? g_disable_note : NULL;
@@ -212,19 +282,38 @@ static int row_text(const char *key, const char *label, const char *desc, const
vfs = fmaxf(fs * cap / tv, fs * 0.75f);
tv = fminf(font->CalcTextSizeA(vfs, FLT_MAX, 0, v.value).x, cap);
}
if (v.arrows && v.value[0]) { // < value >, drawn (the font has no arrow glyphs); a click on one steps
float aw = fs * 0.42f, gap = fs * 0.55f, cy = ty + fs * 0.52f;
float rx = right - aw, lx = rx - gap - tv - gap - aw;
ImU32 ac = off ? kGrey : hov || foc ? SKIN_TEXT : SKIN_TEXT_DIM;
dl->AddTriangleFilled(ImVec2(lx, cy), ImVec2(lx + aw, cy - aw * 0.75f), ImVec2(lx + aw, cy + aw * 0.75f), ac);
dl->AddTriangleFilled(ImVec2(rx + aw, cy), ImVec2(rx, cy - aw * 0.75f), ImVec2(rx, cy + aw * 0.75f), ac);
if (by_mouse && !off) {
float mx = ImGui::GetIO().MousePos.x;
if (mx >= lx - gap && mx <= lx + aw + gap) step = -1;
else if (mx >= rx - gap && mx <= rx + aw + gap) step = 1;
float value_mid = -1; // a stepper's value and its note are centred here, between the arrows
if (v.arrows) { // [<] value [>]: the arrows are drawn (the font has no arrow glyphs), each in a white frame
// The frames stand in fixed places: the right one at the row's right edge, the left one the width of the row's
// widest value before it. They fit over the note line, in the label line's height.
const float bw = floorf(fs * 1.3f), bh = floorf(fs * 1.02f), gap = floorf(fs * 0.4f), cy = floorf(ty + fs * 0.52f);
float col = fminf(fmaxf(v.col, tv), fmaxf(cap - 2 * (bw + gap), fs));
if (tv > col) { // a value wider than the room between the arrows: smaller, then clipped
vfs = fmaxf(fs * col / font->CalcTextSizeA(fs, FLT_MAX, 0, v.value).x, fs * 0.75f);
tv = fminf(font->CalcTextSizeA(vfs, FLT_MAX, 0, v.value).x, col);
}
right = rx - gap;
value_left = lx;
const ImVec2 ra(right - bw, cy - bh * 0.5f), rb(right, cy + bh * 0.5f);
const ImVec2 la(ra.x - gap - col - gap - bw, ra.y), lb(ra.x - gap - col - gap, rb.y);
if (!g_click.parsed || g_click.at < g_click.n) click_test(key, la, lb, ra, rb, ImVec2(p.x + w * 0.25f, p.y + h * 0.5f));
const ImVec2 mp = ImGui::GetIO().MousePos;
// the pointer on an arrow: its frame and two pixels around it, so a click on the frame's own line counts
const float slop = 2 * g_scale;
auto on = [&](ImVec2 a, ImVec2 b) {
return hov && mp.x >= a.x - slop && mp.x < b.x + slop && mp.y >= a.y - slop && mp.y < b.y + slop;
};
const bool hl = on(la, lb), hr = on(ra, rb), down = ImGui::IsMouseDown(ImGuiMouseButton_Left);
if (by_mouse) step = 0; // a click on the row itself only selects it
// The press steps, and a held button repeats as a held key does; after a second it also goes ten steps at a
// time over a long range (opt_slider), as held left / right do.
if (!off && ((hl && !v.end_l) || (hr && !v.end_r)) && ImGui::IsMouseClicked(ImGuiMouseButton_Left, true)) {
step = hl ? -1 : 1;
if (ImGui::GetIO().MouseDownDuration[ImGuiMouseButton_Left] > 1.0f) g_held = true;
}
arrow_box(dl, la, lb, true, hl, hl && down, off || v.end_l, g_scale);
arrow_box(dl, ra, rb, false, hr, hr && down, off || v.end_r, g_scale);
value_mid = (lb.x + ra.x) * 0.5f;
right = ra.x - gap;
value_left = la.x;
}
if (movable) { // up and down arrows left of the value; a click on one moves the row
// a fixed value column (On / Off differ in width) keeps the arrows of the rows in one line
@@ -241,8 +330,8 @@ static int row_text(const char *key, const char *label, const char *desc, const
value_left = cx - gap;
}
if (v.value[0]) {
float vx = right - tv;
ImVec4 vclip(vx, p.y, right, p.y + h);
float vx = value_mid >= 0 ? floorf(value_mid - tv * 0.5f) : right - tv;
ImVec4 vclip(vx, p.y, vx + tv, p.y + h);
dl->AddText(font, vfs, ImVec2(vx, ty + (fs - vfs) * 0.5f),
off ? kGrey : v.is_default ? IM_COL32(232, 222, 198, 255) : IM_COL32(255, 255, 255, 255), v.value, NULL,
0, &vclip);
@@ -252,10 +341,11 @@ static int row_text(const char *key, const char *label, const char *desc, const
if (v.note || v.is_default) { // under the value, right-aligned with it; its start shows when it is too long
const char *t = v.note ? v.note : menu_text(UI_MENU_DEFAULT_MARK);
float tw = fminf(font->CalcTextSizeA(small, FLT_MAX, 0, t).x, cap);
ImVec4 nclip(right - tw, p.y, right, p.y + h);
dl->AddText(font, small, ImVec2(right - tw, ny), v.note ? IM_COL32(250, 206, 130, 255) : SKIN_TEXT_DIM, t, NULL, 0,
&nclip);
if (tw > 0) note_right = right - tw - fs * 0.6f;
// a stepper's note is centred under its value, as far as the row's right edge lets it
float nx = value_mid >= 0 ? fminf(floorf(value_mid - tw * 0.5f), p.x + w - pad - tw) : right - tw;
ImVec4 nclip(nx, p.y, nx + tw, p.y + h);
dl->AddText(font, small, ImVec2(nx, ny), v.note ? IM_COL32(250, 206, 130, 255) : SKIN_TEXT_DIM, t, NULL, 0, &nclip);
if (tw > 0) note_right = nx - fs * 0.6f;
}
// the label and its note, clipped before the value
ImVec4 clip(p.x, p.y, value_left - fs * 0.6f, p.y + h);
@@ -311,11 +401,17 @@ int opt_save_tick(int force) {
// ---- rows
// An on / off row is a stepper too: either arrow, A, left or right flips it.
static float on_off_width(void) {
const char *const names[] = {menu_text(UI_MENU_ON), menu_text(UI_MENU_OFF)};
return value_width(names, 2);
}
bool opt_toggle(const char *key, UiTextId label, UiTextId desc, bool inverted) {
const char *d = cfg_default(key);
bool set = cfg_bool(key) != 0, def = d && *d && strcmp(d, "0") != 0;
bool on = set != inverted; // what the row shows
RowView v = {menu_text(on ? UI_MENU_ON : UI_MENU_OFF), set == def, false, NULL};
RowView v = {menu_text(on ? UI_MENU_ON : UI_MENU_OFF), set == def, true, NULL, on_off_width()};
if (!row(key, label, desc, v)) return false; // any step flips it
apply(key, set ? "0" : "1");
return true;
@@ -326,7 +422,7 @@ bool opt_toggle_value(const char *key, UiTextId label, UiTextId desc, const char
// when it is an on value, so turning the switch off and on again puts the profile's own value back
const char *d = cfg_default(key);
bool on = strcmp(cfg_value(key), off_value) != 0, def = d && strcmp(d, off_value) != 0;
RowView v = {menu_text(on ? UI_MENU_ON : UI_MENU_OFF), on == def, false, note};
RowView v = {menu_text(on ? UI_MENU_ON : UI_MENU_OFF), on == def, true, note, on_off_width()};
if (!row(key, label, desc, v)) return false; // any step flips it
apply(key, on ? off_value : def && *d ? d : on_value);
return true;
@@ -339,7 +435,8 @@ bool opt_button(const char *id, UiTextId label, UiTextId desc, const char *value
bool opt_toggle_text(const char *id, const char *label, const char *desc, bool on, const char *value) {
// off is the plain state: its value drawn dim, without the "default" mark
RowView v = {value ? value : menu_text(on ? UI_MENU_ON : UI_MENU_OFF), !on, false, on ? NULL : ""};
RowView v = {value ? value : menu_text(on ? UI_MENU_ON : UI_MENU_OFF), !on, true, on ? NULL : "",
value ? 0 : on_off_width()};
return row_text(id, label, desc, v, false) != 0; // any step flips it
}
@@ -391,7 +488,7 @@ bool opt_choice_list(const char *key, UiTextId label, const char *const *values,
int def = -1;
for (int i = 0; d && i < n; i++)
if (!strcmp(values[i], d)) def = i;
RowView v = {cur >= 0 ? names[cur] : now, cur == def, true, note};
RowView v = {cur >= 0 ? names[cur] : now, cur == def, true, note, value_width(names, n), cur == 0, cur == n - 1};
int step = row(key, label, desc, v);
if (!step || n <= 0) return false;
int nv;
@@ -405,7 +502,8 @@ bool opt_choice_list(const char *key, UiTextId label, const char *const *values,
int opt_choice_index(const char *key, UiTextId label, const char *const *names, int n, int cur, int def, UiTextId desc,
const char *note) {
RowView v = {cur >= 0 && cur < n ? names[cur] : "", cur == def, true, note};
RowView v = {cur >= 0 && cur < n ? names[cur] : "", cur == def, true, note, value_width(names, n), cur == 0,
cur == n - 1};
int step = row(key, label, desc, v);
if (!step || n <= 0) return -1;
int nv = cur < 0 ? (step < 0 ? n - 1 : 0) : step == 2 ? (cur + 1) % n : cur + step < 0 ? 0 : cur + step >= n ? n - 1 : cur + step;
@@ -452,9 +550,13 @@ bool opt_slider(const char *key, UiTextId label, float lo, float hi, UiTextId de
// follows the fallback's default, not 0, even once the key is set.
if (fallback && !(d && *d)) d = cfg_default(fallback);
double v = cfg_double(src), def = d && *d ? strtod(d, NULL) : lo;
char shown[48];
char shown[48], ends[2][48];
snprintf(shown, sizeof shown, fmt, v);
RowView rv = {shown, fabs(v - def) < step * 0.01, true, note};
snprintf(ends[0], sizeof ends[0], fmt, (double)lo);
snprintf(ends[1], sizeof ends[1], fmt, (double)hi);
const char *const widest[] = {shown, ends[0], ends[1]};
RowView rv = {shown, fabs(v - def) < step * 0.01, true, note, value_width(widest, 3), v <= lo + step * 0.01,
v >= hi - step * 0.01};
int s = row(key, label, desc, rv);
if (!s) return false;
int steps = g_held && (hi - lo) > 40 * step ? 10 : 1; // held: ten steps at a time over a long range

View File

@@ -1,7 +1,9 @@
// Settings pages in two panes (ui/option_rows.cpp): option rows on the left, the selected (controller) or hovered (mouse)
// row's description on the right. Each row shows a setting of the cfg table (cfg.h) and changes it through cfg_set: A or
// a click steps it (toggle, next value), left and right step it. A row marks the default value, says "after restart" when
// the setting is read at start, and is greyed out with a note when an environment variable fixes it.
// row's description on the right. Each row shows a setting of the cfg table (cfg.h) and changes it through cfg_set: A
// steps it (toggle, next value), left and right step it, and the mouse steps it on the two framed arrows beside its
// value and nowhere else on the row (a click on the row selects it; held on an arrow it repeats). A row marks the
// default value, says "after restart" when the setting is read at start, and is greyed out with a note when an
// environment variable fixes it.
//
// opt_begin("##screen", scale);
// opt_toggle("NX2NAVI_FULLSCREEN", UI_MENU_FULLSCREEN, UI_MENU_FULLSCREEN_DESC);

View File

@@ -173,11 +173,13 @@ void vk_color_release(void); // before the device goes away
// The brightness, contrast, saturation and gamma values (as the settings write them) of a colour profile, NULL for
// custom or an unknown name (vk_color.c).
const char *const *vk_color_preset(const char *name);
// HDR output (vk_hdr.c). vk_hdr_wanted: NX2NAVI_HDR, read once. vk_hdr_surface_format: the HDR10 surface format when wanted
// and offered (1). vk_hdr_set_active: gfx_present.c says whether the swapchain is HDR10 (vk_hdr_active). vk_hdr_encode:
// HDR output (vk_hdr.c). vk_hdr_armed: the HDR path is set up for this run (decided at the start). vk_hdr_wanted:
// NX2NAVI_HDR as the menu has it now. vk_hdr_surface_format: the HDR10 surface format when wanted and offered (1). vk_hdr_set_active: gfx_present.c says whether the swapchain is HDR10 (vk_hdr_active). vk_hdr_encode:
// like vk_color_record, src's crop as PQ code values (RGBA16F, crop {0, 0, w, h}). vk_hdr_ui_target / vk_hdr_ui_compose:
// the in-game menu's RGBA8 target, then blended onto the swapchain image. vk_hdr_to_srgb8: 10-bit PQ pixels to sRGB bytes.
int vk_hdr_armed(void);
int vk_hdr_wanted(void);
int vk_hdr_needs_restart(void); // the menu's HDR row: the value set takes effect at the next start
void vk_hdr_off(void); // probes that read back 8-bit swapchain pixels: SDR whatever the setting
int vk_hdr_surface_format(const VkSurfaceFormatKHR *f, uint32_t n, VkSurfaceFormatKHR *out);
void vk_hdr_metadata(VkSwapchainKHR sc, int new_sc);

View File

@@ -86,7 +86,8 @@ static int create_instance(void) {
// DLSS Frame Generation: Streamline's plugins call its naming functions even when they say it is off
if (validate || (vk_dlssg_wanted() && instance_ext_present(VK_EXT_DEBUG_UTILS_EXTENSION_NAME)))
ext[n_ext++] = VK_EXT_DEBUG_UTILS_EXTENSION_NAME;
if (vk_hdr_wanted() && instance_ext_present(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME)) // HDR10 surface formats
// armed, not wanted: the menu switches HDR on during the run (vk_hdr.c), and the extensions are fixed here
if (vk_hdr_armed() && instance_ext_present(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME)) // HDR10 surface formats
ext[n_ext++] = VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME;
vk_dlaa_instance_exts(ext, &n_ext, 12);
VkApplicationInfo app = {.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, .pApplicationName = "NX2navi",
@@ -324,7 +325,7 @@ static int create_device(void) {
// GPU timestamps on the CPU clock (vk_sync.c): the perf line splits GPU idle time into waiting for a submission or not.
if (has_ext(ep, ne, VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME)) { g_vk.has_calibrated = 2; ext[n_ext++] = VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME; }
else if (has_ext(ep, ne, VK_EXT_CALIBRATED_TIMESTAMPS_EXTENSION_NAME)) { g_vk.has_calibrated = 1; ext[n_ext++] = VK_EXT_CALIBRATED_TIMESTAMPS_EXTENSION_NAME; }
if (vk_hdr_wanted() && has_ext(ep, ne, VK_EXT_HDR_METADATA_EXTENSION_NAME)) { // the display's peak (vk_hdr_metadata)
if (vk_hdr_armed() && has_ext(ep, ne, VK_EXT_HDR_METADATA_EXTENSION_NAME)) { // the display's peak (vk_hdr_metadata)
g_vk.has_hdr_metadata = 1;
ext[n_ext++] = VK_EXT_HDR_METADATA_EXTENSION_NAME;
}

View File

@@ -2530,7 +2530,7 @@ static void support_fill(uint8_t *b, float w, float h, float point_size) {
memcpy(b + 1408, pcss_params(), 16); // pcss (translator patch 0010): soft sun shadows in the cascade compares
memcpy(b + 1424, shadow_spread_params(), 16); // shadow_spread (translator patch 0011): sun shadow filter width
for (int i = 0; i < 73; i++) memcpy(b + 192 + 16 * i, &one, 4); // render scale 1: the host renders at guest size
if (vk_hdr_wanted()) { // read by the game's tonemap only, once vk_hdr.c tonemap_splice has grown its block past the translator's
if (vk_hdr_armed()) { // read by the game's tonemap only, once vk_hdr.c tonemap_splice has grown its block past the translator's
float hdr[16];
vk_hdr_support(hdr);
memcpy(b + 1440, hdr, sizeof hdr);

View File

@@ -1,8 +1,10 @@
// HDR output (NX2NAVI_HDR=1): an HDR10 swapchain (Rec. 2020 primaries, PQ) and a game picture tone mapped to the
// display's range. Settings: NX2NAVI_HDR (read at start); the nits NX2NAVI_HDR_PEAK_NITS (the display's peak),
// display's range. Settings: NX2NAVI_HDR, off and the tone mapper following the menu during a run (vk_hdr_wanted), the
// game's own HDR (native) from the start only; the nits NX2NAVI_HDR_PEAK_NITS (the display's peak),
// NX2NAVI_HDR_GAME_NITS (the picture's white, "paper white") and NX2NAVI_HDR_UI_NITS (the HUD's and this menu's white);
// NX2NAVI_HDR_GAMMA, NX2NAVI_HDR_TONEMAP and the grading sliders NX2NAVI_HDR_EXPOSURE .. NX2NAVI_HDR_GRADE, all read
// when the settings change (vk_hdr_support). With NX2NAVI_HDR=0 nothing here runs and the present is what it was.
// when the settings change (vk_hdr_support). With NX2NAVI_HDR=0 the swapchain is SDR, the spliced tonemap gives the
// game's own SDR picture (vk_hdr_support, p0.w = -1) and the present is what it was.
//
// The game's picture (docs/architecture.md, "HDR"). The game tonemaps its scene into an r11g11b10f target, upscales it
// and draws the HUD into a second one (float, nothing in these passes clamps), and one last draw copies that into the
@@ -45,24 +47,56 @@
// ---- settings
static int g_wanted = -1;
// What the start decides for the whole run (hdr_start, first asked when the Vulkan instance is made):
// - armed: the HDR path is set up, so the menu can switch HDR on and off without a restart: the instance and device
// get the HDR extensions (vk_device.c), the game's tonemap is spliced as its shaders load (vk_hdr_shader_swap) and
// every draw's support buffer carries the settings (vk_exec_draw.c). While the swapchain is SDR the spliced tonemap
// returns the game's own SDR result. Always, unless NX2NAVI_HDR_LIVE = 0 with NX2NAVI_HDR = 0 (the path left out as it
// was before the setting followed the menu: HDR then needs a restart) or a probe turned it off (vk_hdr_off).
// - native: NX2NAVI_HDR = native, the game's own HDR. The display is reported as HDR10 (ov_gfx.c
// QueryDisplayHdrStatus, with the peak and paper white of the settings), and the game then renders its HDR picture
// itself and presents it as PQ-encoded Rec. 2020 in A2B10G10R10 (format 64), which goes to the HDR10 swapchain as it
// is: no tonemap splice, no float picture, no encode (pictures still presented in 8 bits, before the switch, are
// encoded as for 1). The game asks once, so a change to or from native waits for a restart (vk_hdr_needs_restart).
static int g_armed = -1, g_native = -1;
static void hdr_start(void) {
if (g_armed >= 0) return;
g_native = !strcmp(cfg_value("NX2NAVI_HDR"), "native");
g_armed = cfg_bool("NX2NAVI_HDR") || cfg_bool("NX2NAVI_HDR_LIVE"); // last: the mark that both are read
}
int vk_hdr_armed(void) {
hdr_start();
return g_armed;
}
int vk_hdr_native(void) { return vk_hdr_armed() && g_native; }
// HDR is on now: the setting as the menu has it, read again when the settings change. The swapchain follows at the
// next present (gfx_present.c window_settings_live makes it again) and vk_hdr_active then says what it got. A run that
// started with the game's own HDR keeps it: the game goes on presenting its PQ picture whatever the menu says.
int vk_hdr_wanted(void) {
if (g_wanted < 0) g_wanted = cfg_bool("NX2NAVI_HDR");
return g_wanted;
static unsigned seen = ~0u;
static int on;
if (!vk_hdr_armed()) return 0;
if (g_native) return 1;
unsigned v = cfg_version();
if (__atomic_load_n(&seen, __ATOMIC_ACQUIRE) != v) {
__atomic_store_n(&on, cfg_bool("NX2NAVI_HDR") != 0, __ATOMIC_RELEASE);
__atomic_store_n(&seen, v, __ATOMIC_RELEASE);
}
return __atomic_load_n(&on, __ATOMIC_ACQUIRE);
}
// NX2NAVI_HDR = native: the game's own HDR. The display is reported as HDR10 (ov_gfx.c QueryDisplayHdrStatus, with
// the peak and paper white of the settings), and the game then renders its HDR picture itself and presents it as
// PQ-encoded Rec. 2020 in A2B10G10R10 (format 64), which goes to the HDR10 swapchain as it is: no tonemap splice, no
// float picture, no encode (pictures still presented in 8 bits, before the switch, are encoded as for 1).
static int g_native = -1;
int vk_hdr_native(void) {
if (g_native < 0) g_native = !strcmp(cfg_value("NX2NAVI_HDR"), "native");
return g_native && vk_hdr_wanted();
// The menu's HDR row: what is set takes a restart. To or from the game's own HDR, and switching HDR on in a run that
// started without the HDR path (NX2NAVI_HDR_LIVE = 0).
int vk_hdr_needs_restart(void) {
hdr_start();
return (!strcmp(cfg_value("NX2NAVI_HDR"), "native")) != g_native || (!g_armed && cfg_bool("NX2NAVI_HDR"));
}
void vk_hdr_off(void) { g_wanted = 0; }
void vk_hdr_off(void) { g_native = 0, g_armed = 0; }
// The settings, read again when they change. The replay (every draw's support buffer) and the present thread read
// them: an update fills the snapshot not in use and then publishes it.
@@ -529,7 +563,8 @@ fail:
#undef PUT
void vk_hdr_shader_swap(uint32_t **spirv, uint64_t *words) {
if (!vk_hdr_wanted() || vk_hdr_native() || !*spirv || *words < 5) return;
// armed, not wanted: a shader loaded while HDR is off must be ready for the menu switching it on
if (!vk_hdr_armed() || vk_hdr_native() || !*spirv || *words < 5) return;
if (tonemap_splice(spirv, words)) {
static volatile int32_t spliced;
rt_log("hdr: the game's tonemap maps its picture onto the display's range (%d shaders so far)",
@@ -618,8 +653,8 @@ void vk_hdr_image_gone(VkImage image) { // a texture image went away: forget i
// ---- swapchain
// With NX2NAVI_HDR=1, the HDR10 format among the surface's (10-bit UNORM, ST 2084): 1 with *out set, else 0 and the
// reason logged once.
// While NX2NAVI_HDR is on (vk_hdr_wanted), the HDR10 format among the surface's (10-bit UNORM, ST 2084): 1 with *out
// set, else 0 and the reason logged once.
int vk_hdr_surface_format(const VkSurfaceFormatKHR *f, uint32_t n, VkSurfaceFormatKHR *out) {
if (!vk_hdr_wanted()) return 0;
static int logged;