diff --git a/runtime/cfg.c b/runtime/cfg.c index d69e1263..cdf5f37d 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -130,7 +130,7 @@ static const CfgMeta metas[] = { {"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 tonemap shader is swapped as shaders load); the nits apply at once - {"HDR", "0", "0|1", RESTART}, {"HDR_PEAK_NITS", "1000", "400..10000", LIVE}, {"HDR_GAME_NITS", "203", "80..500", LIVE}, + {"HDR", "0", "0|1|native", RESTART}, {"HDR_PEAK_NITS", "1000", "400..10000", LIVE}, {"HDR_GAME_NITS", "203", "80..500", LIVE}, {"HDR_UI_NITS", "203", "80..500", LIVE}, {"HDR_HUE", "80", "0..100", LIVE}, {"DEVICE_PROFILE", "auto", "auto|desktop|deck", RESTART}, {"SHADOW_RES", "game", "game|256|512|768|1024|1536|2048", RESTART}, diff --git a/runtime/gfx.h b/runtime/gfx.h index 7c7e1657..805fb83a 100644 --- a/runtime/gfx.h +++ b/runtime/gfx.h @@ -23,6 +23,7 @@ uint64_t gfx_vsync_ticks(void); // ticks since start uint64_t gfx_vsync_time_ns(uint64_t tick); // kern_now_ns() time of a tick (pacing) double gfx_window_refresh_hz(void); // refresh rate of the window's monitor; 0 unknown int gfx_window_focused(void); +int vk_hdr_native(void); // NX2NAVI_HDR = native: the game's own HDR picture (vk_hdr.c) int gfx_picture_rect(float out[4]); // where the last present put the picture, fractions of the swapchain (x, y, w, h); 0 before one // ---- shader service client (gfx_shader.c) diff --git a/runtime/gfx_present.c b/runtime/gfx_present.c index 538d7815..77262433 100644 --- a/runtime/gfx_present.c +++ b/runtime/gfx_present.c @@ -835,6 +835,10 @@ static VkImage hudless_record(VkCommandBuffer cb, unsigned slot, int w, int h, c static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w, int h, const int crop[4], int defer) { int general = layout == VK_IMAGE_LAYOUT_GENERAL; + { // the presented image's format, when it changes (an HDR picture comes in another one) + static VkFormat last = VK_FORMAT_UNDEFINED; + if (src && fmt != last) last = fmt, rt_log("gfx: the game presents %dx%d images of format %d", w, h, (int)fmt); + } gfx_window_start(); VkCtx *c = vk_ctx(); plat_rmutex_lock(&g_pl); diff --git a/runtime/guest_setjmp.c b/runtime/guest_setjmp.c index 0ef65a75..06c42a1b 100644 --- a/runtime/guest_setjmp.c +++ b/runtime/guest_setjmp.c @@ -145,6 +145,16 @@ static void ov_longjmp(Cpu *c) { char at[160]; rt_log("longjmp to buffer %#llx from %s: back into the guest function that called setjmp", (unsigned long long)env, ldr_describe(c->x[30], at, sizeof at)); + // the guest frames that led here (the frame-pointer chain), to name what failed + // (from the setjmp's caller: its return address, then the frame-pointer chain of x29 as setjmp saw it) + rt_log(" setjmp called from %s", ldr_describe(e.x[11], at, sizeof at)); + uint64_t fp = e.x[10]; + for (int k = 0; k < 10 && fp && mem_readable(fp, 16); k++) { + const uint64_t *f = (const uint64_t *)(uintptr_t)fp; + rt_log(" caller %d: %s", k, ldr_describe(f[1], at, sizeof at)); + if (f[0] <= fp) break; + fp = f[0]; + } } for (int i = 0; i < 12; i++) c->x[19 + i] = e.x[i]; for (int i = 0; i < 8; i++) c->v[8 + i] = e.d[i]; diff --git a/runtime/ov_gfx.c b/runtime/ov_gfx.c index 9807a15a..ecb66633 100644 --- a/runtime/ov_gfx.c +++ b/runtime/ov_gfx.c @@ -125,20 +125,33 @@ __attribute__((constructor)) static void ov_nvn_procs_register(void) { nvn_procs // through them (main+0x4dc1e0 hit a null virtual in ClearLayerExtendedSwapInterval). A desktop window is SDR, // fixed 60 Hz, without VRR; HDR metadata and swap-interval changes are accepted and dropped. static void vi_ok(Cpu *c) { ret_x(c, 0); } +static void vi_hdr_meta(Cpu *c) { // the game's HDR content metadata (native HDR): the swapchain's comes from the settings + static int n; + if (!n++) rt_log("vi: the game sets HDR content metadata (luminance %llu..%llu)", (unsigned long long)c->x[1], + (unsigned long long)c->x[2]); + ret_x(c, 0); +} // Result QueryDisplayHdrStatus(bool *enabled, HdrOutputFormat *format, uint16_t *peak, const Display *) static void ov_vi_hdr_status(Cpu *c) { uint8_t *on = arg_p(c, 0); uint32_t *format = arg_p(c, 1); uint16_t *peak = arg_p(c, 2); - if (on) *on = 0; - if (format) *format = 0; - if (peak) *peak = 0; + // Native HDR (vk_hdr.c vk_hdr_native): an HDR10 display (format 1, which makes the game present PQ in 10 bits) of + // the settings' peak; otherwise SDR, as a desktop window is + int hdr = vk_hdr_native(); + int nits = cfg_int("NX2NAVI_HDR_PEAK_NITS"); + if (on) *on = (uint8_t)hdr; + if (format) *format = hdr ? 1 : 0; + if (peak) *peak = hdr ? (uint16_t)(nits > 0 && nits < 65535 ? nits : 1000) : 0; + static int logged; + if (!logged++) rt_log("vi: QueryDisplayHdrStatus -> %s", hdr ? "HDR10 (the game's native HDR)" : "SDR"); ret_x(c, 0); } // Result QueryDisplayHdrPaperWhite(uint16_t *nits, const Display *): the SDR reference white static void ov_vi_paper_white(Cpu *c) { uint16_t *nits = arg_p(c, 0); - if (nits) *nits = 203; + int w = cfg_int("NX2NAVI_HDR_GAME_NITS"); // native HDR: the paper white of the settings + if (nits) *nits = (uint16_t)(vk_hdr_native() && w > 0 && w < 10000 ? w : 203); ret_x(c, 0); } // Result QueryDisplayCurrentRefreshRate(RefreshRate *rate, bool *variable, const Display *): RefreshRate is taken @@ -166,10 +179,10 @@ HOST_OVERRIDE("_ZN2nn2vi21QueryDisplayHdrStatusEPbPNS0_15HdrOutputFormatEPtPKNS0 HOST_OVERRIDE("_ZN2nn2vi25QueryDisplayHdrPaperWhiteEPtPKNS0_7DisplayE", ov_vi_paper_white); HOST_OVERRIDE("_ZN2nn2vi30QueryDisplayCurrentRefreshRateEPNS0_11RefreshRateEPbPKNS0_7DisplayE", ov_vi_refresh_rate); HOST_OVERRIDE("_ZN2nn2vi24QueryDisplayVrrSupportedEPbPKNS0_7DisplayE", ov_vi_vrr_supported); -HOST_OVERRIDE("_ZN2nn2vi52SetLayerNextHdrStandardContentLuminanceRangeMetadataEPNS0_5LayerEtt", vi_ok); -HOST_OVERRIDE("_ZN2nn2vi52SetLayerNextHdrExtendedContentLuminanceRangeMetadataEPNS0_5LayerEtt", vi_ok); -HOST_OVERRIDE("_ZN2nn2vi35SetLayerNextHdrContentGamutMetadataEPNS0_5LayerENS0_15HdrContentGamutE", vi_ok); -HOST_OVERRIDE("_ZN2nn2vi39SetLayerNextHdrToSdrToneMappingMetadataEPNS0_5LayerENS0_14HdrToneMappingE", vi_ok); +HOST_OVERRIDE("_ZN2nn2vi52SetLayerNextHdrStandardContentLuminanceRangeMetadataEPNS0_5LayerEtt", vi_hdr_meta); +HOST_OVERRIDE("_ZN2nn2vi52SetLayerNextHdrExtendedContentLuminanceRangeMetadataEPNS0_5LayerEtt", vi_hdr_meta); +HOST_OVERRIDE("_ZN2nn2vi35SetLayerNextHdrContentGamutMetadataEPNS0_5LayerENS0_15HdrContentGamutE", vi_hdr_meta); +HOST_OVERRIDE("_ZN2nn2vi39SetLayerNextHdrToSdrToneMappingMetadataEPNS0_5LayerENS0_14HdrToneMappingE", vi_hdr_meta); HOST_OVERRIDE("_ZN2nn2vi28SetLayerExtendedSwapIntervalEPNS0_5LayerEii", vi_ok); HOST_OVERRIDE("_ZN2nn2vi30ClearLayerExtendedSwapIntervalEPNS0_5LayerE", vi_ok); HOST_OVERRIDE("_ZN2nn2vi36SetDisplay120HzRefreshRateCapabilityEPNS0_7DisplayEb", vi_ok); diff --git a/runtime/ov_movie2.c b/runtime/ov_movie2.c index a18bd665..ea57ec46 100644 --- a/runtime/ov_movie2.c +++ b/runtime/ov_movie2.c @@ -36,6 +36,7 @@ #include "cfg.h" #include "gfx.h" #include "host.h" +#include "nvn_api.h" #include "platform/platform.h" #define R_NO_FRAME 0x7bu @@ -586,6 +587,10 @@ static void ov_acquire(Cpu *c) { uint64_t ysz = (uint64_t)p.w * p.h, uvsz = ysz / 2; if (p.nv12 && buf && cap >= ysz + uvsz) { memcpy(buf, p.nv12, (size_t)(ysz + uvsz)); + // The game's YUV pass reads this buffer on the GPU. A host write into pool storage is one the page write + // tracking may not see in time (a mirror or texture over it kept its older or still-empty bytes: frames + // with stripes of zeros, which NV12 shows as green), so the back end is told directly. + nvn_guest_written((uint64_t)(uintptr_t)buf, ysz + uvsz); uint64_t uvp = (uint64_t)(uintptr_t)(buf + ysz); memcpy(fr + 0x10, &uvp, 8); memcpy(fr + 0x28, &ysz, 8), memcpy(fr + 0x30, &uvsz, 8); diff --git a/runtime/ui/menu.cpp b/runtime/ui/menu.cpp index 7df22bc8..766e3f79 100644 --- a/runtime/ui/menu.cpp +++ b/runtime/ui/menu.cpp @@ -757,6 +757,20 @@ static void row_upscaling(void) { if (nv >= 2) opt_apply("NX2NAVI_TAA", nv >= 7 ? "fsr3" : "dlss"); } +// HDR (vk_hdr.c), after a restart: off, the runtime's own (the game's tonemap kept above SDR white) or the game's native +// HDR. The brightness rows follow while it is on. +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_ENHANCED), menu_text(UI_MENU_HDR_NATIVE)}; + opt_choice_list("NX2NAVI_HDR", UI_MENU_HDR, values, names, 3, UI_MENU_HDR_DESC, -1, menu_text(UI_MENU_HDR_NOTE)); + if (!cfg_bool("NX2NAVI_HDR")) return; + opt_slider("NX2NAVI_HDR_PEAK_NITS", UI_MENU_HDR_PEAK, 400, 4000, UI_MENU_HDR_PEAK_DESC, 50, "%.0f nits"); + opt_slider("NX2NAVI_HDR_GAME_NITS", UI_MENU_HDR_GAME, 80, 500, UI_MENU_HDR_GAME_DESC, 5, "%.0f nits"); + opt_slider("NX2NAVI_HDR_UI_NITS", UI_MENU_HDR_UI, 80, 500, UI_MENU_HDR_UI_DESC, 5, "%.0f nits"); + // the hue correction belongs to the runtime's own HDR tonemap; the native one has the game's + if (strcmp(cfg_value("NX2NAVI_HDR"), "native")) opt_slider("NX2NAVI_HDR_HUE", UI_MENU_HDR_HUE, 0, 100, UI_MENU_HDR_HUE_DESC, 5, "%.0f %%"); +} + // Frame generation (vk_fg.cpp), after a restart; it needs a temporal upscaler's motion vectors static void row_framegen(void) { static const char *const values[] = {"off", "fsr3", "dlss"}; @@ -797,6 +811,7 @@ static void tab_screen(float scale) { if (wide_available()) row_game_res(); row_upscaling(); row_framegen(); + row_hdr(); // RCAS runs only under FSR (vk_fsr.c vk_fsr_record): with bilinear or DLSS the slider would do nothing if (strcmp(cfg_value("NX2NAVI_UPSCALER"), "fsr") || cfg_bool("NX2NAVI_DLAA")) opt_disable_next(UI_MENU_SHARPNESS_NEEDS_FSR); opt_slider("NX2NAVI_SHARPNESS", UI_MENU_SHARPNESS, 0, 100, UI_MENU_SHARPNESS_DESC, 5, "%.0f %%"); diff --git a/runtime/ui_text.c b/runtime/ui_text.c index 34104462..f6d03a0f 100644 --- a/runtime/ui_text.c +++ b/runtime/ui_text.c @@ -119,8 +119,10 @@ static const char *const text_en[UI_TEXT_COUNT] = { [UI_MENU_HDR_GROUP] = "HDR", [UI_MENU_HDR] = "HDR output", [UI_MENU_HDR_DESC] = "Bright highlights on an HDR display. Needs HDR turned on in Windows (\"Use HDR\") or in a " - "Wayland compositor with colour management; without it the picture stays SDR. Applies after a " - "restart. The colour profile above is for SDR and is not used in HDR.", + "Wayland compositor with colour management; without it the picture stays SDR. Native: the " + "game's own HDR, as on a Switch 2 on an HDR TV. Enhanced: the game's picture with its highlights " + "kept above the SDR white. Applies after a restart. The colour profile above is for SDR and is " + "not used in HDR.", [UI_MENU_HDR_PEAK] = "Peak brightness", [UI_MENU_HDR_PEAK_DESC] = "Your display's peak in nits; the brightest highlights stop there.", [UI_MENU_HDR_GAME] = "Game brightness", @@ -522,6 +524,8 @@ static const char *const text_en[UI_TEXT_COUNT] = { "NVIDIA's Streamline files next to the .exe. It pauses while this menu is open. Takes " "effect after a restart.", [UI_MENU_FRAMEGEN_OFF] = "Off", + [UI_MENU_HDR_NOTE] = "after restart", + [UI_MENU_HDR_ENHANCED] = "Enhanced", [UI_MENU_HDR_NATIVE] = "Native (the game's)", [UI_MENU_FRAMEGEN_NOTE] = "after restart; needs DLSS or FSR 3.1", [UI_MENU_GAME_RES] = "Resolution", [UI_MENU_GAME_RES_DESC] = "The height of the game's picture: 1440p is the game's own; higher draws a sharper picture " diff --git a/runtime/ui_text.h b/runtime/ui_text.h index db0a4d7d..5f65f576 100644 --- a/runtime/ui_text.h +++ b/runtime/ui_text.h @@ -244,7 +244,7 @@ typedef enum { // the settings menu's Picture rows for this game (ui/menu.cpp tab_screen): one upscaling choice (bilinear, FSR 1 // or a DLSS mode) and the picture's resolution UI_MENU_UPSCALING, UI_MENU_UPSCALING_DESC, UI_MENU_UPSCALING_DLSS_NOTE, UI_MENU_FRAMEGEN, UI_MENU_FRAMEGEN_DESC, - UI_MENU_FRAMEGEN_OFF, UI_MENU_FRAMEGEN_NOTE, + UI_MENU_FRAMEGEN_OFF, UI_MENU_FRAMEGEN_NOTE, UI_MENU_HDR_NOTE, UI_MENU_HDR_ENHANCED, UI_MENU_HDR_NATIVE, UI_MENU_GAME_RES, UI_MENU_GAME_RES_DESC, UI_MENU_GAME_RES_NATIVE, UI_TEXT_COUNT } UiTextId; diff --git a/runtime/vk_hdr.c b/runtime/vk_hdr.c index 275a8ea3..2728a878 100644 --- a/runtime/vk_hdr.c +++ b/runtime/vk_hdr.c @@ -46,6 +46,16 @@ int vk_hdr_wanted(void) { return g_wanted; } +// 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 swap, 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(); +} + void vk_hdr_off(void) { g_wanted = 0; } // The three nits, read again when the settings change. The replay (every draw's support buffer) and the present @@ -152,7 +162,7 @@ static int fsr_unclamp(uint32_t *w, uint64_t *words) { } void vk_hdr_shader_swap(uint32_t **spirv, uint64_t *words) { - if (!vk_hdr_wanted() || !*spirv || *words < 5) return; + if (!vk_hdr_wanted() || vk_hdr_native() || !*spirv || *words < 5) return; int unclamped = fsr_unclamp(*spirv, words); if (unclamped > 0) { static volatile int32_t fsrs; @@ -281,7 +291,7 @@ void vk_hdr_draw(VkExec *ex, const VkPipeInfo *info) { } void vk_hdr_present_source(const VkTexImage *win, VkImage *image, VkFormat *format) { - if (!vk_hdr_wanted()) return; + if (!vk_hdr_wanted() || vk_hdr_native()) return; vk_hdr_track = 1; plat_mutex_lock(&rec_lock); int r = -1; @@ -477,6 +487,12 @@ static int init(VkCtx *c) { VkImage vk_hdr_encode(VkCommandBuffer cb, unsigned slot, VkImage src, VkFormat fmt, const int crop[4]) { VkCtx *c = vk_ctx(); + // native HDR: the game's picture holds PQ codes already; the present scales it into the swapchain as it is + if (vk_hdr_native() && (fmt == VK_FORMAT_A2B10G10R10_UNORM_PACK32 || fmt == VK_FORMAT_A2R10G10B10_UNORM_PACK32)) { + static int told; + if (!told++) rt_log("hdr: native: the game's own HDR10 picture goes to the swapchain as it is"); + return VK_NULL_HANDLE; + } if (!H.tried) init(c); if (!H.ok || !img_ensure(c, &H.out, (uint32_t)crop[2], (uint32_t)crop[3], VK_FORMAT_R16G16B16A16_SFLOAT, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT)) diff --git a/tools/make_game.py b/tools/make_game.py index b8cdb1ba..1f6fed4f 100644 --- a/tools/make_game.py +++ b/tools/make_game.py @@ -381,8 +381,15 @@ def step_compile(out, cache, msys, jobs): for src, err in bad[:3]: print(f'--- {src}\n{err}', file=sys.stderr) fail(f'{len(bad)} files did not compile') + # the manifest (per-monitor DPI awareness: the picture at the monitor's real size, not one Windows stretches) and the + # icon, runtime/nx2navi.rc through MSYS2's windres + rco = obj / 'nx2navi_rc.o' + r = subprocess.run([str(msys / 'bin' / 'windres.exe'), '-I', str(rt), '-O', 'coff', '-i', str(rt / 'nx2navi.rc'), + '-o', str(rco)], capture_output=True, text=True, env=env) + if r.returncode: + fail(f'windres failed on runtime/nx2navi.rc: {r.stderr.strip()[:400]}') rsp = cache / 'link.rsp' - rsp.write_text('\n'.join('"%s"' % o.as_posix() for _, o in srcs), encoding='utf-8') + rsp.write_text('\n'.join('"%s"' % o.as_posix() for o in [o for _, o in srcs] + [rco]), encoding='utf-8') exe = out / 'ocarina.exe' say(' linking') p = subprocess.run([str(clangxx), '-o', str(exe), '@' + str(rsp), '-L', str(msys / 'lib'), *LINK_LIBS],