mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 01:41:02 +00:00
The heap bound in scene_for counts the output's own growth against the 2 GiB limit, so a 7680x4320 output at render_scale game went from the 4096x2304 scene dev renders to 2712x1526, with no setting changed. The bound now shrinks no scene below 4096 a side, the size dev gave; scenes the 4096 cap kept smaller still grow up to the bound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
691 lines
34 KiB
C
691 lines
34 KiB
C
// Game resolution: the size the game composes its frame in (its output: canvas, UI and swapchain) and the size of its
|
|
// 3D scene, above the shipped 1920x1080 and 1600x900. The game does the rendering itself, as with the community
|
|
// resolution mods (UltraCam, Visual Fixes): the patch sets output and scene (recomp/patches-v121.txt,
|
|
// patches-v143.txt; MacNativeGames games/totk/runtime/game_patches.c and game_patches_v143.c, MIT, Copyright (c) 2026
|
|
// MacNativeGames contributors) make the game read both sizes from the patch page, and the texture heap that holds the
|
|
// output- and scene-sized render targets grows with them.
|
|
//
|
|
// Settings, read at start: TOTK_OUTPUT_RES = <w>x<h> (default 1920x1080, the game's own), TOTK_RENDER_SCALE = the
|
|
// scene's size relative to the output: "game" (default, 5/6, the game's 1600x900 at 1080p) or a percentage; above 100
|
|
// the game supersamples its scene, which smooths edges (anti-aliasing) at the cost of GPU time. The game's own final
|
|
// pass (FSR 1 EASU) scales the scene to the output; the present scales the output to the window (vk_fsr.c).
|
|
//
|
|
// Patch page: +16 the scene size {w, h} as floats, +32 the output size and +48 the swapchain size as {w, h} in one
|
|
// 64-bit word, +40 the renderer (1.2.1) or display configuration (1.4.3) the patched code stores there.
|
|
// +56 the camera's aspect ratio as a float when the output is not 16:9 (set aspect), +60 the field-of-view factor
|
|
// (set fov, res_fov_live).
|
|
// 1.4.3 scales the scene it reads by canvas width / the console mode's base width, 1920 docked and 1280 handheld (a
|
|
// 2560x1440 output renders its 1600x900 scene at 2128x1200 docked; MacNativeGames measured the same), so its page holds
|
|
// the wanted size divided by that factor (scene_scale) and the game's 5/6 needs no scene patch. 1.2.1 renders the size
|
|
// it reads (1600x900 on a 2560x1440 output): its page holds the wanted size, and the scene set is on whenever the
|
|
// output differs from 1920x1080.
|
|
//
|
|
// Heap: agl's DynamicTextureAllocator is one fixed heap, "DynamicTextureAllocatorMem" in
|
|
// System/HeapSize/Product.Nin_NX_NVN.HeapDef.byml.zs (113664 KiB, carved from "Graphics", 201728 KiB). At 2560x1440
|
|
// the shipped size runs out at the title screen. Both heaps grow by the same amount: the shipped size times the output's
|
|
// pixel ratio above 1920x1080 plus the scene's above 1600x900 (MacNativeGames' measurement: a 3440x1440 canvas with a
|
|
// 4620x1935 scene crashed at +258 MiB and loaded at +400 MiB; this formula gives +750 MiB there). romfs.c serves the
|
|
// rewritten table (res_heapdef_build) in place of the original; memory.c adds the amount to the application pool, so
|
|
// no other heap shrinks. Without the rewritten table (a RomFS that lacks it, a format this code does not know) the
|
|
// patch sets stay off and the game keeps its own sizes.
|
|
#ifndef _WIN32
|
|
#include <dirent.h>
|
|
#endif
|
|
#include <math.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <zstd.h>
|
|
|
|
#include "cfg.h"
|
|
#include "host.h"
|
|
#include "mods.h"
|
|
#include "ov.h"
|
|
#include "platform/platform.h"
|
|
|
|
#define BASE_W 1920
|
|
#define BASE_H 1080
|
|
#define SCENE_W 1600
|
|
#define SCENE_H 900
|
|
#define DTA_KIB 113664u // DynamicTextureAllocatorMem, shipped
|
|
#define TEX_HEAP_KIB 506880u // TextureHeap, shipped (1.2.1 and 1.4.3): what a mod's own HeapDef is measured against
|
|
#define SCENE_MAX 8192 // per side
|
|
#define SCENE_KEEP 4096 // per side: the heap bound shrinks no scene below this (scene_for)
|
|
#define DTA_MAX_KIB 2031616u // DynamicTextureAllocatorMem grown at most to 1984 MiB, under 2 GiB (decide)
|
|
#define SHADOW_BASE 960 // the sun shadow map's base size (its cascades: a D16 array of 4 layers)
|
|
|
|
static struct {
|
|
int decided;
|
|
unsigned ow, oh, sw, sh; // output and scene
|
|
int scene_set; // the scene differs from the game's 5/6 of the output
|
|
int heapdef_ok; // romfs.c serves the grown HeapDef
|
|
int on; // res_init switched the patch sets on
|
|
unsigned shadow; // fixed sun shadow map size (TOTK_SHADOW_RES), 0: the game's own
|
|
double dlss_k; // TOTK_DLSS quality/balanced/performance/ultra_performance: the scene's share of the output, else 0
|
|
double base_k; // the scene's share without a DLSS mode (render_scale, or the game's 5/6)
|
|
unsigned hw, hh; // the largest scene a DLSS mode change can ask for: the heap is sized for it
|
|
int live; // 1.4.3 with DLSS able to run: the DLSS mode can change while the game runs
|
|
} R;
|
|
|
|
// The scene size is decided before the device exists, so NGX cannot be asked yet. NGX loads DLSS from nvngx_dlss.dll
|
|
// next to totk.exe on Windows, libnvidia-ngx-dlss.so.<version> next to totk on Linux (vk_dlaa.c vk_dlaa_init):
|
|
// without that file DLSS never runs, and a DLSS mode would leave the game's plain scaling at 50..67 % of the output.
|
|
static int dlss_dll(void) {
|
|
#ifdef _WIN32
|
|
char exe[1024], path[1024 + 32];
|
|
if (!plat_exe_path(exe, sizeof exe)) return 1;
|
|
char *last = plat_last_sep(exe);
|
|
if (!last) return 1;
|
|
snprintf(path, sizeof path, "%.*s/nvngx_dlss.dll", (int)(last - exe), exe);
|
|
return plat_file_exists(path);
|
|
#else
|
|
char exe[1024];
|
|
if (!plat_exe_path(exe, sizeof exe)) return 1;
|
|
char *last = plat_last_sep(exe);
|
|
if (!last) return 1;
|
|
*last = 0;
|
|
DIR *d = opendir(exe);
|
|
if (!d) return 1;
|
|
int found = 0;
|
|
for (struct dirent *e; !found && (e = readdir(d));)
|
|
found = !strncmp(e->d_name, "libnvidia-ngx-dlss.so.", 22);
|
|
closedir(d);
|
|
return found;
|
|
#endif
|
|
}
|
|
|
|
static uint64_t shadow_kib(void);
|
|
|
|
// The scene size for a share k of the output, within SCENE_MAX a side and within what a texture heap of DTA_MAX_KIB
|
|
// holds next to the output and the shadow map (res_heap_extra_kib's growth). 1.2.1 already misplaced textures in a
|
|
// pool past 2 GiB, where a signed 32-bit offset turns negative (recomp/patches-v121.txt, set pooloff); larger heaps
|
|
// have not been run. The heap bound never takes the scene below 4096 a side, the size earlier builds gave: an output
|
|
// near 8K grows the heap past 2 GiB for its own targets, and its scene stays what it was. Needs R.shadow. 1 when the
|
|
// scene was made smaller than k asks.
|
|
static int scene_for(double k, unsigned *w, unsigned *h) {
|
|
double sw = R.ow * k, sh = R.oh * k;
|
|
double fit = fmin(1.0, fmin(SCENE_MAX / sw, SCENE_MAX / sh));
|
|
double keep = fmin(1.0, fmin(SCENE_KEEP / sw, SCENE_KEEP / sh));
|
|
double room = (double)(DTA_MAX_KIB - shadow_kib()) / DTA_KIB - 1.0 -
|
|
fmax(0.0, (double)R.ow * R.oh / (BASE_W * BASE_H) - 1.0);
|
|
double most = (1.0 + fmax(0.0, room)) * SCENE_W * SCENE_H; // scene pixels that fit
|
|
if (sw * sh * fit * fit > most) fit = fmax(keep, sqrt(most / (sw * sh)));
|
|
*w = (unsigned)(sw * fit + 0.5);
|
|
*h = (unsigned)(sh * fit + 0.5);
|
|
return fit < 1.0;
|
|
}
|
|
|
|
// The scene's share of the output for a TOTK_DLSS mode; 0 for dlaa (the scene keeps base_k), while DLSS/DLAA is off
|
|
// (the mode the menu still holds would shrink the scene for the plain upscaler) and where DLSS cannot run (dlss_dll).
|
|
static double dlss_share(const char *m) {
|
|
static const struct { const char *name; double k; } dlss[] = {
|
|
{"quality", 2.0 / 3.0}, {"balanced", 0.58}, {"performance", 0.5}, {"ultra_performance", 1.0 / 3.0}};
|
|
static int dll = -1;
|
|
if (!cfg_bool("TOTK_DLAA")) return 0;
|
|
for (int i = 0; i < (int)(sizeof dlss / sizeof dlss[0]); i++)
|
|
if (!strcmp(m, dlss[i].name)) {
|
|
if (dll < 0 && !(dll = dlss_dll())) {
|
|
#ifdef _WIN32
|
|
rt_log("resolution: DLSS %s needs nvngx_dlss.dll next to totk.exe, which is missing: the scene follows "
|
|
"render_scale", m);
|
|
#else
|
|
rt_log("resolution: DLSS %s needs libnvidia-ngx-dlss.so.<version> next to totk, which is missing: the "
|
|
"scene follows render_scale", m);
|
|
#endif
|
|
}
|
|
return dll ? dlss[i].k : 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// A game resolution that is not 16:9 needs the HUD's layout for its shape (romfs-patches-<w>x<h>, romfs.c
|
|
// overlay_init), which the installer makes but a developer build only has after tools/make_romfs_patches.py --aspect.
|
|
// Without it the camera would widen while the 16:9 HUD stretched across the picture, so the game draws the largest
|
|
// 16:9 size that fits inside the asked one and the camera aspect patch stays off (res_init); gfx_present.c shows the
|
|
// picture with bars (present setting aspect). Not when TOTK_ROMFS_PATCHES names the overlay itself or the game version
|
|
// has no camera aspect patch. TOTK_OUTPUT_RES is restart-only (cfg.c, RESTART), so this one decision holds all run.
|
|
static void shape_fallback(void) {
|
|
const char *own = cfg_get("TOTK_ROMFS_PATCHES");
|
|
unsigned out[2] = {R.ow, R.oh};
|
|
if (own || !res_wants_wide_overlay(out) || romfs_shaped_ready(R.ow, R.oh)) return;
|
|
unsigned h = R.oh, w = R.ow;
|
|
if ((uint64_t)w * 9 <= (uint64_t)h * 16) h = (unsigned)((uint64_t)w * 9 / 16) & ~1u;
|
|
w = (unsigned)((uint64_t)h * 16 / 9) & ~1u;
|
|
rt_log("resolution: no romfs-patches-%ux%u for the HUD at this shape; the game draws %ux%u (16:9) and the present "
|
|
"adds bars. Make the layout with: tools/make_romfs_patches.py --version <v121|v143> --aspect %ux%u --out "
|
|
"build/<version>/romfs-patches-%ux%u", R.ow, R.oh, w, h, R.ow, R.oh, R.ow, R.oh);
|
|
R.ow = w, R.oh = h;
|
|
}
|
|
|
|
static void decide(void) {
|
|
if (R.decided) return;
|
|
R.decided = 1;
|
|
R.ow = BASE_W;
|
|
R.oh = BASE_H;
|
|
const char *o = cfg_value("TOTK_OUTPUT_RES");
|
|
unsigned w = 0, h = 0;
|
|
char tail;
|
|
if (sscanf(o, "%ux%u%c", &w, &h, &tail) == 2 && w >= 1280 && h >= 720 && w <= 7680 && h <= 4320) {
|
|
R.ow = w & ~1u;
|
|
R.oh = h & ~1u;
|
|
shape_fallback();
|
|
} else {
|
|
rt_log("resolution: TOTK_OUTPUT_RES=%s is not <width>x<height> in 1280x720..7680x4320; 1920x1080", o);
|
|
}
|
|
const char *s = cfg_value("TOTK_RENDER_SCALE");
|
|
double k = 5.0 / 6.0;
|
|
if (strcmp(s, "game")) {
|
|
char *end;
|
|
double pct = strtod(s, &end);
|
|
if (end != s && !*end && pct >= 25 && pct <= 400) {
|
|
k = pct / 100.0;
|
|
R.scene_set = 1;
|
|
} else {
|
|
rt_log("resolution: TOTK_RENDER_SCALE=%s is not game or a percentage 25..400; game", s);
|
|
}
|
|
}
|
|
const char *sr = cfg_value("TOTK_SHADOW_RES");
|
|
if (strcmp(sr, "game")) {
|
|
unsigned n = (unsigned)atoi(sr);
|
|
if (n >= 256 && n <= 4096 && !(n & 7)) R.shadow = n;
|
|
else rt_log("resolution: TOTK_SHADOW_RES=%s is not game or a size 256..4096 (a multiple of 8); game", sr);
|
|
}
|
|
// A DLSS mode other than dlaa sets the scene size instead. On 1.4.3 with DLSS able to run (DLAA on, the DLL there)
|
|
// the mode can change while the game runs (res_dlss_mode): the scene patch is then always on and the heap is sized
|
|
// for the largest scene the modes ask for. Everyone else keeps the game's own scene size and its dynamic resolution
|
|
// unless render_scale or a DLSS mode asks for another, and a heap sized for that scene alone.
|
|
R.base_k = k;
|
|
R.live = RT_V143 && cfg_bool("TOTK_DLAA") && dlss_dll();
|
|
const char *m = cfg_value("TOTK_DLSS");
|
|
R.dlss_k = dlss_share(m);
|
|
if (R.dlss_k) rt_log("resolution: DLSS %s: the scene at %.0f %% of the output (render_scale ignored)", m, R.dlss_k * 100);
|
|
double sk = R.dlss_k ? R.dlss_k : k;
|
|
if (R.live || R.dlss_k) R.scene_set = 1;
|
|
if (scene_for(sk, &R.sw, &R.sh)) {
|
|
R.scene_set = 1;
|
|
rt_log("resolution: a %.0fx%.0f scene is larger than the port allows (8192 a side, a texture heap under 2 GiB "
|
|
"past 4096 a side); %ux%u", R.ow * sk, R.oh * sk, R.sw, R.sh);
|
|
}
|
|
if (R.live) scene_for(fmax(k, 2.0 / 3.0), &R.hw, &R.hh);
|
|
else R.hw = R.sw, R.hh = R.sh;
|
|
}
|
|
|
|
// Texture heap for a sun shadow map above the base size: the four layers of 2 bytes, three maps in flight (the engine
|
|
// makes the array anew each frame).
|
|
static uint64_t shadow_kib(void) {
|
|
return R.shadow > SHADOW_BASE ? ((uint64_t)R.shadow * R.shadow - SHADOW_BASE * SHADOW_BASE) * 8 * 3 / 1024 : 0;
|
|
}
|
|
|
|
static int bigger(void) {
|
|
decide();
|
|
return R.ow != BASE_W || R.oh != BASE_H || R.scene_set || shadow_kib();
|
|
}
|
|
|
|
uint64_t res_heap_extra_kib(void) {
|
|
if (!bigger()) return 0;
|
|
double grow = fmax(0.0, (double)R.ow * R.oh / (BASE_W * BASE_H) - 1.0) +
|
|
fmax(0.0, (double)R.hw * R.hh / (SCENE_W * SCENE_H) - 1.0);
|
|
uint64_t kib = grow > 0 ? (uint64_t)ceil(DTA_KIB * (1.0 + grow) / 1024.0) * 1024 - DTA_KIB : 0;
|
|
return kib + ((shadow_kib() + 1023) & ~1023ull); // whole MiB
|
|
}
|
|
|
|
// 1.4.3's factor between the scene size it reads from the page and the one it renders. The game's scene setter
|
|
// (main+0x585e48 and the code before it) multiplies the page's size by the canvas width (display configuration +0x10)
|
|
// over the current console mode's base width (+0xf4 of the mode's entry, mode 1 handheld: main+0x2dac1c stores
|
|
// GetOperationMode != 1 at +0x546). The base is 1280 handheld and 1920 docked. The canvas is the output whenever the
|
|
// scene patch is on: docked the game's own canvas is 1920, handheld res_init switches the output patch on (a 1280 base
|
|
// with the game's 1280x720 handheld canvas would render R.ow / 1280 times the scene). 1.2.1 renders the size it reads.
|
|
static double scene_scale(void) {
|
|
if (!RT_V143) return 1.0;
|
|
return (double)R.ow / (ov_handheld() ? 1280 : BASE_W);
|
|
}
|
|
|
|
uint64_t res_heap_extra_bytes(void) { return ((res_heap_extra_kib() << 10) + 0x1fffff) & ~0x1fffffull; }
|
|
|
|
// TOTK_TEX_HEAP_EXTRA_MB (0 to 4096): more room in "TextureHeap" and its parent "ゲームグラフィックス", the heap the
|
|
// world's textures stream into. HD texture packs made for emulators with 8 GB of DRAM grow it by about 3.3 GiB in their
|
|
// own HeapDef; without the room the game runs out and starts its panic Blood Moon over and over. The pool and the heap
|
|
// cap grow by the same amount (memory.c mem_init).
|
|
static uint64_t mod_tex_heap_kib(void);
|
|
|
|
static uint64_t cfg_tex_heap_kib(void) {
|
|
const char *v = cfg_get("TOTK_TEX_HEAP_EXTRA_MB");
|
|
if (!v || !*v) return 0;
|
|
char *end;
|
|
long mb = strtol(v, &end, 10);
|
|
if (end == v || *end || mb < 0 || mb > 4096) {
|
|
static int logged;
|
|
if (!logged++) rt_log("resolution: TOTK_TEX_HEAP_EXTRA_MB=%s is not 0..4096; 0", v);
|
|
return 0;
|
|
}
|
|
return (uint64_t)mb << 10;
|
|
}
|
|
|
|
// The larger of TOTK_TEX_HEAP_EXTRA_MB and what an enabled mod's own HeapDef adds to the texture heap (mod_tex_heap_kib).
|
|
static uint64_t tex_heap_extra_kib(void) {
|
|
uint64_t c = cfg_tex_heap_kib(), m = mod_tex_heap_kib();
|
|
return c > m ? c : m;
|
|
}
|
|
|
|
uint64_t res_tex_heap_extra_bytes(void) { return ((tex_heap_extra_kib() << 10) + 0x1fffff) & ~0x1fffffull; }
|
|
|
|
// ---- the HeapDef table. BYML version 2..7, little endian ("YB"): a hash node is {u8 0xC1, u24 count} and count
|
|
// entries {u24 key index, u8 type, u32 value}; an int (type 0xD1) is stored in the entry, so a size changes in place.
|
|
typedef struct { uint8_t *p; size_t n; uint32_t keys; } Byml;
|
|
|
|
static int u32_at(const Byml *b, size_t off, uint32_t *v) {
|
|
if (off + 4 > b->n) return 0;
|
|
memcpy(v, b->p + off, 4);
|
|
return 1;
|
|
}
|
|
|
|
static const char *key_name(const Byml *b, uint32_t k) {
|
|
uint32_t head, at;
|
|
if (!u32_at(b, b->keys, &head) || (head & 0xff) != 0xc2 || k >= head >> 8 || !u32_at(b, b->keys + 4 + 4 * k, &at) ||
|
|
b->keys + at >= b->n || !memchr(b->p + b->keys + at, 0, b->n - b->keys - at))
|
|
return NULL;
|
|
return (const char *)b->p + b->keys + at;
|
|
}
|
|
|
|
// The offset of the value of hash node `node`'s entry `name`, which must have type `type`; 0 when there is none.
|
|
static size_t field(const Byml *b, uint32_t node, const char *name, uint8_t type) {
|
|
uint32_t head;
|
|
if (!u32_at(b, node, &head) || (head & 0xff) != 0xc1) return 0;
|
|
for (uint32_t i = 0; i < head >> 8; i++) {
|
|
uint32_t e;
|
|
if (!u32_at(b, node + 4 + 8 * (size_t)i, &e)) return 0;
|
|
const char *k = key_name(b, e & 0xffffff);
|
|
if (k && !strcmp(k, name)) return e >> 24 == type ? node + 8 + 8 * (size_t)i : 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static size_t heap_size_field(const Byml *b, uint32_t heaps, const char *heap) {
|
|
size_t at = field(b, heaps, heap, 0xc1);
|
|
uint32_t node;
|
|
return at && u32_at(b, at, &node) ? field(b, node, "Size", 0xd1) : 0;
|
|
}
|
|
|
|
// A HeapDef in b's place (zstd frame in[0..n) with its content size, no dictionary): b.p malloc'd and *heaps its
|
|
// "Heaps" node; 0 when it is not one (b.p freed). why: what the log line says the caller keeps then.
|
|
static int heapdef_open(const void *in, size_t n, Byml *b, uint32_t *heaps, const char *why) {
|
|
b->p = NULL;
|
|
if (ZSTD_getDictID_fromFrame(in, n)) {
|
|
rt_log("resolution: the HeapDef table needs a zstd dictionary; %s", why);
|
|
return 0;
|
|
}
|
|
unsigned long long raw = ZSTD_getFrameContentSize(in, n);
|
|
if (raw == ZSTD_CONTENTSIZE_UNKNOWN || raw == ZSTD_CONTENTSIZE_ERROR || raw > (64u << 20)) {
|
|
rt_log("resolution: the HeapDef table is not a zstd frame with a size; %s", why);
|
|
return 0;
|
|
}
|
|
*b = (Byml){malloc((size_t)raw), (size_t)raw, 0};
|
|
if (!b->p) return 0;
|
|
size_t got = ZSTD_decompress(b->p, b->n, in, n);
|
|
uint32_t root;
|
|
uint16_t ver = 0;
|
|
if (!ZSTD_isError(got) && got == b->n && b->n >= 16) memcpy(&ver, b->p + 2, 2);
|
|
size_t heaps_at = 0;
|
|
if (ver >= 2 && ver <= 7 && !memcmp(b->p, "YB", 2) && u32_at(b, 4, &b->keys) && u32_at(b, 12, &root) &&
|
|
(heaps_at = field(b, root, "Heaps", 0xc1)) && u32_at(b, heaps_at, heaps))
|
|
return 1;
|
|
free(b->p);
|
|
b->p = NULL;
|
|
return 0;
|
|
}
|
|
|
|
// HD texture packs made for the emulators ship their own HeapDef with "TextureHeap" and its parent grown (HDPacks.com's
|
|
// 4K pack for TotK: by 3,419,000 KiB, 3.3 GiB, for the emulators' 8 GB memory option). Served as it is, the game asks
|
|
// for that room and the application pool has none: it crashed at start (issue #673). So before mem_init sizes the pool,
|
|
// every switched-on mod's HeapDef is read (mods.c mods_peek_romfs: mods_scan needs paths main sets later) and the
|
|
// largest TextureHeap growth over the shipped size counts as TOTK_TEX_HEAP_EXTRA_MB (the larger of the two, at most
|
|
// 4 GiB). romfs.c heapdef_overlay then serves another table grown by that amount in the pack's place: the table of the
|
|
// highest switched-on mod below it whose texture heap is not grown, else the game's own, so the other heaps keep the
|
|
// sizes the content that runs needs.
|
|
#define HEAPDEF_REL "System/HeapSize/Product.Nin_NX_NVN.HeapDef.byml.zs"
|
|
static struct { int done; uint64_t kib; } MT;
|
|
|
|
// "TextureHeap" of the HeapDef table in[0..n) in KiB; 0 when the table cannot be read or has no such size (the failure
|
|
// is logged with why).
|
|
static int32_t heapdef_tex_kib(const void *in, size_t n, const char *why) {
|
|
Byml b;
|
|
uint32_t heaps;
|
|
size_t th_at = 0;
|
|
int32_t th = 0;
|
|
if (!heapdef_open(in, n, &b, &heaps, why)) return 0;
|
|
if ((th_at = heap_size_field(&b, heaps, "TextureHeap"))) memcpy(&th, b.p + th_at, 4);
|
|
else rt_log("resolution: the HeapDef table has no TextureHeap size; %s", why);
|
|
free(b.p);
|
|
return th;
|
|
}
|
|
|
|
static void mod_heapdef(const char *name, const char *romfs, void *ctx) {
|
|
(void)ctx;
|
|
char path[2300], why[400];
|
|
if (snprintf(path, sizeof path, "%s/%s", romfs, HEAPDEF_REL) >= (int)sizeof path) return;
|
|
uint64_t n = 0;
|
|
uint8_t *in = plat_file_read(path, &n);
|
|
if (!in) return;
|
|
snprintf(why, sizeof why, "mod %s: its texture heap growth is left out", name);
|
|
int32_t th = heapdef_tex_kib(in, (size_t)n, why);
|
|
free(in);
|
|
if (!th) return;
|
|
int64_t grow = (int64_t)th - (int64_t)TEX_HEAP_KIB;
|
|
if (grow > (int64_t)(4096u << 10)) grow = 4096u << 10;
|
|
if (grow > 0 && (uint64_t)grow > MT.kib) MT.kib = (uint64_t)grow;
|
|
rt_log("resolution: mod %s ships a HeapDef with TextureHeap %d KiB (the game's %u KiB)%s", name, th, TEX_HEAP_KIB,
|
|
grow > 0 ? ": the port grows the texture heap and the pool by the difference" : "");
|
|
}
|
|
|
|
static uint64_t mod_tex_heap_kib(void) {
|
|
if (!MT.done) MT.done = 1, mods_peek_romfs(mod_heapdef, NULL);
|
|
return MT.kib;
|
|
}
|
|
|
|
// Only such a table is swapped for the game's own (romfs.c heapdef_overlay): a mod table that changes other heaps
|
|
// keeps them. A table it cannot read stays the base (logged).
|
|
int res_heapdef_tex_grown(const void *in, size_t n) {
|
|
return heapdef_tex_kib(in, n, "the mod's table stays the base") > (int32_t)TEX_HEAP_KIB;
|
|
}
|
|
|
|
// A table that can stand in for an HD pack's (romfs.c heapdef_overlay): readable, with TextureHeap at most the shipped
|
|
// size. A content mod's table (The Second Quest grows RSDB and UI for its own files) is one.
|
|
int res_heapdef_plain(const void *in, size_t n) {
|
|
int32_t th = heapdef_tex_kib(in, n, "it does not stand in for the texture pack's table");
|
|
return th > 0 && th <= (int32_t)TEX_HEAP_KIB;
|
|
}
|
|
|
|
int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n) {
|
|
uint64_t extra = res_heap_extra_kib(), tex = tex_heap_extra_kib();
|
|
*out = NULL;
|
|
if (!extra && !tex) return 0;
|
|
Byml b;
|
|
uint32_t heaps = 0;
|
|
size_t dta_at = 0, gfx_at = 0;
|
|
if (heapdef_open(in, n, &b, &heaps, "the game keeps its own resolution")) {
|
|
dta_at = heap_size_field(&b, heaps, "DynamicTextureAllocatorMem");
|
|
gfx_at = heap_size_field(&b, heaps, "Graphics");
|
|
}
|
|
if (!dta_at || !gfx_at) {
|
|
rt_log("resolution: the HeapDef table has no DynamicTextureAllocatorMem / Graphics sizes; the game keeps its own "
|
|
"resolution");
|
|
free(b.p);
|
|
return 0;
|
|
}
|
|
int32_t dta, gfx;
|
|
memcpy(&dta, b.p + dta_at, 4);
|
|
memcpy(&gfx, b.p + gfx_at, 4);
|
|
int32_t dta2 = dta + (int32_t)extra, gfx2 = gfx + (int32_t)extra;
|
|
memcpy(b.p + dta_at, &dta2, 4);
|
|
memcpy(b.p + gfx_at, &gfx2, 4);
|
|
int32_t th = 0, th2 = 0, gg = 0, gg2 = 0;
|
|
if (tex) { // TOTK_TEX_HEAP_EXTRA_MB: the world texture heap and its parent grow by the same amount
|
|
static const char parent[] = "\xe3\x82\xb2\xe3\x83\xbc\xe3\x83\xa0\xe3\x82\xb0\xe3\x83\xa9\xe3\x83\x95"
|
|
"\xe3\x82\xa3\xe3\x83\x83\xe3\x82\xaf\xe3\x82\xb9"; // ゲームグラフィックス
|
|
size_t th_at = heap_size_field(&b, heaps, "TextureHeap"), gg_at = heap_size_field(&b, heaps, parent);
|
|
if (th_at && gg_at) {
|
|
memcpy(&th, b.p + th_at, 4);
|
|
memcpy(&gg, b.p + gg_at, 4);
|
|
th2 = th + (int32_t)tex, gg2 = gg + (int32_t)tex;
|
|
memcpy(b.p + th_at, &th2, 4);
|
|
memcpy(b.p + gg_at, &gg2, 4);
|
|
} else {
|
|
rt_log("resolution: the HeapDef table has no TextureHeap sizes; TOTK_TEX_HEAP_EXTRA_MB left out");
|
|
tex = 0;
|
|
if (!extra) {
|
|
free(b.p);
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
size_t cap = ZSTD_compressBound(b.n);
|
|
void *z = malloc(cap);
|
|
size_t zn = z ? ZSTD_compress(z, cap, b.p, b.n, 9) : 0;
|
|
free(b.p);
|
|
if (!z || ZSTD_isError(zn)) {
|
|
free(z);
|
|
return 0;
|
|
}
|
|
*out = z;
|
|
*out_n = zn;
|
|
if (extra)
|
|
rt_log("resolution: HeapDef DynamicTextureAllocatorMem %d -> %d KiB, Graphics %d -> %d KiB", dta, dta2, gfx,
|
|
gfx2);
|
|
if (tex)
|
|
rt_log("resolution: HeapDef TextureHeap %d -> %d KiB and its parent %d -> %d KiB (TOTK_TEX_HEAP_EXTRA_MB or a "
|
|
"mod's HeapDef)", th, th2, gg, gg2);
|
|
return 1;
|
|
}
|
|
|
|
void res_heapdef_served(void) { R.heapdef_ok = 1; }
|
|
|
|
// ---- sun shadow map size: each frame the engine sizes its shadow cascades to the area they cover, down to about 400 near
|
|
// the ground, which draws foliage shadows blocky, and each change of size moves the shadow texels, so leaf shadows jump.
|
|
// A fixed size (the shadow set, recomp/patches-v121.txt) keeps them still and sharper. Page +92.
|
|
static void shadow_init(uint8_t *page) {
|
|
if (!R.shadow) {
|
|
rt_log("resolution: sun shadow map sized by the game (TOTK_SHADOW_RES=game)");
|
|
return;
|
|
}
|
|
int set = rt_patch_set("shadow");
|
|
if (set < 0) {
|
|
rt_log("resolution: the generated C has no shadow patch set: the game sizes its shadow map");
|
|
return;
|
|
}
|
|
int32_t n = (int32_t)R.shadow;
|
|
memcpy(page + 92, &n, 4);
|
|
rt_patch_switch(set, 1);
|
|
rt_log("resolution: sun shadow map %ux%u (TOTK_SHADOW_RES)", R.shadow, R.shadow);
|
|
}
|
|
|
|
// ---- start: the patch page and the sets
|
|
static uint64_t pack(unsigned w, unsigned h) { return (uint64_t)h << 32 | w; }
|
|
|
|
// TOTK_SHADOW_RANGE (percent, 50 to 400; 100 = the game's): the sun's shadow cascades end at 20, 40, 80 and 160 m.
|
|
// Other values multiply those distances through the cascade patch set (recomp/patches-v143.txt), so the sharp first
|
|
// cascade reaches further and the visible steps between cascades move away from the player, at the cost of shadow
|
|
// texels per metre (TOTK_SHADOW_SCALE pays that back).
|
|
static void res_shadow_range(void) {
|
|
int pct = cfg_int("TOTK_SHADOW_RANGE"), set = rt_patch_set("cascade");
|
|
if (pct == 100) return;
|
|
if (set < 0) {
|
|
rt_log("resolution: the generated C has no cascade patch set; shadow_range stays 100");
|
|
return;
|
|
}
|
|
float k = (float)(pct < 50 ? 50 : pct > 400 ? 400 : pct) / 100.0f, d[4] = {20 * k, 40 * k, 80 * k, 160 * k};
|
|
memcpy((uint8_t *)rt_patch_page() + 64, d, sizeof d);
|
|
rt_patch_switch(set, 1);
|
|
rt_log("resolution: shadow cascades end at %g, %g, %g and %g m", d[0], d[1], d[2], d[3]);
|
|
}
|
|
|
|
void res_init(void) {
|
|
res_shadow_range();
|
|
if (!bigger()) {
|
|
rt_log("resolution: the game's own (output 1920x1080, scene 1600x900)");
|
|
shadow_init(rt_patch_page());
|
|
return;
|
|
}
|
|
int output = rt_patch_set("output"), scene = rt_patch_set("scene");
|
|
if (output < 0 || scene < 0) {
|
|
rt_log("resolution: the generated C has no output and scene patch sets; the game keeps its own resolution");
|
|
return;
|
|
}
|
|
fs_init(); // romfs.c serves the grown HeapDef from here on (res_heapdef_served)
|
|
if (res_heap_extra_kib() && !R.heapdef_ok) { // sizes at or below the shipped ones fit the shipped heaps
|
|
rt_log("resolution: no grown HeapDef table; the game keeps its own resolution");
|
|
return;
|
|
}
|
|
uint8_t *page = rt_patch_page();
|
|
double k = scene_scale();
|
|
int scene_on = R.scene_set || (!RT_V143 && (R.ow != BASE_W || R.oh != BASE_H));
|
|
// Handheld, 1.4.3 draws a 1280x720 canvas unless the output patch is on, whatever the output setting (measured: a
|
|
// 1920x1080 output with the patch off composed the frame at 1280x720 and only the swapchain was 1920x1080). With
|
|
// the scene patch on the output patch is switched on too, so the canvas is the output size that scene_scale and
|
|
// the heap size (res_heap_extra_kib) assume.
|
|
int output_on = R.ow != BASE_W || R.oh != BASE_H || (RT_V143 && scene_on && ov_handheld());
|
|
float sc[2] = {(float)(R.sw / k), (float)(R.sh / k)};
|
|
uint64_t out = pack(R.ow, R.oh), none = 0;
|
|
memcpy(page + 16, sc, sizeof sc);
|
|
memcpy(page + 32, &out, 8);
|
|
memcpy(page + 40, &none, 8);
|
|
memcpy(page + 48, &out, 8);
|
|
rt_patch_switch(output, output_on);
|
|
rt_patch_switch(scene, scene_on);
|
|
R.on = 1;
|
|
if (R.live) g_fnprof = 1; // until the game sets its render size: res_fn_hit, or res_present gives up
|
|
// Another shape than 16:9 (more than 1% off): the camera takes the output's ratio (recomp/patches-v121.txt and
|
|
// patches-v143.txt, set aspect), so a 21:9 output shows more of the world at the sides instead of a stretched 16:9 view. The HUD's layouts
|
|
// for that shape come from romfs-patches-<w>x<h> (romfs.c overlay_init, tools/make_romfs_patches.py --aspect).
|
|
double ratio = (double)R.ow / R.oh, game = (double)BASE_W / BASE_H;
|
|
int aspect = rt_patch_set("aspect");
|
|
if (!res_is_16x9(R.ow, R.oh)) {
|
|
if (aspect < 0) {
|
|
rt_log("resolution: output %ux%u is not 16:9 and this game version has no camera aspect patch; the view "
|
|
"is stretched", R.ow, R.oh);
|
|
} else {
|
|
float r = (float)ratio;
|
|
memcpy(page + 56, &r, sizeof r);
|
|
rt_patch_switch(aspect, 1);
|
|
rt_log("resolution: camera aspect ratio %.4f (16:9 is %.4f)", ratio, game);
|
|
}
|
|
} else {
|
|
rt_patch_switch(aspect, 0); // the page holds no ratio
|
|
}
|
|
shadow_init(page);
|
|
rt_log("resolution: output %ux%u, scene %ux%u%s; texture heap +%llu KiB", R.ow, R.oh, R.sw, R.sh,
|
|
scene_on ? "" : " (the game's own 5/6 of the output)", (unsigned long long)res_heap_extra_kib());
|
|
}
|
|
|
|
// DLSS upscaling: textures are sampled for the output's resolution, not the scene's (NVIDIA's DLSS guide: a mip bias
|
|
// of log2(render / output)), so their detail survives the scaling. 0 without a DLSS mode.
|
|
float res_dlss_lod_bias(void) {
|
|
decide();
|
|
return R.dlss_k ? (float)log2(R.dlss_k) : 0.0f;
|
|
}
|
|
|
|
// 16:9 within 1% (1366x768 counts): the shape the game's own layouts and camera are made for.
|
|
int res_is_16x9(unsigned w, unsigned h) {
|
|
double rel = ((double)w / h) / ((double)BASE_W / BASE_H);
|
|
return rel >= 0.99 && rel <= 1.01;
|
|
}
|
|
|
|
// The game resolution asks for a romfs-patches-<w>x<h> overlay: it is not 16:9 and the game version has the camera
|
|
// aspect patch (set aspect); the UI layouts of another shape only fit a camera that follows the same shape.
|
|
int res_wants_wide_overlay(const unsigned out[2]) {
|
|
return !res_is_16x9(out[0], out[1]) && rt_patch_set("aspect") >= 0;
|
|
}
|
|
|
|
// Field of view (TOTK_FOV, percent of the game's own, 70..140, live): the camera's vertical view angle times the
|
|
// factor on page +60 (recomp/patches-v143.txt, set fov). The horizontal angle follows through the aspect ratio. 100
|
|
// switches the set off, so the camera code is the game's own. Runs on a settings change (gfx_present.c
|
|
// window_settings_live), the first time at the first present.
|
|
#define FOV_MIN 70
|
|
#define FOV_MAX 140
|
|
void res_fov_live(void) {
|
|
static int set = -2, shown = 100;
|
|
if (set == -2) set = rt_patch_set("fov");
|
|
int pct = cfg_int("TOTK_FOV");
|
|
pct = pct < FOV_MIN ? FOV_MIN : pct > FOV_MAX ? FOV_MAX : pct;
|
|
if (pct == shown) return;
|
|
shown = pct;
|
|
if (set < 0) {
|
|
rt_log("resolution: this game version has no field-of-view patch; TOTK_FOV=%d does nothing", pct);
|
|
return;
|
|
}
|
|
float k = pct / 100.0f;
|
|
memcpy((uint8_t *)rt_patch_page() + 60, &k, sizeof k); // before the switch: a site that sees it on reads k
|
|
rt_patch_switch(set, pct != 100);
|
|
rt_log("resolution: field of view %d%% of the game's", pct);
|
|
}
|
|
|
|
// TOTK_DYNAMIC_RES = 0 holds the game's dynamic resolution at 100 % (recomp/patches-v121.txt and patches-v143.txt,
|
|
// set fixres), so the scene renders at the size render_scale chose. The game picks its tier (100, 90 or 80 %) mostly
|
|
// from a fixed cost model made for the Switch: one term grows with the object counts, one with (scene width / 1600)
|
|
// squared. The GPU time it also measures (two NVN report timestamps, a frame apart) only lowers that prediction when the
|
|
// GPU is slower than the model says, which a PC GPU never is, so the model alone decides. Measured on 1.2.1: a
|
|
// 2867-wide scene stays at 100 %, one wider than about 2970 does not, and a 4267-wide one (5120x1440 at render_scale
|
|
// 83) fits no tier and falls back to 80 % every frame, whatever the GPU's load. 1 (the default) leaves the game's own
|
|
// stepping and the set off. Runs on a settings change (gfx_present.c window_settings_live), like res_fov_live.
|
|
void res_dynres_live(void) {
|
|
static int set = -2, shown = -1;
|
|
if (set == -2) set = rt_patch_set("fixres");
|
|
int dyn = cfg_bool("TOTK_DYNAMIC_RES");
|
|
if (dyn == shown) return;
|
|
shown = dyn;
|
|
if (set < 0) {
|
|
if (!dyn) rt_log("resolution: this game version has no fixed-resolution patch; the game keeps its dynamic resolution");
|
|
return;
|
|
}
|
|
rt_patch_switch(set, !dyn);
|
|
rt_log("resolution: dynamic resolution %s",
|
|
dyn ? "on: the game's own 100, 90 or 80 %" : "off: the scene stays at 100 %");
|
|
}
|
|
|
|
void res_wanted(unsigned out[2]) {
|
|
decide();
|
|
out[0] = R.ow, out[1] = R.oh;
|
|
}
|
|
|
|
// A TOTK_DLSS change while the game runs (vk_dlaa.c, at a present): the new scene size goes to the patch page, where
|
|
// the game reads it when it sets its render size (main+0x585e48). 1.4.3 does that once, at start: its renderer setup
|
|
// (main+0x7000e67028) calls main+0x585bd0(renderer, force), which picks the console mode's sizes and flags the render
|
|
// targets for re-creation. The setup call is caught through the function-entry hook (res_fn_hit, on only until then),
|
|
// and the next nvnQueuePresentTexture on the game's thread calls it again with force set (res_present), between frames
|
|
// as at start. The heap was sized for the largest scene a mode asks for. 1 when the size changed.
|
|
#define SET_RENDER_SIZE 0x7000585bd0ull
|
|
static uint64_t renderer; // x0 of the game's own call of SET_RENDER_SIZE
|
|
static volatile int size_pending; // the page holds a size the game has not set yet
|
|
|
|
void res_fn_hit(uint64_t addr, uint64_t x0) {
|
|
if (addr != SET_RENDER_SIZE) return;
|
|
renderer = x0;
|
|
g_fnprof = 0;
|
|
rt_log("resolution: renderer at %#llx; the scene size can change while the game runs", (unsigned long long)x0);
|
|
}
|
|
|
|
void res_present(Cpu *c) {
|
|
// The hook runs on every guest function entry while it waits: should the game set its render size before
|
|
// res_init or never, it stops after 1800 presents (half a minute at 60 fps) and modes change at the next start.
|
|
static unsigned waited;
|
|
if (g_fnprof && !renderer && ++waited == 1800) {
|
|
g_fnprof = 0;
|
|
rt_log("resolution: the game's render size setup was not seen; DLSS modes change at the next start");
|
|
}
|
|
if (!size_pending || !renderer) return;
|
|
size_pending = 0;
|
|
Cpu keep = *c; // the guest's registers as at the present call: the setup runs as if it never happened
|
|
rt_call(c, SET_RENDER_SIZE, 2, (uint64_t)renderer, (uint64_t)1);
|
|
memcpy(c->x, keep.x, sizeof c->x);
|
|
memcpy(c->v, keep.v, sizeof c->v);
|
|
c->nzcv = keep.nzcv, c->fpsr = keep.fpsr;
|
|
rt_log("resolution: the game set the scene size %ux%u", R.sw, R.sh);
|
|
}
|
|
|
|
int res_dlss_mode(const char *m) {
|
|
decide();
|
|
double k = dlss_share(m);
|
|
if (k == R.dlss_k || !R.on) return 0;
|
|
if (!RT_V143 || !renderer) {
|
|
rt_log("resolution: DLSS %s takes effect at the next start (the game set its render size already)", m);
|
|
return 0;
|
|
}
|
|
R.dlss_k = k;
|
|
unsigned w, h;
|
|
scene_for(k ? k : R.base_k, &w, &h);
|
|
if (w == R.sw && h == R.sh) return 0;
|
|
double s = scene_scale();
|
|
float sc[2] = {(float)(w / s), (float)(h / s)};
|
|
memcpy((uint8_t *)rt_patch_page() + 16, sc, sizeof sc);
|
|
R.sw = w, R.sh = h;
|
|
size_pending = 1;
|
|
rt_log("resolution: DLSS %s: scene %ux%u", m, w, h);
|
|
return 1;
|
|
}
|
|
|
|
void res_sizes(unsigned out[2], unsigned scene[2]) {
|
|
decide();
|
|
out[0] = R.on ? R.ow : BASE_W, out[1] = R.on ? R.oh : BASE_H;
|
|
scene[0] = R.on ? R.sw : SCENE_W, scene[1] = R.on ? R.sh : SCENE_H;
|
|
}
|