Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5159b55
Program cache: bounds-check records, one instance per cache file
xellow78-lgtm Nov 7, 2026
ca12896
Merge NotRexed/hookshot upstream (c02f290) into fix/program-cache-crash
xellow78-lgtm Nov 7, 2026
1f346f1
Hookshot Launcher: update the build tools from GitHub and rebuild in …
xellow78-lgtm Nov 7, 2026
f1a9f20
Merge NotRexed/hookshot main (871dc6e)
xellow78-lgtm Nov 7, 2026
e74396e
Merge branch 'NotRexed:main' into main
xellow78-lgtm Nov 7, 2026
5bfcd4b
Merge NotRexed/hookshot main (628ac04)
claude Nov 7, 2026
0935acb
Mouse: log each step of the mouse path
xellow78-lgtm Nov 7, 2026
1ab76c7
Merge fork main (Sync fork at 14:30)
xellow78-lgtm Nov 7, 2026
23dcca3
Launcher v2: Update asks GitHub for the newest commit when it starts
xellow78-lgtm Nov 7, 2026
1e1ba97
Merge branch 'NotRexed:main' into main
xellow78-lgtm Nov 7, 2026
3df95e2
Camera finder (HOS_CAMFIND=1, F6): find the camera's yaw and pitch in…
xellow78-lgtm Nov 7, 2026
8250c6f
Camera finder v2: turn on, then back; the axes check each other; ever…
xellow78-lgtm Nov 7, 2026
c7bad56
Native mouse look (HOS_MOUSE_NATIVE=1): the mouse turns the camera's …
xellow78-lgtm Nov 7, 2026
b8b4171
Merge branch 'NotRexed:main' into main
xellow78-lgtm Nov 7, 2026
e46f18a
Native mouse look on by default; up and down the right way; no hitch
xellow78-lgtm Nov 7, 2026
12dcf15
Merge branch 'NotRexed:main' into main
xellow78-lgtm Nov 7, 2026
5f28a81
The menu remade: tabs, mouse and controller; Camera & Mouse settings
xellow78-lgtm Nov 7, 2026
3ef68e9
Merge branch 'NotRexed:main' into main
xellow78-lgtm Nov 7, 2026
23c48b7
Controls in the menu: every key, mouse button and pad button can be c…
xellow78-lgtm Nov 7, 2026
6413197
Merge NotRexed/hookshot main: one set of controls, the fork's menu on…
xellow78-lgtm Nov 8, 2026
2f984cc
Merge NotRexed/hookshot main: text speed and quality presets in the t…
xellow78-lgtm Nov 8, 2026
07db9a4
Merge NotRexed/hookshot main
xellow78-lgtm Nov 8, 2026
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
12 changes: 10 additions & 2 deletions gpu/vk/vk_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ enum { HOS_PAD_SOUTH, HOS_PAD_EAST, HOS_PAD_WEST, HOS_PAD_NORTH, HOS_PAD_BACK, H
HOS_PAD_LSTICK, HOS_PAD_RSTICK, HOS_PAD_LSHOULDER, HOS_PAD_RSHOULDER, HOS_PAD_UP, HOS_PAD_DOWN, HOS_PAD_LEFT,
HOS_PAD_RIGHT, HOS_PAD_LTRIGGER, HOS_PAD_RTRIGGER, HOS_PAD_N };
enum { HOS_BIND_PAD, HOS_BIND_KEY };
/* (hookshot) keys only (HOS_BIND_KEY): the left stick and the camera from the keyboard, after the Switch's buttons */
enum { HOS_KEY_MOVE_UP = HOS_SW_N, HOS_KEY_MOVE_DOWN, HOS_KEY_MOVE_LEFT, HOS_KEY_MOVE_RIGHT, HOS_KEY_CAM_UP,
HOS_KEY_CAM_DOWN, HOS_KEY_CAM_LEFT, HOS_KEY_CAM_RIGHT, HOS_KEY_N };

