Skip to content

feat(menu): add compact TV-style tuning bar for live setting tweaks - #2378

Merged
hckrman101 merged 2 commits into
NotRexed:mainfrom
B16B0SS:feat/menu-tuning-bar
Nov 11, 2026
Merged

hckrman101 merged 2 commits into
NotRexed:mainfrom
B16B0SS:feat/menu-tuning-bar

Conversation

@B16B0SS

@B16B0SS B16B0SS commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

Why is this PR needed?

The full settings menu covers ~80% of the screen, making it difficult to gauge real-time visual changes (sharpness, resolution scaling, upscaler presets) during live gameplay. This PR introduces a compact TV-style tuning bar at the bottom of the screen (~10% height) so settings can be adjusted with over 90% of the game scene visible. It can be invoked from the normal settings menu or as a special keybind directly from the game.

What changes?
Compact Tuning Bar (draw_tune_bar): Renders a sleek bottom card showing the current tab, active setting row, value editor, and navigation hints without full-screen dimming.

Two Entry / Exit Paths:
Option A (In-menu toggle): Pressing the configured modifier key (Right Shift by default) or controller X/Square switches smoothly between the full menu and the tuning bar.
Option B (Direct game shortcut): Holding the configured key while pressing Insert or F1 jumps directly from the game into the compact tuning bar; Esc / B closes straight back to the game.

Controls Configuration (Alt menu shortcut): Adds an entry under the Controls tab to choose between Right Shift (default) and Right Alt, leaving left-hand gaming keys (WASD, sprint/roll) completely free from interference.

Deduplicated Rendering (row_value_cell): Sliders, toggles, choices, and subpage links share a single unified cell renderer between the full menu and tuning bar, preventing double maintenance when upstream adds new options.

Dynamic Prompts: Menu prompt bar shows [RShift] Tune or [RAlt] Tune (and controller West button dot) dynamically.

New Test: Added tests/test_menu_tune.py covering Option A/B triggers, prompt glyphs, and non-dimmed drawing.

@B16B0SS

B16B0SS commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author
2026-11-11T12:37:40,197050645-04:00 2026-11-11T12:38:51,784596023-04:00 2026-11-11T12:43:53,514887464-04:00

@hckrman101 hckrman101 left a comment

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, a bar you can tune with while still seeing the picture is a nice idea, and pulling the value cell out into row_value_cell keeps the two views from drifting. The screenshots in the thread help. The settings key is saved by row name (alt_menu_shortcut= in hookshot_options.ini), so no option id is used and nothing collides with main or the open menu PRs (#2177, #2304, #2197, #2119 touch other hunks). When the menu is closed the new code only drains the menu's own key queue, so game input isn't touched.

A few things to fix before it goes in (details inline):

  • Right Shift + Tab toggles the bar. Shift+Tab is the existing "previous tab" key. With Right Shift the Shift press arrives a frame or more before Tab, so !press[12] doesn't catch it and the menu flips into (or out of) tune mode before the tab changes.
  • "Right Alt" can't toggle on Windows. The queued presses come from WM_KEYDOWN in host/display.c, but a plain Alt press arrives as WM_SYSKEYDOWN, which isn't recorded. So take_key_press(VK_RMENU/VK_MENU) never fires there. Holding it with Insert still works (that path uses GetAsyncKeyState). Linux is fine, because display_sdl.c maps both keys.
  • The dim is dropped for every page while tune_mode is set, not only for the bar. Open a page from the bar (Controls, Teleport, Inventory) or get the restart question, and the full-screen page draws straight over the undimmed game.
  • The full menu's Debug-tab prompts lose their confirm entry for tools that both change and confirm (tools 0, 1 and 7 "Toggle", 4 "Set", 6 "Apply", 9 "Switch"). Before this PR the prompts showed Change and the confirm action together.

Nits:

  • The PR says X/Square, but pad(HOS_PAD_NORTH) toggles the bar too, and the glyph only lights the West dot.
  • row_can_tune and menu_is_tuning() are never used.
  • On the Motion page the bar's header reads "Settings" (tab_title gets TAB_MOTION).
  • "Alt menu shortcut" reads like a second key for opening the menu. Something like "Tuning bar key" says what the row does.
  • The new test only greps for exact source lines, so it pins the wording and doesn't test behaviour. That's fine, but a small compiled check of the toggle and the dim would catch the issues above.

CI hasn't run yet: the workflows are waiting for approval (action_required). The static checks I looked at that cut code out of vk_menu.cpp (test_menu_keys' key-poll block, test_audio_volume's tab_titles regex, the fps tests) still match the new layout, but this needs a real run. Since this adds a new UI mode and a new keybind, whether it belongs in the menu is NotRexed's call.

Comment thread gpu/vk/vk_menu.cpp Outdated
// values change by a click on their arrows or halves, a slider's drag, Left and Right, A and Enter)
if (ptr.wheel && drag_row < 0) wheel_rows[cur_tab] += ptr.wheel, changed = true;
}
const bool tune_toggle = (tune_press && !press[12]) || pad(HOS_PAD_WEST) || pad(HOS_PAD_NORTH);

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 Right Shift as the key, Shift+Tab (previous tab) toggles tune mode: the Shift press lands a frame or more before Tab, so press[12] is still false when tune_press fires. You could fire on the release of the key when nothing else was pressed while it was held, or skip the toggle while Tab is down or was pressed during the hold. Also, HOS_PAD_NORTH toggles too, but the description and the glyph only mention X/Square (West).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

change to release fire

