diff --git a/docs/architecture.md b/docs/architecture.md index fa4f2aad..810973dd 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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. diff --git a/docs/controls.md b/docs/controls.md index 7c139b2b..add8c150 100644 --- a/docs/controls.md +++ b/docs/controls.md @@ -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 ` 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 diff --git a/nx2navi.ini.example b/nx2navi.ini.example index 38949930..1723ae53 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -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 diff --git a/runtime/cfg.c b/runtime/cfg.c index d766cf3e..7a2a59b4 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -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}, diff --git a/runtime/gfx_present.c b/runtime/gfx_present.c index cb2a9def..3a8d1592 100644 --- a/runtime/gfx_present.c +++ b/runtime/gfx_present.c @@ -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); diff --git a/runtime/ui/menu.cpp b/runtime/ui/menu.cpp index 20f15acd..0d38beeb 100644 --- a/runtime/ui/menu.cpp +++ b/runtime/ui/menu.cpp @@ -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=[:]: at that presented frame the menu opens on its first tab; each tab stays // frames (default 30) and its last frame's swapchain image goes to NX2NAVI_MENU_TEST_DIR (default: the // working directory) as menu-test--.png; after the last tab the menu closes. +// NX2NAVI_MENU_TEST_CLICK=:[,...]: mouse clicks on the rows of the page shown (ui/option_rows.cpp click_test). // NX2NAVI_MENU_TEST_SET=:=[,=...][;:...]: 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)}; diff --git a/runtime/ui/option_rows.cpp b/runtime/ui/option_rows.cpp index 9a7909a7..21937619 100644 --- a/runtime/ui/option_rows.cpp +++ b/runtime/ui/option_rows.cpp @@ -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=:[,:...], 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 diff --git a/runtime/ui/option_rows.h b/runtime/ui/option_rows.h index 7ce6fcce..e865b80b 100644 --- a/runtime/ui/option_rows.h +++ b/runtime/ui/option_rows.h @@ -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); diff --git a/runtime/vk.h b/runtime/vk.h index 4ca82456..3e067c79 100644 --- a/runtime/vk.h +++ b/runtime/vk.h @@ -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); diff --git a/runtime/vk_device.c b/runtime/vk_device.c index d3482d3f..7b635f4f 100644 --- a/runtime/vk_device.c +++ b/runtime/vk_device.c @@ -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; } diff --git a/runtime/vk_exec_draw.c b/runtime/vk_exec_draw.c index 6d00dccc..23fbc3ed 100644 --- a/runtime/vk_exec_draw.c +++ b/runtime/vk_exec_draw.c @@ -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); diff --git a/runtime/vk_hdr.c b/runtime/vk_hdr.c index 836584bd..1b0d7962 100644 --- a/runtime/vk_hdr.c +++ b/runtime/vk_hdr.c @@ -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;