/* what the executable gives the back end */
typedef struct MkwVkHost {
Expand Down Expand Up @@ -73,7 +76,8 @@ typedef struct MkwVkHost {
uint32_t (*pad_raw)(void);
/* (hookshot) the controls, for the Insert menu's Controls page. button: the Switch's, HOS_SW_*; set HOS_BIND_PAD:
the gamepad input that presses it (a HOS_PAD_* position, -1 none; slot 0 only); set HOS_BIND_KEY: the keys that
do (Windows key codes, 0 none; slots 0 and 1). bind with button -1: all back to their defaults */
do (Windows key codes, 0 none; slots 0 and 1; a mouse button's too, and HOS_KEY_* past the Switch's buttons).
bind with button -1: all back to their defaults; -2: only the set's (the gamepad's or the keys') */
int (*binding)(int set, int button, int slot);
void (*bind)(int set, int button, int slot, int code);
} MkwVkHost;
Expand Down Expand Up @@ -113,9 +117,13 @@ typedef struct MkwVkRenderer {
void (*nvn_texture_created)(const nvn_tex *t);
/* the CPU set an event's value: in order with the submits (the GPU's waits on it) */
void (*nvn_event_signal)(uint64_t gpu, uint32_t value);
int (*option)(int id); /* (hookshot, the Insert menu) 0: game time follows the clock, 1: the menu is open, 2: dynamic resolution, 3: its fixed scale in percent, 4: the controllers count behind other windows, 5: text speed (0 normal to 3 instant) */
int (*option)(int id); /* (hookshot, the Insert menu) 0: game time follows the clock, 1: the menu is open, 2: dynamic resolution, 3: its fixed scale in percent, 4: the controllers count behind other windows, 5: text speed (0 normal to 3 instant),
20: the mouse's sensitivity in percent, 21: its vertical inverted, 22: native mouse look (host/nn_hid.c, mouse_native.c) */
/* a submit with its queue */
void (*nvn_submit_q)(const uint64_t *handles, int n, uint64_t queue);
/* (hookshot, the menu) from the game's side: 20 the mouse's sensitivity in percent (the - and = keys; kept in the file),
29 the mouse wheel while the menu is open (host/display.c, in WHEEL_DELTA units) */
void (*option_set)(int id, int value);
} MkwVkRenderer;

/* the DLL's one export: null when Vulkan is not usable (the executable then falls back to WebGPU) */
Expand Down
875 changes: 562 additions & 313 deletions gpu/vk/vk_menu.cpp

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions gpu/vk/vk_menu.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,17 @@ struct Options {
int preset = 0; // quality preset: 0 Custom, 1 Minimum (4GB), 2 Low (6GB), 3 Balanced (8GB), 4 High (12GB), 5 Ultra (16GB+)
int vram_mb = 0; // dynamic texture cache budget limit in MB
int mirror_mb = 0; // staging mirror limit in MB
int mouse_native = 1; // the mouse turns the camera's own angles (host/mouse_native.c); 0: as the right stick
int mouse_sens = 100; // percent: 0.1 degrees a count at 100 (host/mouse_native.c)
int mouse_invert = 0; // up and down swapped
// (the controls are the host's: host/nn_hid.c keeps them, the menu shows and changes them through host->binding and bind)
};
extern Options opts;

void menu_init(); // (the options from the file, once)
void menu_tick(); // (each frame shown: the keys, while the game's window has the focus)
bool menu_is_open();
void menu_option_set(int id, int value); // (from the game's side: 20 the mouse's sensitivity, 29 the wheel)
void menu_note_frame(bool game_frame); // (the frame rates the FPS counter shows)
/// Over the window's image (TRANSFER_DST_OPTIMAL), w x h: the menu and the FPS counter. slot: the frame in flight, 0 to 2.
void menu_record(VkCommandBuffer cmd, VkImage dst, int slot, uint32_t w, uint32_t h);
Expand Down
4 changes: 3 additions & 1 deletion gpu/vk/vk_replay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3134,7 +3134,9 @@ void fill_replay_table(MkwVkRenderer& t) {
t.nvn_present = present;
t.nvn_register_texture = register_texture_queued;
t.nvn_event_signal = event_signal_queued;
t.option = [](int id) -> int { return id == 0 ? opts.realtime : id == 1 ? menu_is_open() : id == 2 ? opts.resolution == 0 : id == 3 ? (opts.resolution == 1 ? 100 : opts.resolution == 2 ? 75 : opts.resolution == 3 ? 67 : opts.resolution == 4 ? 50 : 0) : id == 4 ? opts.pad_background : id == 5 ? opts.text_speed : 0; };
t.option = [](int id) -> int { return id == 0 ? opts.realtime : id == 1 ? menu_is_open() : id == 2 ? opts.resolution == 0 : id == 3 ? (opts.resolution == 1 ? 100 : opts.resolution == 2 ? 75 : opts.resolution == 3 ? 67 : opts.resolution == 4 ? 50 : 0) : id == 4 ? opts.pad_background : id == 5 ? opts.text_speed :
id == 20 ? opts.mouse_sens : id == 21 ? opts.mouse_invert : id == 22 ? opts.mouse_native : 0; };
t.option_set = menu_option_set;
t.nvn_register_sampler = register_sampler_queued;
t.nvn_set_texture_pool_size = set_texture_pool_size;
t.nvn_texture_released = texture_released_of;
Expand Down
77 changes: 65 additions & 12 deletions gpu/vk/vk_shader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@
// A program's stages are emitted with one Backend::Bindings, so their descriptors share one set in Eden's order
// (per stage: uniform buffers, storage buffers, texture buffers, image buffers, textures, images), which
// vk_pipeline.cpp lays out from the stages' Info the same way.
#ifndef _WIN32
#include <fcntl.h>
#include <sys/file.h>
#endif
#include <io.h>
#include <algorithm>
#include <cctype>
#include <chrono>
#include <stdexcept>
#include <cstdio>
Expand Down Expand Up @@ -828,6 +833,26 @@ void disk_init() {
const DWORD n = GetModuleFileNameA(nullptr, exe, MAX_PATH);
std::string path(exe, n);
path = path.substr(0, path.find_last_of("\\/") + 1) + "hookshot_programs.bin";
// (one Hookshot at a time on a cache: a second one, started while the first wrote, took the record being written for one
// cut short and cut the file there, under the first, whose list of records then named bytes written over since. Held
// until the process ends)
{
uint64_t h = 0xCBF29CE484222325ull;
for (const char ch : path) h = (h ^ (uint8_t)tolower((unsigned char)ch)) * 1099511628211ull;
char name[64];
snprintf(name, sizeof name, "Local\\HookshotProgramCache_%016llx", (unsigned long long)h);
#ifdef _WIN32
const HANDLE held = CreateMutexA(nullptr, FALSE, name);
const bool taken = !held || GetLastError() == ERROR_ALREADY_EXISTS;
#else // (linux: a lock on a file beside the cache, let go when the process ends)
const int fd = open((path + ".lock").c_str(), O_CREAT | O_RDWR | O_CLOEXEC, 0644);
const bool taken = fd >= 0 && flock(fd, LOCK_EX | LOCK_NB) != 0;
#endif
if (taken) {
logf("vk: program cache %s: another Hookshot is using it: this one runs without it", path.c_str());
return;
}
}
const uint64_t stamp = disk_stamp();
int64_t good = 0, whole = 0;
if (FILE* f = fopen(path.c_str(), "rb")) {
Expand Down Expand Up @@ -878,18 +903,35 @@ template <class V> void put_list(std::vector<uint8_t>& o, const V& v) {
put(o, (uint64_t)v.size());
for (const auto& e : v) put(o, e);
}
// (a record is read within its own bytes: one made wrong, a count that ran past them, read on into the heap until it met an
// unmapped page, a crash in program_cache_load. Past the end, bad is set, the gets give zeros, and the record is left out)
struct Reader {
const uint8_t* p;
const uint8_t* end;
bool bad = false;
bool has(uint64_t n) {
if (!bad && n > (uint64_t)(end - p)) bad = true;
return !bad;
}
template <class T> T get() {
alignas(T) uint8_t b[sizeof(T)];
std::memcpy(b, p, sizeof(T));
p += sizeof(T);
alignas(T) uint8_t b[sizeof(T)]{};
if (has(sizeof(T))) {
std::memcpy(b, p, sizeof(T));
p += sizeof(T);
}
return *(T*)b;
}
template <class V> void list(V& v) {
/// a count of elements of size elem, no more than the bytes left could hold (0 when bad)
uint64_t count(size_t elem) {
const uint64_t n = get<uint64_t>();
if (!bad && n > (uint64_t)(end - p) / (elem ? elem : 1)) bad = true;
return bad ? 0 : n;
}
template <class V> void list(V& v) {
const uint64_t n = count(sizeof(typename V::value_type));
v.clear();
for (uint64_t i = 0; i < n; i++) v.push_back(get<typename V::value_type>());
v.reserve((size_t)n);
for (uint64_t i = 0; i < n && !bad; i++) v.push_back(get<typename V::value_type>());
}
};

Expand All @@ -909,6 +951,7 @@ void put_info(std::vector<uint8_t>& o, const Shader::Info& i) {
for (const auto& h : i.handle_programs) put_list(o, h);
}
void get_info(Reader& r, Shader::Info& i) {
if (!r.has(sizeof i)) return; // (left as constructed: the record is bad, and left out)
alignas(Shader::Info) static thread_local uint8_t fresh[sizeof(Shader::Info)];
std::memcpy(fresh, (const void*)&i, sizeof i); // (those members as constructed, here)
std::memcpy((void*)&i, r.p, sizeof i);
Expand All @@ -929,11 +972,11 @@ void get_info(Reader& r, Shader::Info& i) {
r.list(i.image_buffer_descriptors);
r.list(i.texture_descriptors);
r.list(i.image_descriptors);
for (uint64_t n = r.get<uint64_t>(); n; n--) {
for (uint64_t n = r.count(2 * sizeof(Shader::IR::Attribute)); n && !r.bad; n--) {
const auto k = r.get<Shader::IR::Attribute>();
i.legacy_stores_mapping[k] = r.get<Shader::IR::Attribute>();
}
i.handle_programs.resize((size_t)r.get<uint64_t>());
i.handle_programs.resize((size_t)r.count(sizeof(uint64_t))); // (each at least its own count)
for (auto& h : i.handle_programs) r.list(h);
}

Expand Down Expand Up @@ -985,7 +1028,7 @@ void program_cache_load(uint64_t key, std::vector<std::unique_ptr<Program>>& var
_fseeki64(disk_in, d.at, SEEK_SET);
if (fread(data.data(), 1, d.size, disk_in) != d.size) continue;
}
Reader r{data.data()};
Reader r{data.data(), data.data() + data.size()};
auto p = std::make_unique<Program>();
p->key = key;
const uint32_t kind = r.get<uint32_t>();
Expand All @@ -994,9 +1037,9 @@ void program_cache_load(uint64_t key, std::vector<std::unique_ptr<Program>>& var
p->variant = answers_hash(*p);
p->workgroup = r.get<std::array<uint32_t, 3>>();
p->shared_memory = r.get<uint32_t>();
const uint64_t n = r.get<uint64_t>();
bool ok = true;
for (uint64_t k = 0; k < n; k++) {
const uint64_t n = r.count(1);
bool ok = !r.bad && n <= 8; // (a program has at most a few stages)
for (uint64_t k = 0; ok && k < n; k++) {
p->stages.emplace_back();
Stage& st = p->stages.back();
st.nvn_stage = r.get<int>();
Expand All @@ -1005,10 +1048,20 @@ void program_cache_load(uint64_t key, std::vector<std::unique_ptr<Program>>& var
r.list(spirv);
r.list(st.c1);
get_info(r, st.info);
if (r.bad || spirv.empty()) {
ok = false;
break;
}
st.module = make_module(spirv);
ok = ok && st.module;
}
if (!ok) continue;
if (!ok) {
static int logged;
if (r.bad && logged++ < 20)
logf("vk: program cache: the record of program %016llx at %lld (%zu bytes) is not whole: left out, translated again",
(unsigned long long)key, (long long)d.at, d.size);
continue;
}
p->ready.store(true, std::memory_order_release);
variants.push_back(std::move(p));
st_cache_hits++;
Expand Down
Loading