Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 35 additions & 9 deletions gpu/vk/vk_menu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <chrono>
#include <cstdio>
#include <cstring>
#include <iterator>
#include <mutex>
#include <string>
#include <vector>
Expand Down Expand Up @@ -74,9 +75,19 @@ const char* tx_percent(int v) {
struct Out {
uint32_t w, h;
};
constexpr Out kOutputs[] = {{2048, 1152}, {2560, 1440}, {3840, 2160}, {1920, 1080}};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With 1080p appended, the menu now cycles 2048x1152 → 2560x1440 → 3840x2160 → 1920x1080, so the smallest size comes after 4K. To keep the saved ids as they are (3 = 1080p) but show the sizes in order, the row could step through a display-order table, e.g. {3, 0, 1, 2} (with Left/Right mapping through it), or tx_output_res / the row could use a small id↔position mapping. Same for the readme table, which lists it last.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should break away from this int setting now before we add other of other resolutions too

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the kOutputOrder table does it: Left/Right, wheel and the value halves all go through set_row, so they step in size order, and the saved output_res / HOS_OUTPUT_RES ids keep their meaning. Fine with keeping the int setting here, a different format can be its own PR if more sizes come.

constexpr int kLastOutput = std::size(kOutputs) - 1;
int output_choice(int v) { return v >= 0 && v <= kLastOutput ? v : 0; }
// Display the sizes from smallest to largest while keeping the IDs used by saved settings.
constexpr int kOutputOrder[] = {3, 0, 1, 2};
static_assert(std::size(kOutputOrder) == std::size(kOutputs));
int step_output_choice(int id, int dir) {
const int* pos = std::find(std::begin(kOutputOrder), std::end(kOutputOrder), output_choice(id));
const int next = std::clamp((int)(pos - std::begin(kOutputOrder)) + dir, 0, kLastOutput);
return kOutputOrder[next];
}
Out shown_output() {
static const Out t[] = {{2048, 1152}, {2560, 1440}, {3840, 2160}};
return opts.handheld ? Out{1536, 864} : t[std::min(std::max(opts.output_res, 0), 2)];
return opts.handheld ? Out{1536, 864} : kOutputs[output_choice(opts.output_res)];
}
constexpr int kResPercent[] = {0, 100, 75, 67, 50};
// The size the game draws the 3D at for a fixed scale of its output, a twelfth of it: every twelfth asked came out exact at the three
Expand All @@ -97,8 +108,11 @@ const char* tx_resolution(int v) {
return b;
}
const char* tx_output_res(int v) {
static const char* t[] = {"2048x1152 (game, restart)", "2560x1440 (restart)", "3840x2160 (restart)"};
return t[v];
static char b[48];
v = output_choice(v);
const Out o = kOutputs[v];
snprintf(b, sizeof b, "%ux%u (%srestart)", o.w, o.h, v ? "" : "game, ");
return b;
}
const char* tx_speed(int v) { return v ? "Real time" : "Per frame"; }
const char* tx_text_speed(int v) {
Expand Down Expand Up @@ -183,7 +197,7 @@ Row rows[] = {
{TAB_GRAPHICS, "Resolution", &opts.resolution, 0, 4, 1, tx_resolution,
"How sharp the 3D is drawn. Auto lets the game lower it when the GPU is busy. A fixed percentage stays steady: lower is "
"faster."},
{TAB_GRAPHICS, "Output resolution", &opts.output_res, 0, 2, 1, tx_output_res,
{TAB_GRAPHICS, "Output resolution", &opts.output_res, 0, kLastOutput, 1, tx_output_res,
"The size the game draws its whole frame at, HUD included, before it is scaled to your window. The 3D is drawn at the "
"Resolution percentage of it. Docked only. Applies after a restart."},
{TAB_GRAPHICS, "Upscaling", &opts.upscale, 0, 2, 1, tx_upscale,
Expand Down Expand Up @@ -444,6 +458,10 @@ void save() {
/// A row's new value v, as the arrows set it (dir: 1 up, -1 down, for the rows that skip or wrap), saved unless !keep
/// (a slider's drag: saved once it is let go)
void set_row(Row& r, int v, int dir, bool keep = true) {
if (r.value == &opts.output_res) {
v = step_output_choice(*r.value, dir);
if (v == *r.value) return;
}
if (r.value == &opts.framegen && v == 1) v = dir > 0 ? 2 : 0; // (off, 2x ... 8x)
if (r.value == &opts.mouse_sens) v = std::min(std::max(v, r.lo), r.hi); // (a scale: it stops at its ends)
if (r.value == &opts.pad_layout) {
Expand Down Expand Up @@ -907,7 +925,12 @@ void paint_menu(HDC dc, uint32_t w, uint32_t h, int s) {
GetTextExtentPoint32A(dc, t.c_str(), (int)t.size(), &z);
return (int)z.cx;
};
auto framed = [&](const std::string& t) { return arrows ? "< " + t + " >" : t; };
const bool output_row = r.value == &opts.output_res;
const bool left_arrow = arrows && (!output_row || step_output_choice(*r.value, -1) != *r.value);
const bool right_arrow = arrows && (!output_row || step_output_choice(*r.value, 1) != *r.value);
auto framed = [&](const std::string& t) {
return arrows ? (left_arrow ? "< " : " ") + t + (right_arrow ? " >" : " ") : t;
};
std::string v = framed(label);
if (width(v) > room && arrows) { // (slides: the longest piece from marquee_pos that fits)
size_t start = label.size(); // (the last start that still shows the name's end)
Expand Down Expand Up @@ -1081,8 +1104,11 @@ void menu_init() {
char* eq = strchr(line, '=');
if (!eq) continue;
*eq = 0;
for (Row& r : rows)
if (r.value && !strcmp(line, ini_key(r))) *r.value = std::min(std::max(atoi(eq + 1), r.lo), r.hi);
for (Row& r : rows) {
if (!r.value || strcmp(line, ini_key(r))) continue;
const int v = atoi(eq + 1);
*r.value = r.value == &opts.output_res ? output_choice(v) : std::min(std::max(v, r.lo), r.hi);
}
has_resolution |= !strcmp(line, "resolution");
v2 |= !strcmp(line, "options_version");
if (!strcmp(line, "dynamic_resolution")) old_dynres = atoi(eq + 1);
Expand Down Expand Up @@ -1110,7 +1136,7 @@ void menu_init() {
if (r.value == &opts.upscale) *r.value = std::min(std::max(atoi(e), r.lo), r.hi);
if (const char* e = getenv("HOS_REALTIME_STEP")) opts.realtime = atoi(e) != 0;
if (const char* e = getenv("HOS_DYNRES")) opts.resolution = atoi(e) != 0 ? 0 : 1;
if (const char* e = getenv("HOS_OUTPUT_RES")) opts.output_res = std::min(std::max(atoi(e), 0), 2);
if (const char* e = getenv("HOS_OUTPUT_RES")) opts.output_res = output_choice(atoi(e));
}

bool resolution_auto() { return opts.resolution == 0; }
Expand Down
2 changes: 1 addition & 1 deletion gpu/vk/vk_menu.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ struct Options {
int framegen = 0; // made frames: 0 off, or 2, 3, 4 times the game's frames
int pace = 0; // frames shown on an even line (vk_framegen.cpp)
int resolution = 0; // the 3D's: 0 Auto (the game's dynamic resolution), else 100%, 75%, 67%, 50% of its full scale (nx2/oot_patches.py)
int output_res = 0; // the game's whole frame, docked: 0 its 2048x1152, 1 2560x1440, 2 3840x2160; read once, at the start (host/nn_applet.c)
int output_res = 0; // the game's whole frame, docked: 0 its 2048x1152, 1 2560x1440, 2 3840x2160, 3 1920x1080 (host/nn_applet.c)
int upscale = 0; // to the window: 0 off (the plain copy to the window), 1 Catmull-Rom, 2 Lanczos
int sharpness = 0; // 0 to 100
int realtime = 1; // game time follows the clock (host/nn_vi.c); 0: per frame (slower frames, slower game)
Expand Down
17 changes: 12 additions & 5 deletions host/nn_applet.c
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,26 @@ static int handheld(void) {
return v;
}
/* (hookshot) The Insert menu's Output resolution (output_res=, HOS_OUTPUT_RES over it), docked only: 0 the game's 2048 x 1152,
1 2560 x 1440, 2 3840 x 2160. The game's frame is its display size (2560 x 1440 as shipped; nx2/oot_patches.py
1 2560 x 1440, 2 3840 x 2160, 3 1920 x 1080. The game's frame is its display size (2560 x 1440 as shipped; nx2/oot_patches.py
apply_output_size asks hos_output_size) times its graphics scale (GetCurrentGraphicsScale: 0 x1, 1 x0.8, 2 x0.667). Here it
saw scale 1 (x0 left at the call before's answer), so 2048 x 1152; 1 and 2 ask for x1 of a 2560 x 1440 or 3840 x 2160 display. */
saw scale 1 (x0 left at the call before's answer), so 2048 x 1152; each explicit size uses x1. */
static int output_mode(void) {
static int v = -1;
if (v < 0) {
static const char *const names[] = {"2048x1152 (the game's)", "2560x1440", "3840x2160", "1920x1080"};
v = getenv("HOS_OUTPUT_RES") ? atoi(getenv("HOS_OUTPUT_RES")) : ini_option("output_res", 0);
if (v < 0 || v > 2 || handheld()) v = 0;
hos_log("oe: output resolution %s", v == 2 ? "3840x2160" : v == 1 ? "2560x1440" : "2048x1152 (the game's)");
if ((unsigned)v >= sizeof names / sizeof *names || handheld()) v = 0;
hos_log("oe: output resolution %s", names[v]);
}
return v;
}
uint32_t hos_output_size(int height) { return output_mode() == 2 ? (height ? 2160 : 3840) : (height ? 1440 : 2560); }
uint32_t hos_output_size(int height) {
switch (output_mode()) {
case 2: return height ? 2160 : 3840;
case 3: return height ? 1080 : 1920;
default: return height ? 1440 : 2560;
}
}
/* (hookshot) The render targets' GPU pool (nx2/oot_patches.py), 0x13100000 bytes as shipped, for 2560 x 1440: a 3840x2160 3D at
the 4K output ran out of it. Scaled with the output's pixels; HOS_RT_POOL_MB=<MB> over it */
uint64_t hos_rt_pool_size(uint64_t game) {
Expand Down
8 changes: 5 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ and the right one closes the menu. Your choices are saved in `hookshot_options.i
| | Frame generation | Off, 2x to 8x | Off |
| | Frame pacing | Off, On | Off |
| | Resolution | Auto (the game's own dynamic resolution), 100%, 75%, 67%, 50% (each with its size) | Auto |
| | Output resolution | 2048x1152, 2560x1440, 3840x2160 (after a restart) | 2048x1152 |
| | Output resolution | 1920x1080, 2048x1152, 2560x1440, 3840x2160 (after a restart) | 2048x1152 |
| | Upscaling | Off, Catmull-Rom, Lanczos | Off |
| | Sharpness | 0 to 100%, in steps of 10 | 0% |
| | Handheld profile | Off, On (after a restart) | Off |
Expand Down Expand Up @@ -222,8 +222,10 @@ and the right one closes the menu. Your choices are saved in `hookshot_options.i
- **Frame generation** makes extra frames between the game's own. It adds a little input delay, and fast motion can
show artifacts. **Frame pacing** shows frames at an even interval: less stutter, up to one frame of delay.
- **Output resolution** is the size of the game's whole frame, HUD included, before it is scaled to your window.
Resolution is a share of it, and its row shows the size for the output you chose. Docked only. At 3840x2160 the game's
render targets get a bigger GPU pool, so a full-size 3D fits. `HOS_OUTPUT_RES=0/1/2` sets it for one run.
On a 1920x1080 display, the 1920x1080 option keeps HUD and text sharp when the game fills the screen by mapping them
pixel for pixel. The separate **Resolution** setting scales the 3D scene as a share of the output size. Docked only.
At 3840x2160 the game's render targets get a bigger GPU pool so full-size 3D fits. For one run, set `HOS_OUTPUT_RES`
to `3` for 1920x1080, `0` for 2048x1152, `1` for 2560x1440, or `2` for 3840x2160.
- **Upscaling** and **Sharpness** also work when Frame generation is Off.
- **Handheld profile** runs the game as if the console were undocked: lower resolution, lighter on the GPU.
- **Robust buffers:** a read past a buffer's end is dropped instead of stopping the GPU. Auto is on, on every card; Off
Expand Down
1 change: 1 addition & 0 deletions tests/run_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"tests/test_taau_env_lookup.py": ["--cxx", "g++"],
"tests/test_target_overlap.py": ["--cxx", "g++"],
"tests/test_menu_hit.py": ["--cxx", "g++"],
"tests/test_output_resolution.py": ["--cxx", "{cxx}"],
"tests/test_nvn_buffer_map.py": ["--cc", "gcc"],
"tests/test_audio_voice_biquad.py": ["--cc", "gcc"],
"tests/test_cpp_preflight.py": ["--cxx", "g++"],
Expand Down
84 changes: 84 additions & 0 deletions tests/test_output_resolution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Exercise the host and menu's docked output sizes without a game dump."""
import argparse
import os
import subprocess
import tempfile
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--cxx", default="g++")
args = parser.parse_args()

host = (ROOT / "host/nn_applet.c").read_text(encoding="utf-8")
menu = (ROOT / "gpu/vk/vk_menu.cpp").read_text(encoding="utf-8")
fixture = r'''
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iterator>

using DWORD = unsigned long;
constexpr int MAX_PATH = 260;
DWORD hos_game_path_a(char *path, int size) { return snprintf(path, size, "./hookshot.exe"); }
void hos_log(const char *, ...) {}
struct { int handheld, output_res; } opts;
''' + host[host.index("static int ini_option("):host.index("HLE(_ZN2nn2oe16GetOperationModeEv)")] + menu[
menu.index("struct Out {"):menu.index("const char* tx_speed(")] + r'''
int main(int argc, char **argv) {
assert(argc == 5);
const int mode = atoi(argv[1]);
const unsigned w = atoi(argv[2]), h = atoi(argv[3]);
assert(output_mode() == mode);
assert(hos_output_size(0) == w && hos_output_size(1) == h);
assert(hos_rt_pool_size(305ull << 20) == ((mode == 2 ? 687ull : 305ull) << 20));
assert(kLastOutput == 3);
assert(output_choice(-1) == 0 && output_choice(4) == 0);
assert(step_output_choice(3, -1) == 3);
assert(step_output_choice(3, 1) == 0 && step_output_choice(0, -1) == 3);
assert(step_output_choice(0, 1) == 1 && step_output_choice(1, -1) == 0);
assert(step_output_choice(1, 1) == 2 && step_output_choice(2, -1) == 1);
assert(step_output_choice(2, 1) == 2);
opts.output_res = mode;
opts.handheld = handheld();
assert(strcmp(tx_output_res(mode), argv[4]) == 0);
unsigned scene_w, scene_h;
game_size(1.0f, scene_w, scene_h);
assert(scene_w == (opts.handheld ? 1536 : mode ? w : 2048));
assert(scene_h == (opts.handheld ? 864 : mode ? h : 1152));
if (mode == 3) assert(strcmp(tx_resolution(2), "75% (1440x810)") == 0);
}
'''
cases = (
(0, 0, None, 0, 2560, 1440, "2048x1152 (game, restart)"),
(1, 0, None, 1, 2560, 1440, "2560x1440 (restart)"),
(2, 0, None, 2, 3840, 2160, "3840x2160 (restart)"),
(3, 0, None, 3, 1920, 1080, "1920x1080 (restart)"),
(1, 0, "3", 3, 1920, 1080, "1920x1080 (restart)"),
(3, 1, "3", 0, 2560, 1440, "2048x1152 (game, restart)"),
(4, 0, None, 0, 2560, 1440, "2048x1152 (game, restart)"),
)
with tempfile.TemporaryDirectory(prefix="output-resolution-") as tmp:
work = Path(tmp)
source, exe = work / "fixture.cpp", work / "fixture.exe"
source.write_text(fixture, encoding="utf-8")
subprocess.run([args.cxx, "-std=c++17", str(source), "-o", str(exe)], check=True)
for saved, handheld, override, mode, w, h, label in cases:
(work / "hookshot_options.ini").write_text(f"output_res={saved}\nhandheld_profile={handheld}\n", encoding="ascii")
env = os.environ.copy()
for key in ("HOS_OUTPUT_RES", "HOS_HANDHELD", "HOS_RT_POOL_MB"):
env.pop(key, None)
if override is not None:
env["HOS_OUTPUT_RES"] = override
subprocess.run([str(exe), str(mode), str(w), str(h), label], cwd=work, env=env, check=True)
print("PASS: output sizes, menu order, 1080p override, handheld and render-target budgets")


if __name__ == "__main__":
main()