feat(scripts): the pipeline tour, driven through the control bridge #111

Merged
HumanBean merged 4 commits from HumanBean/Longshot:scripts/pipeline-tour into main 2026-11-14 23:32:23 +00:00
Owner

Type: feat · Risk: low · Tested on: not run yet (this head, d41a45bd) · Needs: Linux, any GPU, a counting renderer pack for --vk-profile (the maintainer's local run)

Why

Every release ships a pipeline list so a new install builds its pipelines before the title, not in play. The tour's part of that list (assets/pipeline_states/tour.bin.xz) comes from a script that was never merged (Hookshot's draft https://github.com/NotRexed/Hookshot/pull/2048), so Longshot has no way to make it again. The renderer's committed play profile (assets/profiles/longshot_vk.profdata.xz, merged in #569) was recorded on one short route (Kokiri Forest and the Market) and should come from the same tour. This adds that script, driven through the control bridge, and lets it record the profile too. See #14 (players' pipeline lists, which the tour's list complements) and #505 (ship play profiles).

What changed

A new script plays the game unattended from a new save through every Teleport destination at both ages and writes the tour's pipeline list. With --vk-profile it also records the renderer's play profile on that route, and merge-profile turns the recording into the profile builds use.

  • scripts: scripts/pipeline_tour.py, ported from https://github.com/NotRexed/Hookshot/pull/2048 (HumanBean's). It plays from a new save through every Teleport destination at both ages, with the items, a dive, the pause menu, and the sword, shield and an item once an age and at the stops with passes of their own. Then it writes tour.bin.xz + tour.md through nx2/pipeline_states.py tour (--publish), which main already reads. The stops, the left-out stops, the presses, the memory guard (MEM, systemd scope), the games of 8 stops, merge, report and value are the same as in the draft. What changed from it:
    • The control bridge drives it instead of HOS_INPUT_LIVE and log polling (Game, map_change, new_game, start_game). It uses teleport by the destination's index and age, and asks again on refused, timeout or cancelled. It gives up when the refusal says asking again can't help (PERMANENT, the game's own reasons from oot_teleport), or after five sends that don't move Link (the Haunted Wasteland). It waits for arrival with wait map (loading_end after the age switch), then calls status until the arrival's event is over. Presses are sequence with units: hos (which scripts/longshot_ctl.py now sends as frame steps), and waits are wait present. Frames come from the replies, and the GPU and build for tour.md from status. The game is asked to quit, so its lists are flushed.
    • One fallback: the Debug tab's inventory still goes through HOS_INPUT_LIVE's !cheat lines, because the bridge doesn't do it.
    • No host change: the draft's !cheat 998 state line is replaced by the bridge's status.game. Its !teleport <map> <start> live command and the extras part (boss rooms) are left out.
    • Faster: the cap is Uncapped (HOS_FPS_MAX=1000, frame_rate_cap=6) where the draft used 240, still with every frame a 60th of game time (HOS_FPS_STEP=0, Game speed Per frame). V-Sync is off. The game skips the opening itself (HOS_SKIP_OPENING=1) in every part but the one that watches it. The draft's other settings stay (headless, 640x360, game upscaler at 50%, no pacing, frame generation, sharpening or HDR, instant text). None of these change a pipeline's key.
    • Less waiting at each stop (this head):
      • The sword, shield and an item were used at 16 stops (10 names in ACT_AT, at both ages). A pipeline is a program plus fixed draw state, and a scene's lighting and fog are inputs to the same pipelines, so they're now used once per age (ITEMS_ONCE: Kokiri Forest as the child, Hyrule Field as the adult). They're also used at the stops with passes of their own that an item's draws could combine with (ITEMS_PASSES, each with its reason: Zora's Domain and the Water Temple for water, the Shadow Temple for the Lens of Truth (child only, since the adult's visit is left out), Death Mountain Crater (adult only) and the Fire Temple for heat haze). That makes 10 uses. Each one gets its own pause-menu pick, numbered across the whole route (item_stops), so a pick no longer repeats in every game of 8 stops. The every-item inventory, the dive and the pause menu stay as they were.
      • The settle: a stop was left after 150 frames with no new pipeline (2.5 s of game time), 600 at most. It already counted frames (the bridge's wait present). It's now 60 frames (--settle-quiet, on run and tour), and 600 at most as before (--settle-max: those are frames that are still bringing pipelines). The tour passes both to its runs, and report.txt says which were used (--settle-quiet 150 is the old wait).
      • The inventory's !cheat lines were 70 ms of wall time apart. They're now 32 ms apart (the game reads the live file once 30 ms have passed, host/nn_hid.c live_input), followed by three presents (the render thread keeps the game a frame ahead of its presents). A frame slowed by a pipeline build is now waited for, which the wall-clock pause alone didn't do. The bridge's start poll is 0.1 s instead of 0.25 s. Nothing else waits on the game in wall time.
      • The speed settings (headless, 640x360, pacing off, HOS_FPS_MAX=1000, HOS_FPS_STEP=0) were already set in every part's game. They're unchanged, and so are the graphics settings, since those decide which pipelines exist.
      • compare <new list> <old list> and tour --compare-to <old list> count the pipelines the old list has that the new one lacks (lost), and the other way round (gained), for checking that a faster tour loses nothing.
    • The game folder is always put back (this head, from a self-review): the folder is changed only inside the try that restores it, after what can fail (--cpus is checked first). put_back moves the run's files with shutil.move (so --out can be on another drive), one file at a time, and names any that didn't go back. An options file the folder didn't have is removed again (restore_ini). A stopped tour (Ctrl+C or SIGTERM) gives its run two minutes to end its game, dump the profile and put the folder back (run_child); before, subprocess.run killed the run a quarter of a second in and left the headless game running. A run holds further stops while it puts things back (hold_stops), and writes its report as "stopped". A game whose bridge never answers is ended (start_game). The bridge client's own Failed (a crash during a wait) ends the run with its report instead of a traceback. A run clears an earlier tour's states.bin and report. A leftover <game>-tour copy is refused, and a copy that can't reflink says so.
    • Only the run's play counts (this head): cache/longshot_computes.bin is put aside too, since the game writes earlier play's compute programs into a new list. The game gets none of the shell's HOS_*/RX_*/A64_*/OOT_* switches (run_environ), an empty HOS_ENV_FILE instead of the folder's longshot_env.txt, and its own --user folder for the saves. game_exe is scripts/longshot_ctl.py's: bin/longshot in a builder's folder, whose top-level longshot starts the launcher.
    • Waits (this head): the teleport's 8 s refusal is wall time, so it's waited out and no longer pressed at. The lake-bottom teleport, to the map Link is already in, waits for its load to end (reload). calm takes only a fresh snapshot (game.frame within 10 presents of the reply's, as docs/control.md asks).
    • --publish (this head): tour.md names no path of the run's (RX_VK_SHOTS), and its settings are the options the runs had (their --ini), not BASE_INI alone.
    • Main's game folder: the pipeline notes and the lists play adds to are in the game folder's cache/ now (host/game_layout.c, vk_draw.cpp state_list_path): the tour puts cache/longshot_pipelines.bin, cache/longshot_pipeline_states.bin (and _v1), the shipped list beside the game, and any loose ones an older build left, aside for a run, and reads cache/longshot_pipeline_states.bin.
    • Renamed to Longshot: longshot_* files, longshot_options.ini, the longshot binary. The Sway window-moving code is dropped (--window WxH just shows a window). --cpus has no PC-specific default.
  • scripts: the renderer's play profile, --vk-profile <dir> on run and tour (Profile, profile_end, counting_renderer, merge_profile):
    • The game folder's renderer must count (RX_VK_PGO=gen: its longshot_vk.so names RX_VK_PGO_FILE); otherwise run and tour refuse before touching anything.
    • Each game runs with RX_VK_PGO_FILE=<run dir>/vk.profraw. Once new_game has control of Link, the tour creates <file>.reset and waits for the renderer to take it (host/pgo_rt.c, every 500 ms), so the loading, the prepare and the title are left out. A renderer that doesn't take it within 10 s fails the run.
    • At each game's end, failed or not (a hard memory stop waits 3 s), it creates <file>.dump, waits up to 10 s for the counters and moves them to <dir>/<game>.profraw, with <game>.json beside it (part, GPU and driver, build, seconds). A dump that writes nothing fails the run, so a recording never ends empty. A game that never reached control keeps nothing.
    • merge-profile <dir> runs the toolchain's llvm-profdata merge -o work/longshot_vk.profdata <dir>/*.profraw (third_party/a64-kit/tools/toolchain.py find("llvm-profdata"), or --llvm-profdata; --out elsewhere). It refuses a folder with no .profraw, a file that isn't LLVM's raw format, one with no counted functions, and a merge that writes nothing.
    • merge-profile --publish also writes assets/profiles/longshot_vk.profdata.xz (Python's lzma, preset 9 | PRESET_EXTREME, as xz -9e) and the lines for assets/profiles/README.md (date, parts, games, build, GPU and driver, OS and CPU, the LLVM version) to <dir>/README-note.md.
    • With the option off, nothing changes: no variable, no reset or dump, the same run commands and reports. The RX_VK_PGO_FILE path is kept out of tour.md's settings.
  • tests: tests/test_pipeline_tour.py, game-free.
    • Source checks: the actions and teleport table against host/hos_main.c, gpu/vk/vk_menu.cpp and gpu/vk/oot_cheat_items.h (the Hookshot is checked again: main fixed its name). The bridge commands and status fields are checked against host/control.c and host/oot_state_tap.c (status's game now comes from the sampled state), and the units: hos steps against scripts/longshot_ctl.py. The lists' places are checked against vk_draw.cpp, vk_paths.h and game_layout.c. RX_VK_PGO_FILE, .reset, .dump and the raw header are checked against host/pgo_rt.c and nx2/build.py. Each of the game's teleport refusals must be either retried or given up on. Every BASE_ENV switch must exist in the source, and every BASE_INI key must be one an Insert menu row writes.
    • Route and input: the route and chunks, and the presses' buttons and spans. The item stops are checked: once per age at the first stop of that age's overworld, plus ITEMS_PASSES (each one visited and with a reason). The picks must all differ, and a game-free play must use the items at exactly those stops and settle by the run's --settle-quiet/--settle-max.
    • Command line: the defaults (--vk-profile off), refusals, --dry-run (which marks the item stops), the switches and the tour's per-game command (with and without --vk-profile, now with the settle's frames). Also checked: the speed settings in every part's game, the settle's defaults (in frames, under the old 150), --settle-quiet 150 passed on by tour, --compare-to, and that the only wall-clock sleeps left are the live lines' gap and the bridge's start poll.
    • Lists: list counts, merge, compare and list_diff (lost and gained both ways, nothing lost against a merge's own input), set_ini, the report (now with its settle line) and --publish (no RX_VK_PGO_FILE in its note).
    • Bridge: a fake bridge (a Unix socket served from a thread, as in tests/test_longshot_ctl.py) behind the real scripts/longshot_ctl.py client, taking frame steps as host/control.c does. It covers a new game to control, a teleport refused twice and retried, a refusal that is final, a teleport that never moves, the age switch, settle (the old 150 frames and the new default), and the inventory through the live file (three frames per line).
    • Renderer profile: a counting renderer told apart from another; run and tour refusing a renderer that doesn't count, before touching the game folder; a stand-in for pgo_rt.c's writer, against which stale files are cleared, the reset comes right after control (and not at all without the option), and the dump is moved to <dir>/<name>.profraw. A writer that takes neither request fails the run. merge-profile is checked with a stand-in llvm-profdata: the command, --out, --publish's xz and note, and its four refusals.
    • Also the memory guard and value.
  • docs: docs/development.md gets a new section, "The pipeline tour", and "The renderer's play profile" now gives the tour's recording and merge-profile in steps 2 and 3. assets/pipeline_states/README.md now names the tour command.

Measurements

Verdict: not measured yet. The tour has not been run on this branch, with or without --vk-profile (WIP until it is).

Hardware: to come (the maintainer's local run, Linux)
Scene: the whole tour route, from a new save
Method: tour wall time and report.txt per part, against the draft's numbers. For this head's faster route: one tour at c23b44f3 (items at 16 stops, 150-frame settle) and one at this head on the same PC and game folder, then compare (expected: 0 lost). For the profile, the recording merged and published, then frame time as in assets/profiles/README.md (the Market save, standing and running, 3 sessions each), main's profile vs the tour's
How to reproduce: a game folder built from main (or a counting runtime for the profile), on Linux, with the commands below

python -I scripts/pipeline_tour.py tour --game <game folder> --out <tour dir> --name play --publish
RX_VK_PGO=gen python -I scripts/dev_packs.py pack <base> vk-count
python -I scripts/dev_packs.py rt <base> <runtime> vk-count
python -I scripts/pipeline_tour.py tour --game <runtime> --out <tour dir> --name play --vk-profile <tour dir>/vk
python -I scripts/pipeline_tour.py merge-profile <tour dir>/vk --publish
python -I scripts/pipeline_tour.py tour --game <game folder> --out <old dir> --name play     (the route before, from a workspace at c23b44f3)
python -I scripts/pipeline_tour.py tour --game <game folder> --out <new dir> --name play --compare-to <old dir>/play.bin
python -I scripts/pipeline_tour.py compare <new dir>/play.bin <old dir>/play.bin

Baseline: main has no tour script, so the before column is the Hookshot draft's own runs (https://github.com/NotRexed/Hookshot/pull/2048), on main's game otherwise.

Before: main @ fe5c60d0 (no tour; the Hookshot draft's runs, RTX 3090, Linux, partial) After: this PR @ d41a45bd
A stop 12-20 s to measure
Speed 1.8x real time at ~107 fps (cap 240) to measure (Uncapped)
Full tour, default parts ~35-40 min (estimated from those runs: 47 stops, 8 games of up to 8 stops, the opening) estimated ~18-23 min

The estimate assumes the stops cost about what they did in the draft. Most of the saving is per game: the game skips its own opening instead of being pressed through it, and there are no 10 s resend waits. This head saves about 8,000 game frames on top of that, estimated from the code: 48 settles × 90 frames, plus 6 fewer item uses × ~630 frames (the pick plus the sword, shield and item, ~10.5 s of game time). That's ~135 s of game time, or ~45-75 s of wall time at 107-180 fps. It also saves ~25 s of !cheat gaps (12 inventory steps × ~60 lines × ~37 ms), so ~1-2 minutes in all. Each stop also waits out the game's 8 s between teleports (wall time), so a stop costs at least 8 s however fast the game runs: only a run can say how much of the estimate is left.

(the runs' report.txt lines go here)

Flags and settings

None added to the game: the HOS_* and RX_VK_* names the script sets for its games (BASE_ENV, HOS_ENV_FILE, HOS_CONTROL, RX_VK_PGO_FILE) are existing switches, each checked against the source by tests/test_pipeline_tour.py. The script's own options (--vk-profile, merge-profile) are documented in docs/development.md, "The pipeline tour" and "The renderer's play profile".

Testing

Tested:

  • The previous head (e182b7e8, on main b9cdc432): python -I -B tests/run_all.py on Linux (CachyOS, clang 23), 493 passed, 2 failed (the same on main: tests/test_bake_portability.py, tests/test_builder_log_read_only.py), 36 skipped. run --dry-run printed the route.
  • c23b44f3: the rebase onto main 5d3ecc78 had no conflicts. Every source text the checks read was looked up by hand in main's files.
  • 84421c99: read, not run.
  • This head (d41a45bd, rebased onto main fe5c60d0 with no conflicts): a code review of the whole PR (in a separate session) found 20 points; the fixes for the confirmed and plausible ones are in this head, with checks in tests/test_pipeline_tour.py (folder_kept: the set-aside and put-back, the options file, game_exe with bin/, --cpus, run_environ, published_settings, run_child for Ctrl+C and SIGTERM; the fake bridge: a stale snapshot, the 8 s refusal). Read and traced by hand, not run.

Not tested:

  • This head's python -I -B tests/run_all.py, tests/test_pipeline_tour.py included: no checks are run on the author's PC.
  • Windows: the script is Linux only (the memory guard reads /proc, the bridge is a Unix socket, the copy is cp --reflink), as its docs say; tests/run_all.py skips its check there.
  • The game: no tour run yet, no --vk-profile recording, and no A/B of this head's route against c23b44f3's.

Must be tested:

  • Linux, the maintainer's PC (by hand): python -I -B tests/run_all.py, expecting tests/test_pipeline_tour.py to pass.
  • Linux, the lab (by hand): a tour at c23b44f3 and one at this head on the same game folder, then compare <new>/play.bin <old>/play.bin. Expected: 0 lost, and the new tour's wall time (its last line) shorter.
  • Linux, the maintainer's PC (by hand): a counting pack's runtime, tour --vk-profile, then merge-profile --publish. Expected: one .profraw per game in <tour dir>/vk, "renderer profile: " in each report.txt, and the .xz and its note written. The pipeline lists should come out as without the option.

Risks

What could break: only the script. A teleport refusal the tour should retry but treats as final (or the other way round), or a stop that misses: a run says so in its report.txt and steps.txt, and three misses in a row end the run. With --vk-profile, a renderer that is slow to take .reset or .dump (more than 10 s) fails its run rather than recording too little.

Turning it off: leave --vk-profile out; revert the commits for the rest.

Compatibility: Nothing changes. The game is unchanged, and the committed profile changes only when someone runs merge-profile --publish and commits it.

Issues

See #14 and #505. Also https://github.com/NotRexed/Hookshot/pull/2048, the draft this ports; #569, the renderer profile it records again.

Reviewer notes

  • run's try/finally, put_back and run_child are what keeps a player's game folder as it was: worth the closest read.
  • map_change and new_game are the parts that replace the draft's log polling. PERMANENT is checked against the game's own refusal texts.
  • ITEMS_ONCE/ITEMS_PASSES and item_stops decide where the items are used. If compare shows pipelines lost, the place to look is the item picks: 10 differ now, where before each game started again from the first (2-4 item uses a game).
  • Profile.reset and profile_end are the profile's two moments. The reset waits for the renderer to delete .reset, which only a counting renderer does, so a wrong game folder fails at its first control and not after a whole tour.
  • The second commit holds the rebase's follow-ups (main's cache/ folder, the sampled status.game, the client's frame steps) together with --vk-profile.
  • Follow-ups:
    • The game-side play profile (hot_funcs.txt, hot_order.txt, longshot_pieces.prof, from an RX_PGO=gen build) is still parked.
    • The extras part (boss rooms through a teleport-to-map command). GummiNaomi's comment on this PR offers a host change for it.
    • A bridge command for the Debug tab's inventory, which would retire the last HOS_INPUT_LIVE use.

Checklist

Everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description) of the status line's type.
  • Why gives the reason plainly and links the issues that fit (the tracker searched).
  • Measurements has a verdict, or one line saying why there is nothing to measure. (not yet: the tour hasn't run on this head)
  • Testing names what was tested, what wasn't, and what must still be tested, where and how.
  • Rebased on current main.
  • python -I -B tests/run_all.py passes (any failure that also fails on main is named). (not run on this head: no tests are run on this PC)
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

When it applies

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Numbers from the same PC, scene and settings, at least 2 interleaved runs each, the median reported. (to come with the tour's run and the profile's frame times)
  • Switches documented (docs/development.md; the README for a menu row) and shipped at their recommended value.
  • A file in docs/tested/ for each new measured run (format in docs/tested-configurations.md). (to come with the run)
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Docs updated.

🤖 Generated with Claude Code

**Type:** feat · **Risk:** low · **Tested on:** not run yet (this head, d41a45bd) · **Needs:** Linux, any GPU, a counting renderer pack for `--vk-profile` (the maintainer's local run) <!-- Open as work in progress (forge.py pr create adds "WIP: ") and mark it ready (forge.py pr ready) once every section and every Everyone box is done. Keep this description current, and post a comment for each new commit, measurement or test run: CONTRIBUTING.md, "Commits and pull requests". --> ## Why Every release ships a pipeline list so a new install builds its pipelines before the title, not in play. The tour's part of that list (`assets/pipeline_states/tour.bin.xz`) comes from a script that was never merged (Hookshot's draft https://github.com/NotRexed/Hookshot/pull/2048), so Longshot has no way to make it again. The renderer's committed play profile (`assets/profiles/longshot_vk.profdata.xz`, merged in https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/569) was recorded on one short route (Kokiri Forest and the Market) and should come from the same tour. This adds that script, driven through the control bridge, and lets it record the profile too. See https://git.magicbean.dev/MagicBeanDev/Longshot/issues/14 (players' pipeline lists, which the tour's list complements) and https://git.magicbean.dev/MagicBeanDev/Longshot/issues/505 (ship play profiles). ## What changed A new script plays the game unattended from a new save through every Teleport destination at both ages and writes the tour's pipeline list. With `--vk-profile` it also records the renderer's play profile on that route, and `merge-profile` turns the recording into the profile builds use. - **scripts: `scripts/pipeline_tour.py`**, ported from https://github.com/NotRexed/Hookshot/pull/2048 (HumanBean's). It plays from a new save through every Teleport destination at both ages, with the items, a dive, the pause menu, and the sword, shield and an item once an age and at the stops with passes of their own. Then it writes `tour.bin.xz` + `tour.md` through `nx2/pipeline_states.py tour` (`--publish`), which main already reads. The stops, the left-out stops, the presses, the memory guard (`MEM`, systemd scope), the games of 8 stops, `merge`, `report` and `value` are the same as in the draft. What changed from it: - **The control bridge drives it** instead of `HOS_INPUT_LIVE` and log polling (`Game`, `map_change`, `new_game`, `start_game`). It uses `teleport` by the destination's index and `age`, and asks again on `refused`, `timeout` or `cancelled`. It gives up when the refusal says asking again can't help (`PERMANENT`, the game's own reasons from `oot_teleport`), or after five sends that don't move Link (the Haunted Wasteland). It waits for arrival with `wait map` (`loading_end` after the age switch), then calls `status` until the arrival's event is over. Presses are `sequence` with `units: hos` (which `scripts/longshot_ctl.py` now sends as frame steps), and waits are `wait present`. Frames come from the replies, and the GPU and build for `tour.md` from `status`. The game is asked to `quit`, so its lists are flushed. - **One fallback:** the Debug tab's inventory still goes through `HOS_INPUT_LIVE`'s `!cheat` lines, because the bridge doesn't do it. - **No host change:** the draft's `!cheat 998` state line is replaced by the bridge's `status.game`. Its `!teleport <map> <start>` live command and the `extras` part (boss rooms) are left out. - **Faster:** the cap is Uncapped (`HOS_FPS_MAX=1000`, `frame_rate_cap=6`) where the draft used 240, still with every frame a 60th of game time (`HOS_FPS_STEP=0`, Game speed Per frame). V-Sync is off. The game skips the opening itself (`HOS_SKIP_OPENING=1`) in every part but the one that watches it. The draft's other settings stay (headless, 640x360, game upscaler at 50%, no pacing, frame generation, sharpening or HDR, instant text). None of these change a pipeline's key. - **Less waiting at each stop** (this head): - **The sword, shield and an item** were used at 16 stops (10 names in `ACT_AT`, at both ages). A pipeline is a program plus fixed draw state, and a scene's lighting and fog are inputs to the same pipelines, so they're now used once per age (`ITEMS_ONCE`: Kokiri Forest as the child, Hyrule Field as the adult). They're also used at the stops with passes of their own that an item's draws could combine with (`ITEMS_PASSES`, each with its reason: Zora's Domain and the Water Temple for water, the Shadow Temple for the Lens of Truth (child only, since the adult's visit is left out), Death Mountain Crater (adult only) and the Fire Temple for heat haze). That makes 10 uses. Each one gets its own pause-menu pick, numbered across the whole route (`item_stops`), so a pick no longer repeats in every game of 8 stops. The every-item inventory, the dive and the pause menu stay as they were. - **The settle:** a stop was left after 150 frames with no new pipeline (2.5 s of game time), 600 at most. It already counted frames (the bridge's `wait present`). It's now 60 frames (`--settle-quiet`, on `run` and `tour`), and 600 at most as before (`--settle-max`: those are frames that are still bringing pipelines). The tour passes both to its runs, and `report.txt` says which were used (`--settle-quiet 150` is the old wait). - **The inventory's `!cheat` lines** were 70 ms of wall time apart. They're now 32 ms apart (the game reads the live file once 30 ms have passed, `host/nn_hid.c` `live_input`), followed by three presents (the render thread keeps the game a frame ahead of its presents). A frame slowed by a pipeline build is now waited for, which the wall-clock pause alone didn't do. The bridge's start poll is 0.1 s instead of 0.25 s. Nothing else waits on the game in wall time. - **The speed settings** (headless, 640x360, pacing off, `HOS_FPS_MAX=1000`, `HOS_FPS_STEP=0`) were already set in every part's game. They're unchanged, and so are the graphics settings, since those decide which pipelines exist. - **`compare <new list> <old list>`** and **`tour --compare-to <old list>`** count the pipelines the old list has that the new one lacks (lost), and the other way round (gained), for checking that a faster tour loses nothing. - **The game folder is always put back** (this head, from a self-review): the folder is changed only inside the `try` that restores it, after what can fail (`--cpus` is checked first). `put_back` moves the run's files with `shutil.move` (so `--out` can be on another drive), one file at a time, and names any that didn't go back. An options file the folder didn't have is removed again (`restore_ini`). A stopped tour (Ctrl+C or SIGTERM) gives its run two minutes to end its game, dump the profile and put the folder back (`run_child`); before, `subprocess.run` killed the run a quarter of a second in and left the headless game running. A run holds further stops while it puts things back (`hold_stops`), and writes its report as "stopped". A game whose bridge never answers is ended (`start_game`). The bridge client's own `Failed` (a crash during a wait) ends the run with its report instead of a traceback. A run clears an earlier tour's `states.bin` and report. A leftover `<game>-tour` copy is refused, and a copy that can't reflink says so. - **Only the run's play counts** (this head): `cache/longshot_computes.bin` is put aside too, since the game writes earlier play's compute programs into a new list. The game gets none of the shell's `HOS_*`/`RX_*`/`A64_*`/`OOT_*` switches (`run_environ`), an empty `HOS_ENV_FILE` instead of the folder's `longshot_env.txt`, and its own `--user` folder for the saves. `game_exe` is `scripts/longshot_ctl.py`'s: `bin/longshot` in a builder's folder, whose top-level `longshot` starts the launcher. - **Waits** (this head): the teleport's 8 s refusal is wall time, so it's waited out and no longer pressed at. The lake-bottom teleport, to the map Link is already in, waits for its load to end (`reload`). `calm` takes only a fresh snapshot (`game.frame` within 10 presents of the reply's, as `docs/control.md` asks). - **`--publish`** (this head): `tour.md` names no path of the run's (`RX_VK_SHOTS`), and its settings are the options the runs had (their `--ini`), not `BASE_INI` alone. - **Main's game folder:** the pipeline notes and the lists play adds to are in the game folder's `cache/` now (`host/game_layout.c`, `vk_draw.cpp` `state_list_path`): the tour puts `cache/longshot_pipelines.bin`, `cache/longshot_pipeline_states.bin` (and `_v1`), the shipped list beside the game, and any loose ones an older build left, aside for a run, and reads `cache/longshot_pipeline_states.bin`. - **Renamed to Longshot:** `longshot_*` files, `longshot_options.ini`, the `longshot` binary. The Sway window-moving code is dropped (`--window WxH` just shows a window). `--cpus` has no PC-specific default. - **scripts: the renderer's play profile, `--vk-profile <dir>`** on `run` and `tour` (`Profile`, `profile_end`, `counting_renderer`, `merge_profile`): - The game folder's renderer must count (`RX_VK_PGO=gen`: its `longshot_vk.so` names `RX_VK_PGO_FILE`); otherwise `run` and `tour` refuse before touching anything. - Each game runs with `RX_VK_PGO_FILE=<run dir>/vk.profraw`. Once `new_game` has control of Link, the tour creates `<file>.reset` and waits for the renderer to take it (`host/pgo_rt.c`, every 500 ms), so the loading, the prepare and the title are left out. A renderer that doesn't take it within 10 s fails the run. - At each game's end, failed or not (a hard memory stop waits 3 s), it creates `<file>.dump`, waits up to 10 s for the counters and moves them to `<dir>/<game>.profraw`, with `<game>.json` beside it (part, GPU and driver, build, seconds). A dump that writes nothing fails the run, so a recording never ends empty. A game that never reached control keeps nothing. - `merge-profile <dir>` runs the toolchain's `llvm-profdata merge -o work/longshot_vk.profdata <dir>/*.profraw` (`third_party/a64-kit/tools/toolchain.py` `find("llvm-profdata")`, or `--llvm-profdata`; `--out` elsewhere). It refuses a folder with no `.profraw`, a file that isn't LLVM's raw format, one with no counted functions, and a merge that writes nothing. - `merge-profile --publish` also writes `assets/profiles/longshot_vk.profdata.xz` (Python's `lzma`, preset `9 | PRESET_EXTREME`, as `xz -9e`) and the lines for `assets/profiles/README.md` (date, parts, games, build, GPU and driver, OS and CPU, the LLVM version) to `<dir>/README-note.md`. - With the option off, nothing changes: no variable, no reset or dump, the same run commands and reports. The `RX_VK_PGO_FILE` path is kept out of `tour.md`'s settings. - **tests: `tests/test_pipeline_tour.py`**, game-free. - **Source checks:** the actions and teleport table against `host/hos_main.c`, `gpu/vk/vk_menu.cpp` and `gpu/vk/oot_cheat_items.h` (the Hookshot is checked again: main fixed its name). The bridge commands and status fields are checked against `host/control.c` and `host/oot_state_tap.c` (status's `game` now comes from the sampled state), and the `units: hos` steps against `scripts/longshot_ctl.py`. The lists' places are checked against `vk_draw.cpp`, `vk_paths.h` and `game_layout.c`. `RX_VK_PGO_FILE`, `.reset`, `.dump` and the raw header are checked against `host/pgo_rt.c` and `nx2/build.py`. Each of the game's teleport refusals must be either retried or given up on. Every `BASE_ENV` switch must exist in the source, and every `BASE_INI` key must be one an Insert menu row writes. - **Route and input:** the route and chunks, and the presses' buttons and spans. The item stops are checked: once per age at the first stop of that age's overworld, plus `ITEMS_PASSES` (each one visited and with a reason). The picks must all differ, and a game-free `play` must use the items at exactly those stops and settle by the run's `--settle-quiet`/`--settle-max`. - **Command line:** the defaults (`--vk-profile` off), refusals, `--dry-run` (which marks the item stops), the switches and the tour's per-game command (with and without `--vk-profile`, now with the settle's frames). Also checked: the speed settings in every part's game, the settle's defaults (in frames, under the old 150), `--settle-quiet 150` passed on by `tour`, `--compare-to`, and that the only wall-clock sleeps left are the live lines' gap and the bridge's start poll. - **Lists:** list counts, `merge`, `compare` and `list_diff` (lost and gained both ways, nothing lost against a merge's own input), `set_ini`, the report (now with its settle line) and `--publish` (no `RX_VK_PGO_FILE` in its note). - **Bridge:** a fake bridge (a Unix socket served from a thread, as in `tests/test_longshot_ctl.py`) behind the real `scripts/longshot_ctl.py` client, taking frame steps as `host/control.c` does. It covers a new game to control, a teleport refused twice and retried, a refusal that is final, a teleport that never moves, the age switch, `settle` (the old 150 frames and the new default), and the inventory through the live file (three frames per line). - **Renderer profile:** a counting renderer told apart from another; `run` and `tour` refusing a renderer that doesn't count, before touching the game folder; a stand-in for `pgo_rt.c`'s writer, against which stale files are cleared, the reset comes right after control (and not at all without the option), and the dump is moved to `<dir>/<name>.profraw`. A writer that takes neither request fails the run. `merge-profile` is checked with a stand-in `llvm-profdata`: the command, `--out`, `--publish`'s xz and note, and its four refusals. - Also the memory guard and `value`. - **docs:** `docs/development.md` gets a new section, "The pipeline tour", and "The renderer's play profile" now gives the tour's recording and `merge-profile` in steps 2 and 3. `assets/pipeline_states/README.md` now names the tour command. ## Measurements **Verdict:** not measured yet. The tour has not been run on this branch, with or without `--vk-profile` (WIP until it is). Hardware: to come (the maintainer's local run, Linux) Scene: the whole tour route, from a new save Method: `tour` wall time and `report.txt` per part, against the draft's numbers. For this head's faster route: one tour at c23b44f3 (items at 16 stops, 150-frame settle) and one at this head on the same PC and game folder, then `compare` (expected: 0 lost). For the profile, the recording merged and published, then frame time as in `assets/profiles/README.md` (the Market save, standing and running, 3 sessions each), main's profile vs the tour's How to reproduce: a game folder built from main (or a counting runtime for the profile), on Linux, with the commands below ``` python -I scripts/pipeline_tour.py tour --game <game folder> --out <tour dir> --name play --publish RX_VK_PGO=gen python -I scripts/dev_packs.py pack <base> vk-count python -I scripts/dev_packs.py rt <base> <runtime> vk-count python -I scripts/pipeline_tour.py tour --game <runtime> --out <tour dir> --name play --vk-profile <tour dir>/vk python -I scripts/pipeline_tour.py merge-profile <tour dir>/vk --publish python -I scripts/pipeline_tour.py tour --game <game folder> --out <old dir> --name play (the route before, from a workspace at c23b44f3) python -I scripts/pipeline_tour.py tour --game <game folder> --out <new dir> --name play --compare-to <old dir>/play.bin python -I scripts/pipeline_tour.py compare <new dir>/play.bin <old dir>/play.bin ``` Baseline: main has no tour script, so the before column is the Hookshot draft's own runs (https://github.com/NotRexed/Hookshot/pull/2048), on main's game otherwise. | | Before: main @ fe5c60d0 (no tour; the Hookshot draft's runs, RTX 3090, Linux, partial) | After: this PR @ d41a45bd | |---|---|---| | A stop | 12-20 s | to measure | | Speed | 1.8x real time at ~107 fps (cap 240) | to measure (Uncapped) | | Full tour, default parts | ~35-40 min (estimated from those runs: 47 stops, 8 games of up to 8 stops, the opening) | estimated ~18-23 min | The estimate assumes the stops cost about what they did in the draft. Most of the saving is per game: the game skips its own opening instead of being pressed through it, and there are no 10 s resend waits. This head saves about 8,000 game frames on top of that, estimated from the code: 48 settles × 90 frames, plus 6 fewer item uses × ~630 frames (the pick plus the sword, shield and item, ~10.5 s of game time). That's ~135 s of game time, or ~45-75 s of wall time at 107-180 fps. It also saves ~25 s of `!cheat` gaps (12 inventory steps × ~60 lines × ~37 ms), so ~1-2 minutes in all. Each stop also waits out the game's 8 s between teleports (wall time), so a stop costs at least 8 s however fast the game runs: only a run can say how much of the estimate is left. ``` (the runs' report.txt lines go here) ``` ### Flags and settings None added to the game: the `HOS_*` and `RX_VK_*` names the script sets for its games (`BASE_ENV`, `HOS_ENV_FILE`, `HOS_CONTROL`, `RX_VK_PGO_FILE`) are existing switches, each checked against the source by `tests/test_pipeline_tour.py`. The script's own options (`--vk-profile`, `merge-profile`) are documented in `docs/development.md`, "The pipeline tour" and "The renderer's play profile". ## Testing **Tested:** - The previous head (e182b7e8, on main b9cdc432): `python -I -B tests/run_all.py` on Linux (CachyOS, clang 23), 493 passed, 2 failed (the same on main: `tests/test_bake_portability.py`, `tests/test_builder_log_read_only.py`), 36 skipped. `run --dry-run` printed the route. - c23b44f3: the rebase onto main 5d3ecc78 had no conflicts. Every source text the checks read was looked up by hand in main's files. - 84421c99: read, not run. - This head (d41a45bd, rebased onto main fe5c60d0 with no conflicts): a code review of the whole PR (in a separate session) found 20 points; the fixes for the confirmed and plausible ones are in this head, with checks in `tests/test_pipeline_tour.py` (`folder_kept`: the set-aside and put-back, the options file, `game_exe` with `bin/`, `--cpus`, `run_environ`, `published_settings`, `run_child` for Ctrl+C and SIGTERM; the fake bridge: a stale snapshot, the 8 s refusal). Read and traced by hand, not run. **Not tested:** - This head's `python -I -B tests/run_all.py`, `tests/test_pipeline_tour.py` included: no checks are run on the author's PC. - Windows: the script is Linux only (the memory guard reads `/proc`, the bridge is a Unix socket, the copy is `cp --reflink`), as its docs say; `tests/run_all.py` skips its check there. - The game: no tour run yet, no `--vk-profile` recording, and no A/B of this head's route against c23b44f3's. **Must be tested:** - Linux, the maintainer's PC (by hand): `python -I -B tests/run_all.py`, expecting `tests/test_pipeline_tour.py` to pass. - Linux, the lab (by hand): a tour at c23b44f3 and one at this head on the same game folder, then `compare <new>/play.bin <old>/play.bin`. Expected: 0 lost, and the new tour's wall time (its last line) shorter. - Linux, the maintainer's PC (by hand): a counting pack's runtime, `tour --vk-profile`, then `merge-profile --publish`. Expected: one `.profraw` per game in `<tour dir>/vk`, "renderer profile: <file>" in each `report.txt`, and the `.xz` and its note written. The pipeline lists should come out as without the option. ## Risks **What could break:** only the script. A teleport refusal the tour should retry but treats as final (or the other way round), or a stop that misses: a run says so in its `report.txt` and `steps.txt`, and three misses in a row end the run. With `--vk-profile`, a renderer that is slow to take `.reset` or `.dump` (more than 10 s) fails its run rather than recording too little. **Turning it off:** leave `--vk-profile` out; revert the commits for the rest. **Compatibility:** Nothing changes. The game is unchanged, and the committed profile changes only when someone runs `merge-profile --publish` and commits it. ## Issues See https://git.magicbean.dev/MagicBeanDev/Longshot/issues/14 and https://git.magicbean.dev/MagicBeanDev/Longshot/issues/505. Also https://github.com/NotRexed/Hookshot/pull/2048, the draft this ports; https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/569, the renderer profile it records again. ## Reviewer notes - `run`'s `try`/`finally`, `put_back` and `run_child` are what keeps a player's game folder as it was: worth the closest read. - `map_change` and `new_game` are the parts that replace the draft's log polling. `PERMANENT` is checked against the game's own refusal texts. - `ITEMS_ONCE`/`ITEMS_PASSES` and `item_stops` decide where the items are used. If `compare` shows pipelines lost, the place to look is the item picks: 10 differ now, where before each game started again from the first (2-4 item uses a game). - `Profile.reset` and `profile_end` are the profile's two moments. The reset waits for the renderer to delete `.reset`, which only a counting renderer does, so a wrong game folder fails at its first control and not after a whole tour. - The second commit holds the rebase's follow-ups (main's `cache/` folder, the sampled `status.game`, the client's frame steps) together with `--vk-profile`. - **Follow-ups:** - The game-side play profile (`hot_funcs.txt`, `hot_order.txt`, `longshot_pieces.prof`, from an `RX_PGO=gen` build) is still parked. - The `extras` part (boss rooms through a teleport-to-map command). GummiNaomi's comment on this PR offers a host change for it. - A bridge command for the Debug tab's inventory, which would retire the last `HOS_INPUT_LIVE` use. ## Checklist ### Everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`) of the status line's type. - [x] Why gives the reason plainly and links the issues that fit (the tracker searched). - [ ] Measurements has a verdict, or one line saying why there is nothing to measure. (not yet: the tour hasn't run on this head) - [x] Testing names what was tested, what wasn't, and what must still be tested, where and how. - [x] Rebased on current `main`. - [ ] `python -I -B tests/run_all.py` passes (any failure that also fails on `main` is named). (not run on this head: no tests are run on this PC) - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### When it applies - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [ ] Numbers from the same PC, scene and settings, at least 2 interleaved runs each, the median reported. (to come with the tour's run and the profile's frame times) - [x] Switches documented (docs/development.md; the README for a menu row) and shipped at their recommended value. - [ ] A file in `docs/tested/` for each new measured run (format in `docs/tested-configurations.md`). (to come with the run) - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [x] Docs updated. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(scripts): the pipeline tour, driven through the control bridge
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 2s
PR description / Description follows the template (pull_request_target) Successful in 2s
CI / Workflow lint (actionlint) (pull_request) Failing after 4s
Third-party parts / As their notes say (pull_request) Successful in 5s
Website / Website on Cloudflare (pull_request_target) Has been skipped
e182b7e84b
Ported from Hookshot's draft #2048 (NotRexed/Hookshot) by HumanBean: scripts/pipeline_tour.py
plays the game from a new save through every Teleport destination at both ages and writes the
tour's pipeline state list (assets/pipeline_states/tour.bin.xz and tour.md, through
nx2/pipeline_states.py tour). It now drives the game through the control bridge (teleport, age,
sequence, wait, status) instead of the live input file and the log; only the Debug tab's
inventory still goes through HOS_INPUT_LIVE's !cheat lines. It runs uncapped, headless, at
640x360, with the game skipping the opening itself in every part but the opening's.

tests/test_pipeline_tour.py checks it without the game, against a fake bridge.
Contributor

Some data for this PR's route, from a tour I ran with my own script over the last two days (Windows, RTX 4070 SUPER, driver 617.42; a build of af15c4a5 plus the host change below). I won't open a competing tour PR. What I'd send instead is below.

What a wider route finds. I recorded cold (RX_VK_PRECOMPILE=0, no notes file) and compared against the shipped sources at 514591a3 (42,072 records, 233 compute programs):

Part Records not in the shipped list Compute programs among them
Every map at every start position of Banc/BancBasedResource/StartPos (370), child and adult, a camera turn each 9,390 782
Every actor type that has a model (2,086 of ActorInfo), made 16 at a time beside Link 2,798 195
Cutscenes, requested by name (480 Dm_* event flows) 9,476 986
Menus (items, system, equipment, collection, map, world map, threads of time, key items) 94 3
A second pass of the maps 90 0
Together, default settings 21,848 (+52%) 1,966

The menus add almost nothing because play already covers them.

What the wider route needs from the host. It's one opt-in change in host/hos_main.c, unused unless the variable is set:

  • HOS_OOT_TELEPORT_EXTRA=<file>: more destinations after the table's, which the bridge's teleport reaches by index. Each line is either <child banc> <child start> <adult banc> <adult start> [boot event], event <name> or actor <name>. An actor line makes up to 16 actors on a ring around Link through the actor manager and deletes them after a few seconds.
  • HOS_OOT_TOUR_SKIP_EVENTS=1: in play only, while an event plays, takes each event's skip edge, so a song lesson doesn't wait for the ocarina.

That is the "extras" follow-up this PR names. If you want it, I'll send it as its own PR for this script to use, or adapt it to whatever bridge command you'd rather have.

Things the run hit that this PR's route may hit too:

  • After the Haunted Wasteland, the Colossus' Spirit Temple entrance and the collapsing Ganon's Castle, the event manager refuses every teleport until the game restarts. More than half of the cutscenes leave it that way too (288 restarts over 480).
  • In the menus, PLUS opens the items page and R/L step only as far as each end; MINUS opens the map page. The game ignores menu buttons for about 6 s after an arrival, while the place's name shows.
  • The tour reaches 2,199 compute programs with the shipped ones, so pipeline_states.py check stops at COMPUTE_MOST (1024).

What I'll send: a contributed list (contrib/gumminaomi-…), so it doesn't touch tour.bin.xz, with COMPUTE_MOST raised. It will include a main vs PR measurement of the first start, the memory the listed compute programs hold, and later starts, as CONTRIBUTING asks. That is WIP until the measurement is in.

Some data for this PR's route, from a tour I ran with my own script over the last two days (Windows, RTX 4070 SUPER, driver 617.42; a build of af15c4a5 plus the host change below). I won't open a competing tour PR. What I'd send instead is below. **What a wider route finds.** I recorded cold (`RX_VK_PRECOMPILE=0`, no notes file) and compared against the shipped sources at 514591a3 (42,072 records, 233 compute programs): | Part | Records not in the shipped list | Compute programs among them | |---|---|---| | Every map at every start position of `Banc/BancBasedResource/StartPos` (370), child and adult, a camera turn each | 9,390 | 782 | | Every actor type that has a model (2,086 of `ActorInfo`), made 16 at a time beside Link | 2,798 | 195 | | Cutscenes, requested by name (480 `Dm_*` event flows) | 9,476 | 986 | | Menus (items, system, equipment, collection, map, world map, threads of time, key items) | 94 | 3 | | A second pass of the maps | 90 | 0 | | **Together, default settings** | **21,848 (+52%)** | **1,966** | The menus add almost nothing because play already covers them. **What the wider route needs from the host.** It's one opt-in change in `host/hos_main.c`, unused unless the variable is set: - `HOS_OOT_TELEPORT_EXTRA=<file>`: more destinations after the table's, which the bridge's `teleport` reaches by index. Each line is either `<child banc> <child start> <adult banc> <adult start> [boot event]`, `event <name>` or `actor <name>`. An actor line makes up to 16 actors on a ring around Link through the actor manager and deletes them after a few seconds. - `HOS_OOT_TOUR_SKIP_EVENTS=1`: in play only, while an event plays, takes each event's skip edge, so a song lesson doesn't wait for the ocarina. That is the "extras" follow-up this PR names. If you want it, I'll send it as its own PR for this script to use, or adapt it to whatever bridge command you'd rather have. **Things the run hit that this PR's route may hit too:** - After the Haunted Wasteland, the Colossus' Spirit Temple entrance and the collapsing Ganon's Castle, the event manager refuses every teleport until the game restarts. More than half of the cutscenes leave it that way too (288 restarts over 480). - In the menus, PLUS opens the items page and R/L step only as far as each end; MINUS opens the map page. The game ignores menu buttons for about 6 s after an arrival, while the place's name shows. - The tour reaches 2,199 compute programs with the shipped ones, so `pipeline_states.py check` stops at `COMPUTE_MOST` (1024). **What I'll send:** a contributed list (`contrib/gumminaomi-…`), so it doesn't touch `tour.bin.xz`, with `COMPUTE_MOST` raised. It will include a main vs PR measurement of the first start, the memory the listed compute programs hold, and later starts, as CONTRIBUTING asks. That is WIP until the measurement is in.
mooki left a comment

Review of the head commit e182b7e84b79 (full hash e182b7e84b7969de8ba955ab5d3184ca9d69bca4), read against its own base b9cdc4326f3b and against current main 7ff5256e0006, which is 125 commits ahead of that base.

I did not run the game (no GPU run on this side yet). What I did: read the script, the test, the docs and the code they depend on at both mains; ran the PR's test on the head and on the head merged onto main 7ff5256e0; broke the script and the game source on purpose, one thing at a time, and ran the test again; and ran the real scripts/pipeline_tour.py end to end against a stand-in for the game (a small program that serves the bridge with the test's own fake and writes a pipeline list where I tell it to). Links to the PR's files are pinned to the head commit; links to main are pinned to 7ff5256e0.

The route logic, the chunking and the resume after a memory stop are careful, and the source checks that exist do break when the game's strings change (I tried renaming a refusal text, the !cheat prefix and the teleport action number in the game source, and changing the five-send rule and the PERMANENT list in the script: each fails the test). Two things stop it from merging as it is, both because main moved after the PR was written, and there are several things I would fix before the first real run.

Blocking

1. Main's game writes the pipeline list and notes in cache/ now; the script still uses the old place.
Main 76699aad3 (#321, 14 November) moved longshot_pipeline_states.bin, longshot_pipelines.bin and longshot_computes.bin into <game>/cache/ (state_list_path, game_layout.c); only the shipped list stays beside the game. The script reads, counts, moves aside and copies the names at the game folder's root (STATE_FILES and LIST, used by list_size, settle, the move aside and the put back).
With the stand-in writing the list in cache/ as main's game does, run finished with exit 0 and 0 pipelines, 0 programs at every step, no states.bin, the old cache/ list left in the game folder and appended to, and the notes not moved aside. settle then waits exactly its 150 quiet frames at every stop, and tour --publish prints "not published (a run failed or made no list)" but returns 0. With the list beside the game (the PR's own base) the same stand-in gave 3 pipelines. A real run would show it in a minute; a short real run is the thing I would do next (see the end).
Fix: use cache/ for the notes, the list and longshot_computes.bin, the game folder for the shipped list, and fail loudly when the list is missing or empty after the first stop.

2. tests/test_pipeline_tour.py fails once this is on main. It passes at the PR's base. Merged onto 7ff5256e0 (no text conflict) it fails twice:

  • the status check searches for the literal "game":{"event":%s,"loading":%s,"age":"%s"} in host/control.c; #58 (1ea02558e) moved that into status_game. The fields are all still there.
  • the fake bridge answers bad_request: units to a sequence without units: "hos". Since #47 (8e8f04c85) the real client turns that script into steps before sending (Bridge.request). The real bridge takes steps, so the script is fine and the fake is stale.

So "Rebased on current main" and "run_all.py run and passing" were true for b9cdc432 and are not now. A rebase and these two edits fix it.

Should fix

3. The test cannot fail for most of what the script does. Twenty-one breaks, one at a time: it fails for 7 (the PERMANENT list, the five-send rule, the refusal texts, the action number, the !cheat prefix, --settings). It still passes with: a wrong list file name (the test makes its own list under tour.LIST), the game never asked to quit, the memory guard never stopping a running game (only the text raise MemoryStop(why) is searched, line 589), the soft limit never firing, a game that ended never noticed, a timeout reply treated as final, the age switch waiting for map instead of loading_end, the bridge losing wait loading_end, or losing the sent and map fields of its replies. The bridge half of the test is the test's own fake, so the description's "the bridge commands and status fields against host/control.c" holds for command names and one status string only. Six of the 14 BASE_INI keys are in a hard-coded set and would pass if the reader dropped them. The fake also invents a renderer string with a driver in it (point 5). A stand-in game driving run once, as I did, would catch most of these.

4. A game that exits during a request ends the run with a traceback and no report. Game.ask lets longshot_ctl.Failed through ("the game closed the connection"); run catches only the script's own Failed, a different class. With the stand-in ending the process during the second teleport: a traceback, exit 1, states.bin kept, but no report.txt or report.json (they are written after the finally), and in tour part_runs returns, so the part's remaining games never run, although a memory stop already resumes in a fresh game. A game that dies between requests is counted as three missed stops instead. A hung game (device lost, no exit) costs, by my reading of Game.frames, about 5 minutes a stop; status.stalled is not used. Fix: catch the client's Failed in ask, write the report in the finally, and resume from the stop where the game ended, with the "twice at the same stop: left out" rule.

5. tour --publish would write a local path into tour.md, and the GPU line has no driver.

  • publish keeps every HOS_OUTPUT*, HOS_FPS* and RX_VK* variable of the run, and run sets RX_VK_SHOTS to the run folder's absolute path. The note I got from the stand-in has RX_VK_SHOTS= with the absolute path of the run folder's shots in its Settings line, plus the screenshot variables. That file is committed.
  • status.renderer is <device name> (Vulkan, native) (describe), with no driver. publish uses it whenever it is not empty, and its fallback regex does not match main's device log line (driver %#x). The current tour.md says "driver 620.70.06 (Linux)" and the README asks for it. The test's fake says Vulkan: Fake GPU, driver 1, which hid this.
    Fix: pass only the settings that matter (output size, the fps variables, the ini), and give tour --gpu and --build options that win, or read the driver from the log line as main prints it.

6. The pause menu is driven with ZL and ZR; this repo says L and R turn its pages. pause_pick, UNDERWATER_GEAR, BOOTS_OFF, PAUSE_TOUR use ZL and ZR. host/oot_menu.c: "0 Collection, 1 Equipment, 2 Items, 3 System; L and R step through them", and the mouse tabs send 0:L/120 and 0:R/120 (558-575); the README names "the L and R signs". GummiNaomi's run says the same (PLUS opens the items page, R and L step to each end) and adds that the game ignores menu buttons for about 6 seconds after an arrival; the script presses PLUS 3.7 seconds after the arrival event ends (LOOK). If ZL and ZR do not turn pages, the item pick at ten stops per age and the iron boots and Zora tunic do not happen, and nothing in the script notices (a ZR press in the world uses the item button, an A may start a talk).
Question: what did #2048's runs show, a picture of the Items page after the pick? If it is L and R, the three scripts need changing, the 6 seconds need respecting, and a bridge shot or the state should confirm that the menu opened.

7. The forced settings can be undone at start, and the environment is not pinned. BASE_INI has no quality_preset and no handheld_profile. With an options file present and quality_preset=6 (Auto), menu_auto_for_gpu runs apply_auto when the saved fingerprint differs, and the fingerprint includes the screen size (auto_settings.h); a headless Linux run has no screen. apply_auto then sets the upscaler, render resolution, sharpness, pacing and Game speed and releases the environment overrides, including HOS_REALTIME_STEP=0. A copy of a game folder that was ever played in a window has exactly that file. handheld_profile=1 makes both output_res and HOS_OUTPUT_RES be ignored (nn_applet.c). The game also reads the copy's longshot_env.txt and the caller's environment; its own comment says HOS_ENV_FILE is for "a scripted or A/B test run" (hos_main.c). I read this, I did not run it. Fix: put quality_preset (Custom) and handheld_profile=0 in BASE_INI, set HOS_ENV_FILE to an empty file, and copy the game's own settings lines into report.txt.

8. "None of these change a pipeline's key" is true for what a record holds, not for which records are made. A record has code hashes, texture answers, mesh flag, topology, colour count, samples, formats and draw state: no size, rate, filter or upscaler (StateRec2), and frame rate, V-Sync, frame generation, sharpness and the window filter are present-only. But the game runs a different upscale compute network for each render share (vk_temporal.h) and compute programs are recorded. The tour at 50% with the game's own upscaler records the 50% network. Auto sets 100% for Balanced, High and Ultra and 75% for Low (kTierResolution), so most players on the game's own upscaler run a network the tour never records. (DLSS and FSR replace those dispatches, so dlss=0 is the right base.) Native HDR changes formats and cannot be reached headless; the docstring's --ini hdr=1 (line 32) is the host-only mode. Question: should the tour also run at 100% and 75% (--ini resolution=1 and 2), or say which tier it covers? value would show what each pass adds.

9. The tour's list also takes the copy's own earlier compute programs. state_open_locked adds a compute record for every key in longshot_computes.bin, "what this install's own play dispatched". That file is not in STATE_FILES (line 63); at the PR's base it is beside the game and equally kept. So "only what this run meets counts" and the README's "a function of the build and the tour's route" do not hold for compute programs; they also count against COMPUTE_MOST. Move it aside too, or tour from a folder with no cache/.

10. Unstick presses start during the host's 8-second lockout, which is not an event. In map_change, after 240 frames of refusals every round presses A, B, stick+A and so on. The host refuses a teleport asked less than 8 seconds of wall clock after the last one, with no event playing (hos_main.c). 240 frames is 4 seconds at 60 fps and under 1 second at 300 fps, so A and B go out in the first half of a lockout, earlier the faster the run. Against the test's fake: 3 refusals gave no press, 10 refusals gave 0:A/100 and 0:B/100. The script's own comment says A next to someone starts a conversation. Whether real stops come within 8 seconds of each other is open. Fix: recognise "one every 8 s" and just wait on the wall clock; keep unstick for an event that lasts.

11. Items. Question. The unlock the script sends is a toggle: for an item already held it takes all of it away (hos_main.c, "unlock / relock"). The route sends the whole set again after the age switch. Do the items the child pass gave survive the switch, and does the second pass then remove them? Also !cheat writes one slot (nn_hid.c); a later line replaces one the game has not taken yet, and it takes one per Link update (not during a load or the pause menu). The script's 70 ms gap (cheat) has no margin for a hitch, and a tour with no pipelines prebuilt is full of them. The game logs cheats: inventory <name> <delta> (had N, now M ...) for each edit that lands; waiting for that line before the next one (the bridge's subscribe takes a log filter) would answer both questions and make "every item" true. This one I could not settle without the game.

12. A wedged event manager (GummiNaomi's data) is retried for minutes and ends the part. Questions. She sees the event manager refuse every teleport until a restart after the Haunted Wasteland, the Colossus' Spirit Temple entrance and the collapsing Ganon's Castle. The refusal then is "an event is playing" (hos_main.c), which PERMANENT treats as passing: 7200 frames of retries with unstick presses per stop, three misses, the run ends, and (point 4) so does the part. The PR describes it as "sent, the map does not change" and a five-send rule that the default route never reaches; she sees refusals. The route puts the Haunted Wasteland last in its age, which protects the default tour parts, but run --part all (the default of run) puts the age switch right after it (the dry run shows visit Haunted Wasteland (child), then age). Which destinations in the table wedge it? "The collapsing Ganon's Castle" could be Hyrule Castle as the adult (index 4, banc GanonCastle), which is the 4th of 15 stops of adult-overworld with 11 after it, or Ganon's Castle (index 30), the last stop of the dungeons part. Either way, a refusal that lasts more than some seconds could be taken as a wedge: stop the game and go on from that stop in a fresh one, as the memory path does.

Smaller

  • Statements in the description and docs that are not true or no longer:
    • The note about Item_Hookshot becoming a second Item_Longshot (Reviewer notes) was fixed on main by #117 (3cbf12e8f, 13 November): rows 37 and 38 are Hookshot and Longshot again. The test still checks Item_Longshot only (line 96).
    • "Game speed Per frame": that row does nothing above 60 fps (nn_vi.c); HOS_FPS_STEP=0 is what gives a 60th of a second a frame, and that is a code comment, not something a test or a run shows.
    • "V-Sync is off": vsync=0 is Auto (0 Auto, 1 On, 2 Off); harmless headless. "The game's own upscaler" for upscaling=0: that row is the window scaling filter; the Upscaler is dlss.
    • Measurements: the baseline column is "Hookshot #2048 (RTX 3090 ...)" with no hash, no Baseline: line and no line on why main was not the comparison (CONTRIBUTING, Measurements). The estimates are marked as estimates, and I could not check them; leaving Measured unticked is right.
  • set_ini restores only an options file that existed; with none, the forced one stays in the game folder (the docs say "restored after").
  • cp -a --reflink=always fails with a traceback on a file system without reflinks; say so in --help or use --reflink=auto.
  • A run with missed stops (1 or 2 in a row) still exits 0 and --publish publishes it; the misses are only in report.txt. tour also replaces tour.bin.xz whole with no count of the pipelines of the current one (11,227) that the new run lacks, and does not run nx2/pipeline_states.py check (GummiNaomi: her tour reaches 2,199 compute programs with the shipped ones and check stops at 1024).
  • The underwater step teleports to the map Link is in, with want = lambda st: True (line 888); wait map is met at once when the map is already that one, so the arrival is not confirmed, only 300 frames waited.
  • A stop dropped for memory (twice at the same stop) is in a printed line only; the report and tour.md do not say so, and floor_gb counts the whole machine, so another program can drop a stop.
  • Question: LEFT_OUT says "Lost Woods (child): 20 pipelines no other stop met" and the next comment says under 10 is not worth a stop (lines 153-167). Which is it?
  • Question: CONTRIBUTING says feat is "something new for players or modders" and makes the next minor release. This adds a maintainer script and docs and ships no list. Is chore(scripts) meant?

GummiNaomi's data against the route

The route is 47 stops (14 of the 61 destination and age pairs are left out), no actors and no cutscenes; her wider run finds 21,848 records (+52%) more than the shipped list, of which maps at every start position are 9,390, actors 2,798, cutscenes 9,476, menus 94. Her "menus add almost nothing" fits the PR's PAUSE_PAGES = False. The wider route is the PR's own follow-up, and her host change (HOS_OOT_TELEPORT_EXTRA, by index) would fit the bridge's teleport; that is for the maintainers. For this PR the useful parts of her data are points 6 (the menu buttons and the 6 seconds) and 12 (the wedge), and the COMPUTE_MOST note under Smaller. The tour list is replaced whole, so everything the old tour met and this route does not is lost; that is worth a count in the report.

Short runs that would settle what is open (not done: no game run on my side)

One game, windowless, a copy of a game folder with its own cache/:

  1. run --part child-overworld --stops 1 --skip-opening: does cache/longshot_pipeline_states.bin grow, what does the report say, and are cache/longshot_computes.bin records in the list (points 1 and 9).
  2. The same run with HOS_FPS_LOG=1: the vi: game clock ... (speed S) at F fps line should give S about F/60.
  3. A stop in ACT_AT (Kokiri Forest) with a bridge shot after the pause presses, once with ZL/ZR and once with L/R; and a copy whose options file has quality_preset=6 and the auto_for of a windowed run, to see vk: Auto (this PC): the hardware changed (points 6 and 7).
  4. --part adult-overworld --stops 1, then grep 'cheats: inventory' log.txt: how many of the 56 landed, and any now 0 after the age switch (point 11).
  5. --part adult-overworld through Hyrule Castle (adult) and one stop after it (point 12).
Review of the head commit `e182b7e84b79` (full hash `e182b7e84b7969de8ba955ab5d3184ca9d69bca4`), read against its own base `b9cdc4326f3b` and against current main `7ff5256e0006`, which is 125 commits ahead of that base. I did not run the game (no GPU run on this side yet). What I did: read the script, the test, the docs and the code they depend on at both mains; ran the PR's test on the head and on the head merged onto main `7ff5256e0`; broke the script and the game source on purpose, one thing at a time, and ran the test again; and ran the real `scripts/pipeline_tour.py` end to end against a stand-in for the game (a small program that serves the bridge with the test's own fake and writes a pipeline list where I tell it to). Links to the PR's files are pinned to the head commit; links to main are pinned to `7ff5256e0`. The route logic, the chunking and the resume after a memory stop are careful, and the source checks that exist do break when the game's strings change (I tried renaming a refusal text, the `!cheat` prefix and the teleport action number in the game source, and changing the five-send rule and the `PERMANENT` list in the script: each fails the test). Two things stop it from merging as it is, both because main moved after the PR was written, and there are several things I would fix before the first real run. ## Blocking **1. Main's game writes the pipeline list and notes in `cache/` now; the script still uses the old place.** Main `76699aad3` (#321, 14 November) moved `longshot_pipeline_states.bin`, `longshot_pipelines.bin` and `longshot_computes.bin` into `<game>/cache/` ([`state_list_path`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/gpu/vk/vk_draw.cpp#L1729-L1732), [`game_layout.c`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/host/game_layout.c#L33-L37)); only the shipped list stays beside the game. The script reads, counts, moves aside and copies the names at the game folder's root ([`STATE_FILES` and `LIST`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L63-L65), used by [`list_size`, `settle`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L553-L583), the [move aside](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L934-L948) and the [put back](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L999-L1006)). With the stand-in writing the list in `cache/` as main's game does, `run` finished with exit 0 and `0 pipelines, 0 programs` at every step, no `states.bin`, the old `cache/` list left in the game folder and appended to, and the notes not moved aside. `settle` then waits exactly its 150 quiet frames at every stop, and `tour --publish` prints "not published (a run failed or made no list)" but returns 0. With the list beside the game (the PR's own base) the same stand-in gave 3 pipelines. A real run would show it in a minute; a short real run is the thing I would do next (see the end). Fix: use `cache/` for the notes, the list and `longshot_computes.bin`, the game folder for the shipped list, and fail loudly when the list is missing or empty after the first stop. **2. `tests/test_pipeline_tour.py` fails once this is on main.** It passes at the PR's base. Merged onto `7ff5256e0` (no text conflict) it fails twice: - [the status check](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/tests/test_pipeline_tour.py#L56-L57) searches for the literal `"game":{"event":%s,"loading":%s,"age":"%s"}` in `host/control.c`; #58 (`1ea02558e`) moved that into [`status_game`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/host/control.c#L885-L892). The fields are all still there. - [the fake bridge](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/tests/test_pipeline_tour.py#L340-L342) answers `bad_request: units` to a `sequence` without `units: "hos"`. Since #47 (`8e8f04c85`) the real client turns that script into `steps` before sending ([`Bridge.request`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/scripts/longshot_ctl.py#L161-L163)). The real bridge takes `steps`, so the script is fine and the fake is stale. So "Rebased on current `main`" and "`run_all.py` run and passing" were true for `b9cdc432` and are not now. A rebase and these two edits fix it. ## Should fix **3. The test cannot fail for most of what the script does.** Twenty-one breaks, one at a time: it fails for 7 (the `PERMANENT` list, the five-send rule, the refusal texts, the action number, the `!cheat` prefix, `--settings`). It still passes with: a wrong list file name (the test makes its own list under `tour.LIST`), the game never asked to `quit`, the memory guard never stopping a running game (only the text `raise MemoryStop(why)` is searched, [line 589](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/tests/test_pipeline_tour.py#L589)), the soft limit never firing, a game that ended never noticed, a `timeout` reply treated as final, the age switch waiting for `map` instead of `loading_end`, the bridge losing `wait loading_end`, or losing the `sent` and `map` fields of its replies. The bridge half of the test is the test's own fake, so the description's "the bridge commands and status fields against `host/control.c`" holds for command names and one status string only. Six of the 14 `BASE_INI` keys are in a [hard-coded set](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/tests/test_pipeline_tour.py#L118-L125) and would pass if the reader dropped them. The fake also invents a renderer string with a driver in it (point 5). A stand-in game driving `run` once, as I did, would catch most of these. **4. A game that exits during a request ends the run with a traceback and no report.** [`Game.ask`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L470-L476) lets `longshot_ctl.Failed` through ("the game closed the connection"); [`run` catches only the script's own `Failed`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L989-L992), a different class. With the stand-in ending the process during the second teleport: a traceback, exit 1, `states.bin` kept, but no `report.txt` or `report.json` (they are written [after the `finally`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L1007-L1014)), and in `tour` [`part_runs` returns](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L1081-L1082), so the part's remaining games never run, although a memory stop already resumes in a fresh game. A game that dies between requests is counted as three missed stops instead. A hung game (device lost, no exit) costs, by my reading of `Game.frames`, about 5 minutes a stop; `status.stalled` is not used. Fix: catch the client's `Failed` in `ask`, write the report in the `finally`, and resume from the stop where the game ended, with the "twice at the same stop: left out" rule. **5. `tour --publish` would write a local path into `tour.md`, and the GPU line has no driver.** - [`publish`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L1159-L1186) keeps every `HOS_OUTPUT*`, `HOS_FPS*` and `RX_VK*` variable of the run, and [`run` sets `RX_VK_SHOTS`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L951) to the run folder's absolute path. The note I got from the stand-in has `RX_VK_SHOTS=` with the absolute path of the run folder's `shots` in its Settings line, plus the screenshot variables. That file is committed. - `status.renderer` is `<device name> (Vulkan, native)` ([`describe`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/gpu/vk/vk_main.cpp#L1544-L1548)), with no driver. `publish` uses it whenever it is not empty, and its fallback regex does not match main's [device log line](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/gpu/vk/vk_main.cpp#L205) (`driver %#x`). The current `tour.md` says "driver 620.70.06 (Linux)" and the README asks for it. The test's fake says `Vulkan: Fake GPU, driver 1`, which hid this. Fix: pass only the settings that matter (output size, the fps variables, the ini), and give `tour` `--gpu` and `--build` options that win, or read the driver from the log line as main prints it. **6. The pause menu is driven with ZL and ZR; this repo says L and R turn its pages.** [`pause_pick`, `UNDERWATER_GEAR`, `BOOTS_OFF`, `PAUSE_TOUR`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L213-L244) use ZL and ZR. [`host/oot_menu.c`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/host/oot_menu.c#L60): "0 Collection, 1 Equipment, 2 Items, 3 System; L and R step through them", and the mouse tabs send `0:L/120` and `0:R/120` ([558-575](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/host/oot_menu.c#L554-L578)); the README names "the L and R signs". GummiNaomi's run says the same (PLUS opens the items page, R and L step to each end) and adds that the game ignores menu buttons for about 6 seconds after an arrival; the script presses PLUS 3.7 seconds after the arrival event ends (`LOOK`). If ZL and ZR do not turn pages, the item pick at ten stops per age and the iron boots and Zora tunic do not happen, and nothing in the script notices (a ZR press in the world uses the item button, an `A` may start a talk). Question: what did #2048's runs show, a picture of the Items page after the pick? If it is L and R, the three scripts need changing, the 6 seconds need respecting, and a bridge `shot` or the state should confirm that the menu opened. **7. The forced settings can be undone at start, and the environment is not pinned.** `BASE_INI` has no `quality_preset` and no `handheld_profile`. With an options file present and `quality_preset=6` (Auto), [`menu_auto_for_gpu`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/gpu/vk/vk_menu.cpp#L4514-L4552) runs `apply_auto` when the saved fingerprint differs, and the fingerprint includes the screen size ([`auto_settings.h`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/gpu/vk/auto_settings.h#L117-L121)); a headless Linux run has no screen. [`apply_auto`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/gpu/vk/vk_menu.cpp#L942-L962) then sets the upscaler, render resolution, sharpness, pacing and Game speed and releases the environment overrides, including `HOS_REALTIME_STEP=0`. A copy of a game folder that was ever played in a window has exactly that file. `handheld_profile=1` makes both `output_res` and `HOS_OUTPUT_RES` be ignored ([`nn_applet.c`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/host/nn_applet.c#L117-L120)). The game also reads the copy's `longshot_env.txt` and the caller's environment; its own comment says `HOS_ENV_FILE` is for "a scripted or A/B test run" ([`hos_main.c`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/host/hos_main.c#L422-L430)). I read this, I did not run it. Fix: put `quality_preset` (Custom) and `handheld_profile=0` in [`BASE_INI`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L96-L98), set `HOS_ENV_FILE` to an empty file, and copy the game's own settings lines into `report.txt`. **8. "None of these change a pipeline's key" is true for what a record holds, not for which records are made.** A record has code hashes, texture answers, mesh flag, topology, colour count, samples, formats and draw state: no size, rate, filter or upscaler ([`StateRec2`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/gpu/vk/vk_draw.cpp#L1917-L1949)), and frame rate, V-Sync, frame generation, sharpness and the window filter are present-only. But the game runs a different upscale compute network for each render share ([`vk_temporal.h`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/gpu/vk/vk_temporal.h#L61-L66)) and compute programs are recorded. The tour at 50% with the game's own upscaler records the 50% network. Auto sets 100% for Balanced, High and Ultra and 75% for Low ([`kTierResolution`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/gpu/vk/auto_settings.h#L51)), so most players on the game's own upscaler run a network the tour never records. (DLSS and FSR replace those dispatches, so `dlss=0` is the right base.) Native HDR changes formats and cannot be reached headless; the docstring's `--ini hdr=1` ([line 32](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L32)) is the host-only mode. Question: should the tour also run at 100% and 75% (`--ini resolution=1` and `2`), or say which tier it covers? `value` would show what each pass adds. **9. The tour's list also takes the copy's own earlier compute programs.** [`state_open_locked`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/gpu/vk/vk_draw.cpp#L1904-L1913) adds a compute record for every key in `longshot_computes.bin`, "what this install's own play dispatched". That file is not in `STATE_FILES` ([line 63](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L63)); at the PR's base it is beside the game and equally kept. So "only what this run meets counts" and the README's "a function of the build and the tour's route" do not hold for compute programs; they also count against `COMPUTE_MOST`. Move it aside too, or tour from a folder with no `cache/`. **10. Unstick presses start during the host's 8-second lockout, which is not an event.** In [`map_change`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L645-L651), after 240 frames of refusals every round presses A, B, stick+A and so on. The host refuses a teleport asked less than 8 seconds of wall clock after the last one, with no event playing ([`hos_main.c`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/host/hos_main.c#L921-L926)). 240 frames is 4 seconds at 60 fps and under 1 second at 300 fps, so A and B go out in the first half of a lockout, earlier the faster the run. Against the test's fake: 3 refusals gave no press, 10 refusals gave `0:A/100` and `0:B/100`. The script's own comment says A next to someone starts a conversation. Whether real stops come within 8 seconds of each other is open. Fix: recognise "one every 8 s" and just wait on the wall clock; keep unstick for an event that lasts. **11. Items. Question.** The unlock the script sends is a toggle: for an item already held it takes all of it away ([`hos_main.c`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/host/hos_main.c#L717), "unlock / relock"). The route sends the whole set again [after the age switch](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L290). Do the items the child pass gave survive the switch, and does the second pass then remove them? Also `!cheat` writes one slot ([`nn_hid.c`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/host/nn_hid.c#L371-L372)); a later line replaces one the game has not taken yet, and it takes one per Link update (not during a load or the pause menu). The script's 70 ms gap ([`cheat`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L521-L528)) has no margin for a hitch, and a tour with no pipelines prebuilt is full of them. The game logs `cheats: inventory <name> <delta> (had N, now M ...)` for each edit that lands; waiting for that line before the next one (the bridge's `subscribe` takes a log filter) would answer both questions and make "every item" true. This one I could not settle without the game. **12. A wedged event manager (GummiNaomi's data) is retried for minutes and ends the part. Questions.** She sees the event manager refuse every teleport until a restart after the Haunted Wasteland, the Colossus' Spirit Temple entrance and the collapsing Ganon's Castle. The refusal then is "an event is playing" ([`hos_main.c`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/host/hos_main.c#L935-L938)), which [`PERMANENT`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L605) treats as passing: 7200 frames of retries with unstick presses per stop, three misses, the run ends, and (point 4) so does the part. The PR describes it as "sent, the map does not change" and a five-send rule that the default route never reaches; she sees refusals. The route puts the Haunted Wasteland last in its age, which protects the default `tour` parts, but `run --part all` (the default of `run`) puts the age switch right after it (the dry run shows `visit Haunted Wasteland (child)`, then `age`). Which destinations in the table wedge it? "The collapsing Ganon's Castle" could be Hyrule Castle as the adult (index 4, banc `GanonCastle`), which is the 4th of 15 stops of `adult-overworld` with 11 after it, or Ganon's Castle (index 30), the last stop of the dungeons part. Either way, a refusal that lasts more than some seconds could be taken as a wedge: stop the game and go on from that stop in a fresh one, as the memory path does. ## Smaller - Statements in the description and docs that are not true or no longer: - The note about `Item_Hookshot` becoming a second `Item_Longshot` ([Reviewer notes](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/111)) was fixed on main by #117 (`3cbf12e8f`, 13 November): [rows 37 and 38](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/gpu/vk/oot_cheat_items.h#L37-L38) are Hookshot and Longshot again. The test still checks `Item_Longshot` only ([line 96](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/tests/test_pipeline_tour.py#L96-L99)). - "Game speed Per frame": that row does nothing above 60 fps ([`nn_vi.c`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/7ff5256e000671d0fccf27f77ba41275916b21df/host/nn_vi.c#L416-L420)); `HOS_FPS_STEP=0` is what gives a 60th of a second a frame, and that is a code comment, not something a test or a run shows. - "V-Sync is off": `vsync=0` is Auto (0 Auto, 1 On, 2 Off); harmless headless. "The game's own upscaler" for `upscaling=0`: that row is the window scaling filter; the Upscaler is `dlss`. - Measurements: the baseline column is "Hookshot #2048 (RTX 3090 ...)" with no hash, no `Baseline:` line and no line on why main was not the comparison (CONTRIBUTING, Measurements). The estimates are marked as estimates, and I could not check them; leaving `Measured` unticked is right. - [`set_ini`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L1004-L1006) restores only an options file that existed; with none, the forced one stays in the game folder (the docs say "restored after"). - [`cp -a --reflink=always`](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L1117) fails with a traceback on a file system without reflinks; say so in `--help` or use `--reflink=auto`. - A run with missed stops (1 or 2 in a row) still exits 0 and `--publish` publishes it; the misses are only in `report.txt`. `tour` also replaces `tour.bin.xz` whole with no count of the pipelines of the current one (11,227) that the new run lacks, and does not run `nx2/pipeline_states.py check` (GummiNaomi: her tour reaches 2,199 compute programs with the shipped ones and `check` stops at 1024). - The underwater step teleports to the map Link is in, with `want = lambda st: True` ([line 888](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L888)); `wait map` is met at once when the map is already that one, so the arrival is not confirmed, only 300 frames waited. - A stop dropped for memory (twice at the same stop) is in a printed line only; the report and `tour.md` do not say so, and `floor_gb` counts the whole machine, so another program can drop a stop. - Question: `LEFT_OUT` says "Lost Woods (child): 20 pipelines no other stop met" and the next comment says under 10 is not worth a stop ([lines 153-167](https://git.magicbean.dev/MagicBeanDev/Longshot/src/commit/e182b7e84b7969de8ba955ab5d3184ca9d69bca4/scripts/pipeline_tour.py#L153-L167)). Which is it? - Question: CONTRIBUTING says `feat` is "something new for players or modders" and makes the next minor release. This adds a maintainer script and docs and ships no list. Is `chore(scripts)` meant? ## GummiNaomi's data against the route The route is 47 stops (14 of the 61 destination and age pairs are left out), no actors and no cutscenes; her wider run finds 21,848 records (+52%) more than the shipped list, of which maps at every start position are 9,390, actors 2,798, cutscenes 9,476, menus 94. Her "menus add almost nothing" fits the PR's `PAUSE_PAGES = False`. The wider route is the PR's own follow-up, and her host change (`HOS_OOT_TELEPORT_EXTRA`, by index) would fit the bridge's `teleport`; that is for the maintainers. For this PR the useful parts of her data are points 6 (the menu buttons and the 6 seconds) and 12 (the wedge), and the `COMPUTE_MOST` note under Smaller. The tour list is replaced whole, so everything the old tour met and this route does not is lost; that is worth a count in the report. ## Short runs that would settle what is open (not done: no game run on my side) One game, windowless, a copy of a game folder with its own `cache/`: 1. `run --part child-overworld --stops 1 --skip-opening`: does `cache/longshot_pipeline_states.bin` grow, what does the report say, and are `cache/longshot_computes.bin` records in the list (points 1 and 9). 2. The same run with `HOS_FPS_LOG=1`: the `vi: game clock ... (speed S) at F fps` line should give S about F/60. 3. A stop in `ACT_AT` (Kokiri Forest) with a bridge `shot` after the pause presses, once with ZL/ZR and once with L/R; and a copy whose options file has `quality_preset=6` and the `auto_for` of a windowed run, to see `vk: Auto (this PC): the hardware changed` (points 6 and 7). 4. `--part adult-overworld --stops 1`, then `grep 'cheats: inventory' log.txt`: how many of the 56 landed, and any `now 0` after the age switch (point 11). 5. `--part adult-overworld` through Hyrule Castle (adult) and one stop after it (point 12).
HumanBean force-pushed scripts/pipeline-tour from e182b7e84b
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 2s
PR description / Description follows the template (pull_request_target) Successful in 2s
CI / Workflow lint (actionlint) (pull_request) Failing after 4s
Third-party parts / As their notes say (pull_request) Successful in 5s
Website / Website on Cloudflare (pull_request_target) Has been skipped
to c23b44f39f
Some checks are pending
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 6s
CI / Workflow lint (actionlint) (pull_request) Successful in 9s
Third-party parts / As their notes say (pull_request) Successful in 13s
beanstalk/approval awaiting maintainer approval
PR description / Description follows the template (pull_request_target) Successful in 6s
2026-11-14 21:36:55 +00:00
Compare
Author
Owner

Rebased onto main 5d3ecc78 (no conflicts) and added the renderer's play-profile recording. New head c23b44f3. The rebase follow-ups: the lists are in the game folder's cache/ now, so the tour puts cache/'s and the loose ones aside and reads cache/longshot_pipeline_states.bin; status.game comes from the sampled state; the client sends units:hos as frame steps; the source checks and the fake bridge follow main's text, and the Hookshot item is checked again. New: --vk-profile

on run and tour (RX_VK_PGO_FILE set, .reset once the game has control of Link, .dump at each game's end to /.profraw, refused or failed when the renderer does not count), and merge-profile (llvm-profdata merge into work/longshot_vk.profdata; --publish writes assets/profiles/longshot_vk.profdata.xz as xz -9e and the README note). The description follows main's new template. Not run: neither tests/run_all.py nor the tour on this head; it stays WIP until the local run.

Rebased onto main 5d3ecc78 (no conflicts) and added the renderer's play-profile recording. New head c23b44f3. The rebase follow-ups: the lists are in the game folder's cache/ now, so the tour puts cache/'s and the loose ones aside and reads cache/longshot_pipeline_states.bin; status.game comes from the sampled state; the client sends units:hos as frame steps; the source checks and the fake bridge follow main's text, and the Hookshot item is checked again. New: --vk-profile <dir> on run and tour (RX_VK_PGO_FILE set, .reset once the game has control of Link, .dump at each game's end to <dir>/<game>.profraw, refused or failed when the renderer does not count), and merge-profile (llvm-profdata merge into work/longshot_vk.profdata; --publish writes assets/profiles/longshot_vk.profdata.xz as xz -9e and the README note). The description follows main's new template. Not run: neither tests/run_all.py nor the tour on this head; it stays WIP until the local run.
perf(scripts): the pipeline tour uses the items once an age and where a pass could meet them, and settles in 60 frames
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 5s
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 7s
Third-party parts / As their notes say (pull_request) Successful in 17s
3917b27777
The sword, the shield and an item from the pause menu drew at ten stops of
each age: a pipeline is a program and its fixed draw state, and a scene's
light and fog are inputs to the same pipelines, so they are now used once an
age (ITEMS_ONCE: Kokiri Forest as the child, Hyrule Field as the adult) and
at the stops with passes of their own (ITEMS_PASSES: Zora's Domain, the
Water Temple, the Shadow Temple, Death Mountain Crater, the Fire Temple),
each with its own pause menu pick across the whole tour: 10 uses, 16 before.

A stop's settle (already counted in frames) waits 60 frames with no new
pipeline instead of 150, 600 at most as before: --settle-quiet and
--settle-max on run and tour (the tour passes them to its runs), said in
report.txt. The inventory's live lines are 32 ms and two frames apart
instead of 70 ms. compare <new> <old> and tour --compare-to <old> count the
pipelines each list has that the other lacks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
HumanBean force-pushed scripts/pipeline-tour from 3917b27777
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 5s
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 7s
Third-party parts / As their notes say (pull_request) Successful in 17s
to 84421c993e
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 7s
CI / Workflow lint (actionlint) (pull_request) Successful in 18s
Third-party parts / As their notes say (pull_request) Successful in 15s
PR description / Description follows the template (pull_request_target) Successful in 6s
2026-11-14 22:35:10 +00:00
Compare
Author
Owner

Faster tour, as asked on 2026-11-14 (84421c99, rebased onto main 82886e89 with no conflicts). The sword, shield and an item were used at 16 stops. They're now used once per age (ITEMS_ONCE: Kokiri Forest as the child, Hyrule Field as the adult) and at the stops with their own passes (ITEMS_PASSES: Zora's Domain, the Water Temple, the Shadow Temple, Death Mountain Crater, the Fire Temple), 10 uses in all, each with its own pause-menu pick across the whole route. The per-stop settle already counted frames; it now waits 60 quiet frames instead of 150 (600 at most, as before), set with --settle-quiet and --settle-max on run and tour and shown in report.txt. The inventory's !cheat lines are now 32 ms plus three presents apart instead of 70 ms of wall time. New: compare and tour --compare-to , which count the pipelines lost and gained. Estimated from the code: ~8,000 game frames and ~25 s of cheat gaps saved, ~1-2 min in all. Not run yet: the plan is a tour at c23b44f3 and one at this head on the lab, then compare (expected: 0 lost). Tests were updated and not run on this PC. Still WIP.

Faster tour, as asked on 2026-11-14 (84421c99, rebased onto main 82886e89 with no conflicts). The sword, shield and an item were used at 16 stops. They're now used once per age (ITEMS_ONCE: Kokiri Forest as the child, Hyrule Field as the adult) and at the stops with their own passes (ITEMS_PASSES: Zora's Domain, the Water Temple, the Shadow Temple, Death Mountain Crater, the Fire Temple), 10 uses in all, each with its own pause-menu pick across the whole route. The per-stop settle already counted frames; it now waits 60 quiet frames instead of 150 (600 at most, as before), set with --settle-quiet and --settle-max on run and tour and shown in report.txt. The inventory's !cheat lines are now 32 ms plus three presents apart instead of 70 ms of wall time. New: compare <new> <old> and tour --compare-to <old>, which count the pipelines lost and gained. Estimated from the code: ~8,000 game frames and ~25 s of cheat gaps saved, ~1-2 min in all. Not run yet: the plan is a tour at c23b44f3 and one at this head on the lab, then compare (expected: 0 lost). Tests were updated and not run on this PC. Still WIP.
HumanBean force-pushed scripts/pipeline-tour from 84421c993e
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 7s
CI / Workflow lint (actionlint) (pull_request) Successful in 18s
Third-party parts / As their notes say (pull_request) Successful in 15s
PR description / Description follows the template (pull_request_target) Successful in 6s
to d41a45bd18
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 9s
CI / Workflow lint (actionlint) (pull_request) Successful in 11s
Third-party parts / As their notes say (pull_request) Successful in 13s
PR description / Description follows the template (pull_request_target) Failing after 10s
2026-11-14 23:30:09 +00:00
Compare
Author
Owner

Self-review and fixes (d41a45bd, rebased onto main fe5c60d0 with no conflicts). A code review of the whole PR found 20 points; this head fixes the confirmed and plausible ones. A stopped tour now gives its run two minutes to end its game and put the game folder back (subprocess.run killed it at once and left the game running). The folder is changed only inside the try that restores it, and each file goes back on its own, across drives too. A folder with no options file gets none back. bin/longshot is started in a builder's folder (not the launcher). longshot_computes.bin is put aside, and the shell's and longshot_env.txt's switches are kept out. tour.md names no local path and uses the runs' --ini. The 8 s teleport refusal is waited out, not pressed at. Checks for each are in tests/test_pipeline_tour.py. The description now links issues 14 and 505 and has the Measurements table the template asks for. Not run: neither the checks nor the tour; still WIP until the lab comparison.

Self-review and fixes (d41a45bd, rebased onto main fe5c60d0 with no conflicts). A code review of the whole PR found 20 points; this head fixes the confirmed and plausible ones. A stopped tour now gives its run two minutes to end its game and put the game folder back (subprocess.run killed it at once and left the game running). The folder is changed only inside the try that restores it, and each file goes back on its own, across drives too. A folder with no options file gets none back. bin/longshot is started in a builder's folder (not the launcher). longshot_computes.bin is put aside, and the shell's and longshot_env.txt's switches are kept out. tour.md names no local path and uses the runs' --ini. The 8 s teleport refusal is waited out, not pressed at. Checks for each are in tests/test_pipeline_tour.py. The description now links issues 14 and 505 and has the Measurements table the template asks for. Not run: neither the checks nor the tour; still WIP until the lab comparison.
Author
Owner

Description only: the Measurements' How to reproduce line, and Flags and settings names the existing switches the script sets for its games.

Description only: the Measurements' How to reproduce line, and Flags and settings names the existing switches the script sets for its games.
HumanBean changed title from WIP: feat(scripts): the pipeline tour, driven through the control bridge to feat(scripts): the pipeline tour, driven through the control bridge 2026-11-14 23:32:13 +00:00
HumanBean deleted branch scripts/pipeline-tour 2026-11-14 23:32:24 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!111
No description provided.