Comment thread gpu/vk/vk_menu.cpp Outdated
bool tune_queued = false;
if (host && host->take_key_press) {
tune_queued = host->take_key_press(tune_key);
if (!tune_queued) tune_queued = host->take_key_press(tune_parent_key);

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.

On Windows, take_key_press only sees keys noted from WM_KEYDOWN (host/display.c). A plain Alt press comes as WM_SYSKEYDOWN, so with "Right Alt" chosen this never returns true and the in-menu toggle does nothing. Noting VK_MENU in the WM_SYSKEYDOWN case would fix that, or the tune key could be edge-detected from GetAsyncKeyState like the Linux path. (Releasing a lone Alt also sends SC_KEYMENU to the window, which is worth a quick check in the game.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

noted, and switched to right ctrl just to avoid the entire issue

Comment thread gpu/vk/vk_menu.cpp Outdated
/// rectangles are moved over after, and menu_tick moves the mouse back by menu_ox before it tests the hits
void draw_menu(ui::Ctx& c) {
c.d->AddRectFilled(ImVec2(0, 0), ImVec2(c.w, c.h), ui::rgba(8, 5, 2, ui::kDimAlpha)); // (the game behind, dimmed)
if (!tune_mode) c.d->AddRectFilled(ImVec2(0, 0), ImVec2(c.w, c.h), ui::rgba(8, 5, 2, ui::kDimAlpha)); // (the game behind, dimmed)

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.

This skips the dim for every page while tune_mode is set. From the bar, Enter on Controls opens page 1, Teleport and Inventory open pages 3 and 4, and closing with a restart-needed change gives page 5. All of those then draw full-screen over the undimmed game. if (!(tune_mode && page == 0)) would keep the dim for them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

got it - adjusted to check for page zero to add the dim on secondary screens executed through the compact menu

Comment thread gpu/vk/vk_menu.cpp
ui::Prompt prompt_list[7] = {{ui::kUpDown, "Choose"}};
int prompts_n = 1;
const char* tune_key_name = opts.alt_menu_shortcut == 1 ? "RAlt" : "RShift";
if (selr.value || (selr.cheat >= 0 && (selr.cheat <= 1 || selr.cheat == 7 || cheat_chooses(selr.cheat) || age_shown(selr.cheat)))) {

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.

Small regression: before, a Debug tool that both changes and confirms (cheats 0, 1, 4, 6, 7, 9: "Toggle", "Set", "Apply", "Switch") showed Change and its confirm prompt. This branch shows only Change. Adding if (selr.cheat >= 0) if (const char* says = cheat_confirm(selr.cheat)) prompt_list[prompts_n++] = {ui::kConfirm, says}; here would keep it (there's room in the 7).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I interpreted "prompt" as "hint" here , I think that is what we are looking for. I have added this back in

@B16B0SS

B16B0SS commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author
hookshotminimenu_small.mp4

@B16B0SS

B16B0SS commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

Hey - made the changes you requested - sidestepped right alt and chose right ctrl. Otherwise took your suggestions and pulled head back into so its ready to merge should you like the work

oh and went with "Compact menu"

@hckrman101 hckrman101 left a comment

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 for the quick turnaround, checked the new head against my last review.

Fixed:

  • Shift+Tab: the toggle now fires on the key's release and is cancelled if Tab (or a tab/arrow/Enter/Esc press or a click) happens while it's held, so previous-tab no longer flips the bar. North no longer toggles, only West, which matches the glyph.
  • Windows Alt: sidestepped by switching to Right Ctrl. GetAsyncKeyState(VK_RCONTROL) works on both Windows and the Linux key table (display_sdl.c fills VK_RCONTROL), so the in-menu toggle works on both.
  • Dimming: !(tune_mode && page == 0) keeps the dim for Controls, Teleport, Inventory and the restart question.
  • Debug prompts: the confirm prompt (Toggle/Set/Apply/Switch) is back next to Change.
  • Nits: row_can_tune is gone, the bar says "Motion" on the Motion page, and "Compact menu key" is a clear row name.

New, small:

  • row_value_cell has a branch the old full-menu code didn't: Debug tools with no value but a confirm (Apply x2, Open list, Switch age, Open) now show that word in their value cell instead of the ▸ arrow, in the full menu too (inline). Fine if intended, but it changes the Debug tab's look outside the bar, so worth a line in the description.
  • Nit: in the bar, Restart and Quit get an "Open" prompt (!r.value && r.cheat < 0), where the full menu shows the row's name / "Confirm".
  • Nit: the test's fixture compiles copies of the toggle and dim logic rather than the code in vk_menu.cpp, so it would still pass if menu_tick drifted. Cutting the real snippet out (as other menu tests do) would make it bite.

CI: the build and test workflows (Windows builder, game-free source regressions, Linux AppImage) are waiting for a maintainer to approve the run, so nothing beyond the title checks has run yet. No conflicts with main.

The bugs I raised are fixed. Whether this UI mode belongs in the menu is still NotRexed's call, so I'm leaving the decision with them.

Comment thread gpu/vk/vk_menu.cpp Outdated
halves_cell(c, va, vb, "Off", "On", tool == "On", selected, false);
} else if (r.cheat >= 0 && !tool.empty()) {
choice_cell(c, va, vb, tool, selected, cheat_chooses(r.cheat), cheat_chooses(r.cheat));
} else if (r.cheat >= 0 && cheat_confirm(r.cheat)) {

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.

This branch is new compared with the old inline code: before, a Debug tool with an empty value fell through to !r.value and drew the ▸ arrow; now cheats 2, 3, 8, 9 and 10 show "Apply" / "Open list" / "Switch" / "Open" as their value in the full menu as well. Fine if that's the intent, just flagging that it changes the full Debug tab too.

@B16B0SS
B16B0SS force-pushed the feat/menu-tuning-bar branch from 7aed804 to c44fbb6 Compare November 11, 2026 19:47
@B16B0SS

B16B0SS commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback an kind words. Here is what I have for you
1 - restart/quit - fixed so that on first selection the prompt says the action (enter or quit) and on enter you get "confirm", just like the F1 menu
2 - updated the test to use real code so it actually is a test
3 - unified the button labels under a single cheat_button_text() method so both the full menu and compact bar pull from the same table

let me know if there is anyhting else

@hckrman101 hckrman101 left a comment

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 for the fast follow-up, checked 7aed804 against c44fbb6 (the head was rebased onto main, so I compared the patches).

Fixed since my last review:

  • The test now cuts the real toggle and dim code out of vk_menu.cpp, so it would catch drift in menu_tick.
  • Restart and Quit in the bar: the hint shows the row's name first and "Confirm" on the pending press, like the full menu.
  • Debug button labels: cheat_button_text() is now the one table both the full menu and row_value_cell read, and the full Debug tab draws the same words as before this PR (Mark, Return, Choose map, Switch, Edit items). That also settles my earlier note about the changed Debug look, so nothing needs adding to the description.

Still open (small):

  • The new test is not declared for tests/run_all.py. It compiles a fixture, but has no RUN_ALL_ARGS = ["--cxx", "{cxx}"] (see test_menu_keys.py), so it always uses g++, which the llvm-mingw Windows runner may not have. Please add the line; it already takes --cxx.

Not blocking: the settings key is saved by row name (no option id), so no collision. The three build and test workflows are still waiting for a maintainer to approve the run (action_required), so none of the source checks have run on this head. I read the other tests that cut code from vk_menu.cpp (test_menu_keys, test_audio_volume's tab_titles) against the new layout and they still match, but a real run is needed.

Whether this UI mode and keybind belong in the menu is still NotRexed's call, so I am leaving the decision with them.

Comment thread tests/test_menu_tune.py
import tempfile
from pathlib import Path

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

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.

Add RUN_ALL_ARGS = ["--cxx", "{cxx}"] here (as test_menu_keys.py does) so run_all passes the CI's compiler instead of the default g++.

@hckrman101

Copy link
Copy Markdown
Collaborator

Fixing this myself^

@hckrman101 hckrman101 left a comment

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.

I pushed one small commit on top: tests/test_menu_tune.py now declares RUN_ALL_ARGS = ["--cxx", "{cxx}"], so run_all passes the platform compiler instead of falling back to g++ (it passes locally). That was the last open point; everything from the earlier rounds is fixed. Looks good to me, thanks for iterating on this.

CI still needs a maintainer to approve the workflow runs before it goes in.

@hckrman101
hckrman101 merged commit 6c8c1bf into NotRexed:main Nov 11, 2026
1 check passed
pmwork-personal added a commit to pmwork-personal/Hookshot-pr that referenced this pull request Nov 11, 2026
# Conflicts:
#	gpu/vk/vk_menu.h

vk_menu.h: menu_shortcut stays 2 beside main's new compact_menu_key (NotRexed#2378).
tests/test_menu_keys.py: the fixture stubs tune_mode and tune_down, which
menu_tick's Esc block now sets (NotRexed#2378), and has_vram_budget and
has_memory_budget, which menu_init's options loader now reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pmwork-personal added a commit to pmwork-personal/Hookshot-pr that referenced this pull request Nov 11, 2026
… and doc fixes

A self-review of the PR's diff found that the test checked only versions
2 to 4: a later bump of save()'s version without menu_init's threshold
would still pass. It now loads every version below the one save() writes.

- tests/test_menu_keys.py: every older options_version, and Esc with the
  compact menu key held opens the compact bar, as Insert and F1 do (NotRexed#2378).
- readme.md: Esc goes back or closes with every Menu shortcut; the
  choices cell uses semicolons, as one name has a comma.
- docs/development.md: save() writes options_version=5.
- vk_menu.cpp: the Esc comment names RX_VK_MENU_SCRIPT's ESC and the
  compact bar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hckrman101 added a commit to hckrman101/Hookshot that referenced this pull request Nov 11, 2026
…ction

Merge leftover from NotRexed#2378: the bar's value cell drew the row as a page (an empty cell and an
arrow) and its prompt said Open. It shows Save (then Saved or Not saved) and says Save.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hckrman101 pushed a commit that referenced this pull request Nov 12, 2026
…ess (#2347)

**What this is for:** a player who meets a problem presses one row in
the Insert menu, **Gameplay → Bug report**, and gets a folder with
everything the game problem form asks for: the form's answers in
`report.txt` (version, the map he is in, graphics card and driver
version, CPU, RAM and system, the options changed and the switches set),
this run's log and the run before's (the crashed one after a restart)
with his home folder and user name taken out, and a picture of the game.
Maintainers get reports with the log and the hardware from the start
instead of asking for them. **What is measured:** on Linux (Omarchy, an
Arch Linux; RTX 4090, NVIDIA 615.78.08), on `main` `ba6eef70`: the new
test passes with g++ 16 and clang 23 and fails on `main`; its cases for
the review's points (NUL bytes in a log, a picture that comes late,
which switch values are written, the reason a folder cannot be made);
`tests/run_all.py`; headless runs of this head that pressed the row (one
pressed it in Kokiri Forest, one after a `kill -9` and a restart: both
reports written, read back by a script against the logs they came from
(no file holds the home folder or the user name; each log copy is the
original line for line apart from those two), the after-crash report's
`hookshot_log.1.txt` is the killed start's whole log, and its picture
came through #926's `shot_ask`); the length GitHub takes for the link,
signed out. **Not measured:** Windows at run time (compiled and linked
only), a windowed run, the link opened in a browser (signed out or in),
AMD and Intel drivers' strings, a Steam Deck.

Refs #2346 (closed by the maintainer: tracked in this pull request).

## Problem

- The game problem form (`.github/ISSUE_TEMPLATE/1-game-problem.yml`)
requires the version, where in the game, the graphics card **and driver
version**, CPU, RAM and operating system, and `hookshot_log.txt` (and
`hookshot_log.1.txt` "if you started the game again after the problem").
The player has to find each one by hand.
- `hookshot_log.txt` answers only part of it: its first line has the
version, but the driver is only Vulkan's packed number (`vk: device 0:
NVIDIA GeForce RTX 4090, Vulkan 1.4, driver 0x99d38200, type 2`), there
is no line for the system, the CPU or the options changed, and nothing
logged where the player was (the host reads `Sequence_CurrentBanc` only
for the debug tools).
- After a crash a player starts the game again, and the crashed run's
log becomes `hookshot_log.1.txt` (`host/hos_main.c`, the rotation added
because players sent the new run's log and the crash was gone). It still
has to be found and attached.
- The logs carry the home folder and the user name in their paths (`fs:
mounted content: -> <home>/...`, `vk: pipeline cache <home>/...`), which
a player may not want to post.
- So reports arrive without the log or the hardware, and
`docs/workflow.md` scores such a report at confidence 0.4 and asks for
the log first. This gathers what CONTRIBUTING.md already asks ("Include
the version, your hardware, where you were and `hookshot_log.txt`";
"Report the version you actually ran: ... the first line of
`hookshot_log.txt`"). It changes nothing about where reports go.

## What changed

- `gpu/vk/vk_bug_report.h`, `vk_bug_report.cpp` (new, 59 + 602 lines):
the packer, without Vulkan and without the menu, so the test compiles it
alone. It writes `bug-reports/<YYYY-MM-DD_HH-MM-SS>/` in the game folder
on a thread of its own:
- `report.txt`: the form's questions under the form's own `### `
headings, so `scripts/hardware_report.py`'s `section()` reads it as an
issue: **Hookshot version** (`HOOKSHOT_VERSION`, as the log's first
line); **Where in the game** (the last `map:` line of this run's log,
and the run before's, and a request to add what you were doing);
**Graphics card and driver version** (the device and
`VkPhysicalDeviceDriverProperties`' `driverName` + `driverInfo`, e.g.
`NVIDIA GeForce RTX 4090, NVIDIA 615.78.08`); **CPU, RAM and operating
system** (the CPU's brand string and threads, RAM, `os-release`, desktop
and session type on Linux; `RtlGetVersion` and Wine on Windows);
**Changes from the defaults** (each Insert-menu row not at its default
as the menu shows it; a microphone chosen by its name is not named; then
the switches set, below); **hookshot_log.txt** (what is attached, the
run before's version and date, each log's first `FATAL`, `CRASH` or
`device lost` line); **Screenshots or video**; then a link that opens
the form with version, where, GPU, system and settings filled in (GitHub
fills issue-form fields from query parameters named by the fields' ids).
- **The switches.** CONTRIBUTING.md lists `hookshot_env.txt` among the
files never to share, so the report never copies it, and of the switches
set (the names `hookshot_env.txt` takes: `RX_`, `HOS_`, `HOOKSHOT_`,
`EDEN_`, `NVN_`, `SDL_`; and what hooks other code in:
`VK_INSTANCE_LAYERS`, `MANGOHUD`, `ENABLE_VKBASALT`, `LD_PRELOAD` ...)
it writes each name, and the value only when it cannot hold anything
private: a number of at most 12 characters, a size such as `1920x1080`,
or on/off, true/false, yes/no, and never for a name with `TOKEN`, `KEY`,
`ROOM`, `SECRET`, `PASS`, `SERVER` or `USER` in it. Every other value
reads `(value left out)`: paths, addresses, scripts. Other variables are
not listed at all.
- **The link** stays within 2000 characters and carries no switch value:
the options changed and the switches' names, or, when the names do not
fit, how many switches are set. A player who is not signed in is sent to
GitHub's sign-in page with the whole link inside it, and that page
failed for links from about 3500 characters of escaped text (table
below).
- `hookshot_log.txt` (this run's log up to the press) and
`hookshot_log.1.txt` (the run before's; when this run logs elsewhere,
e.g. from a terminal, the game folder's `hookshot_log.txt`). Read by
length (`fread`, split on newlines), so NUL bytes, which a run ended by
a power cut or a hard kill can leave, are copied as they are. Each line:
the home folder (a whole folder; on Windows either slash and any case)
becomes `~` or `%USERPROFILE%`, the user's name as a folder of any path
`<user>`. A log over 10 MB keeps its first MB and its last 9 MB (GitHub
takes files up to 25 MB).
- `picture.png`: 1280x720, the game's next frame, without the menu
(below). If none comes within 3 s, the report goes without it and says
so; a picture that comes later is deleted again.
- A folder that cannot be made is reported with the system's reason
(`GetLastError()` on Windows, `errno` elsewhere).
- `gpu/vk/vk_menu.cpp` (+89): the row before Restart and Quit
(`kBugReportRow = kRestartRow - 1`), its help (what goes in, that no
saves or game files go in and nothing is sent), its value (Press to save
/ Saving... / Saved / Not saved), Save in both prompt lines (the full
menu's and the compact bar's), the press, `changed_rows_text()` (rows
against a default-constructed `Options`), `bug_report_save()`, and the
picture's ask, `bug_report_picture()`. It follows Save my pipeline
list's pattern (#2447): its value in `row_value_cell`, its line under
the help in `draw_options` once pressed (where the folder is and how to
send it: File Explorer on Windows, a file manager on Linux; on a Steam
Deck, Desktop Mode), and room for that line's longest form in
`measure_help`. The game opens no browser or file manager: Game Mode on
a Deck has neither.
- `gpu/vk/vk_replay.cpp` (+2): `shot_ask_next()`, which calls #926's
`shot_ask(path, -1, done, ctx)` from outside its anonymous namespace
(declared in `vk_internal.h`). The report's picture is the control
bridge's kind of shot: an image and a buffer of its own (1280x720, made
at the first ask), copied from `src`, the game's frame, which the menu
is never drawn into (`menu_record_image` draws only into the
screenshots' image), read back once the GPU is past it and written on a
thread. No other line of `do_present` changes.
- `gpu/vk/vk_main.cpp` (+11), `vk_internal.h` (+6):
`device_report_text()`, and the declaration above.
- `host/hos_main.c` (+12): `map: <Sequence_CurrentBanc> (child|adult)`
when the map or the age changes, in `oot_inventory_refresh` (on Link's
update, at most every 500 ms, where the age is already read for the
Debug tab). It is where the report's place comes from, and it puts the
place into every log. The names are the game's own map names, which the
debug tools' teleport lines already print.
- `readme.md`: the row in the menu table, a paragraph on what it saves
and which values it leaves out, a pointer from "If the problem
continues".
- `tests/test_bug_report.py` (new), and `tests/test_menu_tune.py`: its
check of the compact bar's prompt line now reads the line with the Bug
report in it (the check is otherwise unchanged).
- Unchanged: nothing is sent anywhere; no saves, no game files, not
`hookshot_options.ini` or `hookshot_env.txt` themselves; no new option
or switch; the logs' own rotation; the screenshot path.

## How to try it

Insert menu (Insert, F1 or Esc, or the menu button) → **Gameplay** →
**Bug report** → Enter or A.
The row reads Saved and its help names the folder:
`bug-reports/<date>_<time>` in the game folder, with `report.txt`,
`hookshot_log.txt`, `hookshot_log.1.txt` and `picture.png`.

## How it was tested

**2026-11-11, after the two reviews, head `a75f3ca5` on `main`
`ba6eef70`.** Rebased twice today: onto #926, then onto #2042, #2378,
#2447 and the rest. The conflicts, each kept on both sides: #926's
`shot_ask_record` beside the old readback (which is now gone); #2042's
Debug rows where the Bug report row goes before Restart; #2378's
`row_value_cell()` and #2447's share row, where the row's value cell and
prompt went (the row now follows the share row's pattern, above); a
forward declaration in `host/hos_main.c`. #2042's rules hold for the
row: it has no value, so `preset_exempt` does not apply
(`tests/test_preset_exempt.py` reads only rows with one), and the branch
reads no on/off switch. #2042 also took `TOTK_` out of the names
`hookshot_env.txt` takes; the report follows (the test compares the two
lists).

Environment: Omarchy (an Arch Linux), kernel 7.2.15-1-omarchy, RTX 4090
with NVIDIA 615.78.08, i9-13900K, 94 GB, g++ 16.2.1 and clang 23.1.3.

- The new test on the branch (`python -I -B tests/test_bug_report.py`,
with `--cxx g++` and with `--cxx clang++`; the same line from both):
  ```
bug report: ok (report.txt as the issue form, both logs redacted, the
picture, the long log, the terminal start, switch values, NUL bytes, the
folder's reason)
  ```
Against `main`'s source (the test copied into an export of `ba6eef70`;
with both compilers): it fails at its first step, the menu row:
  ```
  at = menu.index('{TAB_GAMEPLAY, "Bug report", nullptr')
  ValueError: substring not found
  ```
And the review's NUL case against the old log copy (`fgets` and
`line.back()`, put back on its own):
  ```
bug report: the copy of a log with NUL bytes differs: b'[hos host]
hookshot v2.1.0-rc.3-39-gbdfe09c (source 763271ad)\n[hos Main] fs:
mounted content: -> ~/Games\n[hos Main] map: KokiriForest_Child
(child)\n[hos Main] half a line'
  ```
It compiles `vk_bug_report.cpp` alone with a driver and writes reports
from a made-up game folder inside a made-up home folder
(`.../home/alice/Games/Hookshot/`), with paths of the home and of
`/run/media/alice/...` in the logs, map lines and a `CRASH:` line in the
run before's. It checks: the folder holds exactly the report, both logs
and the picture; no file names the home folder, the user as a folder or
the game folder's path (`alice123` and `Malice` stay);
`hardware_report.py`'s `section`, `classify` and `os_of` read the
version, `NVIDIA` / `RTX 40` and Linux from `report.txt`; the place is
the last map, and the run before's; the switches: values kept
(`HOS_HEADLESS=1`, `60`, `off`, `3840x2160`, `-1`, `1.5`) and left out
(a path, a drive path, a menu script, an IP address, a `host:port`, a
13-digit number, and numbers under ROOM, KEY, SERVER, USER and PASSWORD
names), none of those values anywhere in `report.txt`, `PATH` and a
token not listed, no `%3D` in the link; the run before's crash line; the
link's fields and its length (with 98 switches: within 2000, the options
and the count in it, every switch in `report.txt`); the menu's way (a
thread, one report at a time); no frame means no picture, said so; a
picture that comes after the report went without it is deleted; a start
from a terminal; two reports in one second; a 22 MB log cut to its first
and last part with its middle's map still found; a log with a NUL in a
line and 4 KB of NULs at its end copied byte for byte, the map after the
NUL found; a folder that cannot be made, with the system's reason; the
redaction's Windows cases (either slash, any case); this PC's system
line. And the wiring by text: the row and what its help promises, Save
in both prompt lines, the press, the picture asked of `shot_ask_next`
and copied from the game's frame without the menu, the host's `map:`
line, that the switches it lists are the names `hookshot_env.txt` takes
(`env_line_allowed`'s), the form's labels and ids. Eight runs at once at
load 280 all pass (the first version of the menu's case read the
report's state too late under load in the full suite; it now holds the
report until it has looked).
- The checks the review named and those that read the menu's rows, on
this head (with g++ and clang++ where they compile C++):
`test_capture_dimensions.py`, `test_capture_slot_ownership.py`,
`test_menu_env_overrides.py`, `test_menu_keys.py`,
`test_pipeline_share.py`, `test_menu_tune.py`,
`test_stick_dead_zone.py`, `test_preset_exempt.py` (62 rows),
`test_menu_env_on.py`, `test_env_on.py`, `test_fps_stats.py`,
`test_gpu_choice.py`, `test_menu_restart_slots.py`,
`test_mouse_settings.py`: all pass. `vk_bug_report.cpp` alone with
`-Wall -Wextra -Wpedantic -Werror`: no warning from either compiler.
- `python -I -B tests/run_all.py` on Linux, on this head:
  ```
  440 passed, 1 failed, 20 skipped in 600 s (linux, 32 at once)
  ```
The one failure is `test_recorder_sync.py`, which ran out of its 600 s
at load 250-320 (other work ran beside it) and touches nothing of this
branch. An earlier run of the whole suite on this branch failed
`test_bug_report.py` once under that load: its menu case read the
report's state too late (fixed in the test, above).
- Merged into the newest `main` (`fce53453`, nine commits later; it
merges cleanly): `test_bug_report.py` (both compilers),
`test_menu_tune.py`, `test_pipeline_share.py`, `test_menu_keys.py`,
`test_menu_env_overrides.py`, `test_preset_exempt.py` (63 rows),
`test_menu_restart_slots.py`, both capture tests: pass. `test_env_on.py`
fails there and on `fce53453` alone (`HOS_FPS_LOG
(host/oot_high_fps.c:15)`, from #2470), not from this branch.
- Windows: `vk_bug_report.cpp` with a small driver compiles and links
for x86_64-w64-mingw32 (clang 23 with llvm-mingw 20261104's headers and
libraries, `-Wall -Wextra -Werror`: a PE32+ console executable). Not
run.
- In the game, on a build of this head's code
(`v2.1.0-rc.3-177-g9c538c91 (source c4f2afbc)`; the head differs only in
two test files), headless, RTX 4090, `RX_VK_PRECOMPILE=0` (shaders
compiled when first used), load 200 to 320 (other work ran beside it),
the save loaded into Kokiri Forest and the menu script pressing
**Gameplay → Bug report**:

  | Run | What | Harness result | The report |
  |---|---|---|---|
| br-menu2 | the row pressed 55-63 s after the first frame | exit 124
(its 120 s), fatal 0, crash 0, device lost 0, kernel driver lines 0 |
`bug-reports/2026-11-11_16-58-06`: report.txt and both logs, **no
picture**, and it says so. Rightly: the game showed no frame from 39 s
to 107 s (the save loading, shaders compiled as they came, at load 300),
took the scripted keys when frames came again, and stopped once more at
109 s |
| br-crash3 | a first start killed with `kill -9` after 110 s, then a
second start; the row pressed 63 s after its first frame | exit 124 (its
260 s), fatal 0, crash 0, device lost 0, kernel driver lines 0 |
`bug-reports/2026-11-11_17-13-48`: report.txt, both logs
(`hookshot_log.1.txt` is the killed start's whole log), picture.png
(present 1772) |
| br-menu3 | the row pressed 130-138 s after the first frame, in play |
exit 124 (its 200 s), fatal 0, crash 0, device lost 0, kernel driver
lines 0 | `bug-reports/2026-11-11_17-25-32`: report.txt, both logs,
picture.png (present 3063, 138 s) |
  The after-crash run's log lines (the picture's path shortened):
  ```
  [hos BaseProcExecutor5] map: KokiriForest_Child (child)
  [hos Presentation Thread] menu: bug report asked for
[hos host] vk: present 1772 written to
~/Games/.../bug-reports/2026-11-11_17-13-48/picture.png (the control
bridge)
[hos host] menu: bug report written to bug-reports/2026-11-11_17-13-48:
report.txt, hookshot_log.txt (245 KB), hookshot_log.1.txt (117 KB),
picture.png
  ```
Its folder read back against the game folder's logs (`check_report.py`:
each copy compared line by line with its original, with the home folder
and the user name replaced as the game does):
  ```
files: hookshot_log.1.txt (119514 bytes), hookshot_log.txt (250199
bytes), picture.png (2765798 bytes), report.txt (3212 bytes)
no file holds the home folder or the user name (any case, anywhere) [the
checker's line, with the two values left out here]
hookshot_log.txt: 1827 of the original's 2991 lines, 12 of them with the
home or the user replaced, nothing else changed
hookshot_log.1.txt: 991 of the original's 991 lines, 11 of them with the
home or the user replaced, nothing else changed
hardware_report.py: vendor and class ('NVIDIA', 'RTX 40'), OS Linux /
SteamOS
  ok
  ```
br-menu2's and br-menu3's folders pass the same check (1387 of 1571 and
3230 of 3230 lines; 2861 of 3679 and 3230 of 3230 lines; only the home
folder and the user name replaced). A picture's pixel data is not read
as text: br-menu3's picture.png has the bytes `4d 4f 4f 4b 49` among its
pixels (row 176), which an any-case byte search reads as a name; the PNG
holds only IHDR, IDAT and IEND chunks. The host name is in no text file
of either.
- Pictures (headless, 1280x720; the first two are the harness's shots
with the menu drawn in, the third the report's own file):

![1: the Bug report row selected, Press to save. 2: after Enter, Saved,
with the folder under the help. 3: that report's picture.png: the game's
frame without the
menu](https://gist.githubusercontent.com/mooki000/b86bd3f35969803fb032f71f035844f8/raw/a943230353c26f026eef40ea371a06b754cc084f/hookshot-bugreport-shotask.jpg)

1. br-menu3, the row before the press: **Bug report** selected, **Press
to save**, its help (what goes in; no saves or game files; nothing is
sent), the prompt bar **Save Enter**.
2. After Enter: **Saved**, and the paragraph under the help names the
folder (`bug-reports/2026-11-11_17-25-32`) and how to send it.
3. That report's `picture.png`, taken through `shot_ask` at present
3063: child Link and Navi on the path in Kokiri Forest, hearts and
minimap; no menu, not dimmed.

![4: after a kill -9 and a restart, Saved, with its folder under the
help. 5: that report's picture.png: the game's frame without the
menu](https://gist.githubusercontent.com/mooki000/b86bd3f35969803fb032f71f035844f8/raw/a943230353c26f026eef40ea371a06b754cc084f/hookshot-bugreport-aftercrash.jpg)

4. br-crash3, after the `kill -9` and the restart: **Saved**, the folder
`bug-reports/2026-11-11_17-13-48` under the help.
5. That report's `picture.png` (present 1772): Kokiri Forest from low
down, facing the bright sky, hearts and minimap; no menu.

- The `report.txt` the after-crash run wrote, as a player would attach
it (the switches are the test harness's: see which values are written
and which are left out):

<details><summary>The report.txt that run wrote</summary>

```text
Hookshot bug report, made by the Insert menu's Bug report on 2026-11-11 at 17:13.

To send it: open https://github.com/NotRexed/Hookshot/issues/new/choose and choose Game problem (or the link at the end of this file, which fills in the answers below), copy the answers below into the fields of the same names, say what happened, and drag the files of this folder, this one too, into the issue.

### Hookshot version

v2.1.0-rc.3-177-g9c538c91 (source c4f2afbc)

### Where in the game

KokiriForest_Child (child)
(The map this run's log names last: add what you were doing there.)

### Graphics card and driver version

NVIDIA GeForce RTX 4090, NVIDIA 615.78.08

### CPU, RAM and operating system

13th Gen Intel(R) Core(TM) i9-13900K (32 threads), 94 GB, Omarchy (Linux 7.2.15-1-omarchy), Hyprland (wayland)

### Changes from the defaults

Every Insert menu option is at its default.
Switches set (the environment, hookshot_env.txt; a value only when it is a number or on/off, and never for a name with TOKEN, KEY, ROOM, SECRET, PASS, SERVER or USER in it):
HOOKSHOT_NO_UPDATE_CHECK=1
HOS_AUDIO=0
HOS_DISCORD=0
HOS_ENV_FILE=(value left out)
HOS_HEADLESS=1
HOS_INPUT_BACKGROUND=0
HOS_INPUT_EXCLUSIVE=1
HOS_INPUT_FRAME_CLOCK=1
HOS_INPUT_SCRIPT=(value left out)
HOS_PRESENT_TRACE=(value left out)
RX_CPM=(value left out)
RX_VK_DEVICE=0
RX_VK_FRAME_LOG=(value left out)
RX_VK_MENU_SCRIPT=(value left out)
RX_VK_PRECOMPILE=0
RX_VK_SHOTS=(value left out)
RX_VK_SHOT_EVERY=300
RX_VK_SHOT_MENU=1
SDL_AUDIODRIVER=(value left out)
SDL_AUDIO_DRIVER=(value left out)
VK_DRIVER_FILES=(value left out)

### hookshot_log.txt

hookshot_log.txt: this run's log as it was when the report was made, 245 KB.
hookshot_log.1.txt: the run before's log (hookshot v2.1.0-rc.3-177-g9c538c91 (source c4f2afbc), last written 2026-11-11 17:12), 117 KB.
In both, the home folder is written ~ and the user's name <user>.

### Screenshots or video

picture.png: the game's picture when the report was made, without the menu.

### What this folder holds

report.txt (this file), hookshot_log.txt, hookshot_log.1.txt, picture.png. Nothing else from this PC: no saves, no game files, not the options file itself.

The link with the answers filled in:
https://github.com/NotRexed/Hookshot/issues/new?template=1-game-problem.yml&version=v2.1.0-rc.3-177-g9c538c91%20%28source%20c4f2afbc%29&where=KokiriForest_Child%20%28child%29&gpu=NVIDIA%20GeForce%20RTX%204090%2C%20NVIDIA%20615.78.08&system=13th%20Gen%20Intel%28R%29%20Core%28TM%29%20i9-13900K%20%2832%20threads%29%2C%2094%20GB%2C%20Omarchy%20%28Linux%207.2.15-1-omarchy%29%2C%20Hyprland%20%28wayland%29&settings=Every%20Insert%20menu%20option%20is%20at%20its%20default.%0ASwitches%20set%20%28their%20values%20are%20in%20the%20attached%20report.txt%29%3A%20HOOKSHOT_NO_UPDATE_CHECK%2C%20HOS_AUDIO%2C%20HOS_DISCORD%2C%20HOS_ENV_FILE%2C%20HOS_HEADLESS%2C%20HOS_INPUT_BACKGROUND%2C%20HOS_INPUT_EXCLUSIVE%2C%20HOS_INPUT_FRAME_CLOCK%2C%20HOS_INPUT_SCRIPT%2C%20HOS_PRESENT_TRACE%2C%20RX_CPM%2C%20RX_VK_DEVICE%2C%20RX_VK_FRAME_LOG%2C%20RX_VK_MENU_SCRIPT%2C%20RX_VK_PRECOMPILE%2C%20RX_VK_SHOTS%2C%20RX_VK_SHOT_EVERY%2C%20RX_VK_SHOT_MENU%2C%20SDL_AUDIODRIVER%2C%20SDL_AUDIO_DRIVER%2C%20VK_DRIVER_FILES.%0A
```

</details>
- The link's length, signed out (`curl`, dummy links to the form,
2026-11-10; nothing of this PC in them):

  | Link length | Padding | GitHub's answer |
  |---|---|---|
| 1238 | the report's own link (then) | sign-in page, the link kept in
`return_to` |
| 1999, 2500, 2998, 3199 | `%20` | sign-in page, the link kept in
`return_to` |
  | 3500 | `%20` | 400 |
  | 4000, 4500 | `%20` | 500 after the redirect |
| 6000 | `%20` | sign-in page, but `return_to` dropped (the answers
lost) |
  | 6000, 6400 | `x` | sign-in page, the link kept |
  | 6700, 7000, 7500, 7900 | `x` | 500 |
  | 8192 and more | `x` | 414 |

## Not tested

- **Windows, at run time.** The report's GPU line there
(`VkPhysicalDeviceDriverProperties` on a Windows driver), its CPU, RAM
and system line (`__cpuid`, `GlobalMemoryStatusEx`, `RtlGetVersion`,
Wine), the path of the log stderr writes to
(`GetFinalPathNameByHandleA`), `GetLastError`'s reason, the redaction of
real `C:\Users\...` logs, File Explorer: compiled and linked only.
- **A windowed run.** Only headless runs: the picture taken with a
window, HDR (a native HDR frame is copied to 8-bit as the control
bridge's shots are) and frame generation were not seen.
- **The link in a browser.** Not opened in a browser, signed out or
signed in, and no issue was opened from it; only `curl` signed out
(table above). That GitHub fills the form's fields from it is its
documented behaviour, not seen here.
- **A real crash's last lines in the game.** The after-crash run ended
its first start with `kill -9`, which leaves no `CRASH:` line, and under
that load the first start had not reached a map yet; the test covers the
`CRASH:` line and the run before's map with made-up logs.
- AMD and Intel drivers' `driverName`/`driverInfo` text (only NVIDIA's
was read; `hardware_report.py` classifies typical RADV, AMD and Intel
strings correctly when written by hand), a Steam Deck, the AppImage, and
the Linux Builder's Play button (since #2279 it gives the game its
`hookshot_log.txt` again, so the report finds this run's log there; not
run here).

## Risks

- **Something private in a report.** The home folder (as a whole folder;
on Windows either slash and any case) and the user's name as a folder of
any path are replaced in both logs; a switch's value is written only
when it is a number, a size or on/off and its name holds none of the
words above; other environment variables are not listed; a microphone
chosen by name is not named; no saves, no options or env file, no game
files go in. Guarded by the test (made-up home and user, Windows cases,
`alice123` kept, values kept and left out) and, on this machine's real
logs, by the read-back above. Not guarded: something private a log line
carries in another form (a game folder outside the home named after a
person, a machine name in a network path), or a future switch whose
secret is a short number under a name without those words.
- **A report written while the game is busy.** The press does only cheap
work on the menu's thread (flushing the log, reading the environment,
`/etc/os-release`, the device's properties, the menu's rows); copying
the logs and writing the folder run on a thread of their own, one report
at a time (a second press while it saves does nothing). This run's log
is copied as far as it was when the copy began. The picture is waited
for at most 3 s.
- **The picture.** It shares #926's queue (16 asks at most) and its one
readback: a control-bridge shot being read back delays the report's by a
present or two. Without a report nothing changes on the present path.
The control bridge's log line for a written shot also appears for the
report's picture (`vk: present N written to ... (the control bridge)`).
- **The `map:` line in every log.** It is written only when the map or
Link's age changes, from `oot_inventory_refresh` (at most every 500 ms,
where the age is already read). A log that a player posts now says where
he was; the names are the game's own map names, which the debug tools'
teleport lines already print.
- A crash that leaves no line (a kill, a freeze, a power cut): the run
before's log is attached, but nothing in it says how it ended. A game
that stops before its menu can be used cannot make a report; the readme
says to attach `hookshot_log.1.txt` by hand then.

## Overlaps

`pr_overlap.py --branch bug-report` (run on this head, 2026-11-11): 79
open pull requests touch one of these files; 73 merge cleanly with it.
The row stores nothing (no `Options` field, no ini key, no option id),
so no option id can clash. Six conflict in this branch's files, each
hunk read:

- #2417 (the menu in 16 languages): it wraps the menu's texts in
`i18n::tr(...)`, among them the lines of `measure_help`,
`row_value_cell`'s branch for rows without a value, the help line in
`draw_options` and the compact bar's prompt (and the same line in
`tests/test_menu_tune.py`), where this branch adds the Bug report beside
the share row. Whichever lands second wraps the Bug report's texts the
same way; both stay.
- #1521, #1864, #2105, #2459, #2479 (the readme): a row each after
Microphone in the menu table (#2459 moves the Microphone row), where
this branch adds the Bug report's row; both stay.
- #2317 (saves and settings in a per-user data folder, logs in `logs/`)
now merges cleanly, but overlaps in meaning: this branch finds the game
folder through `ini_path()` and the logs in the game folder; after #2317
the settings are in the data folder and the logs in `logs/`. Whichever
lands second moves those two reads to `game_folder()` and `log_path()`.

## Questions for you

1. Is the Insert menu the place you want this, and the game folder the
place for the reports (with #2317, the data folder or `logs/` could be)?
2. The report takes the strict reading of CONTRIBUTING.md on
`hookshot_env.txt` (names, and only harmless values). Should
CONTRIBUTING.md say so, next to the line that lists the file?
3. The link carries the form's one-line answers and, when short enough,
the options and the switches' names. Do you want it at all, or only
`report.txt` to copy from?

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants