mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Add the game's native HDR, render at the monitor's real size, and refresh movie frames on the GPU.
HDR is now a menu row: Off, Enhanced (the runtime's own HDR tonemap) or Native. Native reports an HDR10 display to the game with the settings' peak and paper white; the game then renders its own HDR picture and presents it as PQ in A2B10G10R10, which goes to the HDR10 swapchain as it is. The peak, game, HUD and (for Enhanced) hue rows follow it. The build kit's link left out runtime/nx2navi.rc, so the game had no manifest and no icon: without per-monitor DPI awareness, a 5120x2160 monitor at 150 % gave a 3413x1440 window that Windows stretched. It is compiled with windres and linked again. The movie decoder's frames are copied into the game's buffer by the host; the back end is now told of that write, so the GPU's copy of the buffer does not keep older or empty bytes (green stripes in NV12). A longjmp out of the game's JPEG decoder now logs the guest functions that called it, to trace a later crash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 %%");
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user