fix(oot): preserve Scarecrow's Song recording and playback timing #78

Open
mooki wants to merge 4 commits from mooki/Longshot:fix/oot-scarecrow-song-timing into main
First-time contributor

Why

Scarecrow's Song can lose its recorded rhythm or appear stuck on its first note when the game runs above 60 fps. This keeps fractional time for all three updates of the eight-note song's elapsed counter, without changing existing saves.

The run-wide diagnostic median was lower on the branch, but it is not a speed comparison: the fixed song returns to gameplay while main stays in the note screen.

What changed

  • nx2/oot_patches.py: add playback, eight-note recording and final-note completion to the existing whole-frame carry table. They share the same elapsed counter; no new helper, forced minimum step or frame cap.
  • tests/test_scarecrow_timing.py: compile authored conversion shapes with the real patch and fraction helper. Cover all nine recording/playback combinations at 60, 120 and 240, the final note, counter resets, shared remainders, idempotence and the untouched other recording branch.
  • README.md: explain that already-lost rhythm cannot be recovered; re-record an affected song after updating. Correctly recorded old songs need no re-teaching.
  • docs/tested-configurations.md: keep a row for each of the four new Longshot correctness runs, including the fourth run's pre-play core escapes.

Flags and settings

None. No setting, menu choice, environment variable or saved format is added or changed. The existing above-60 warning still applies to other gameplay.

Measurements

Branch state: on main f41db065cf6667ec638a10aa6990099392d10dca; head 5dd2ee53566bbf9f918bac55eca3e33dc2a6cbcb; whole game-free suite on this head: 510 passed, 0 failed, 19 skipped (main f41db065cf6667ec638a10aa6990099392d10dca: 509 passed, 0 failed, 19 skipped); the commits are unchanged by rebases apart from the results table.

main: d4a69456c9 (2026-11-12)
this PR: fix/oot-scarecrow-song-timing @ 57696d0c21178b439ca35f9ddda27728da2f1de0
Hardware: RTX 4090 24 GB, NVIDIA 615.78.08; Core i9-13900K, 96 GB installed (93 GB reported by the game); Arch Linux 7.2.15-1-omarchy
Scene: adult Link in Hyrule Field at the same saved spot, the same correctly recorded eight-note song; 1280x720 game output, 1920x1080 offscreen renderer surface, Custom preset, Game's own upscaler at 50%, frame generation off, cap 240, precompile off, no window
Method: warmed both builds, then main / branch / main / branch in one isolated 344-second correctness session. Four 85.7–85.8-second processes. Captures at five and twenty seconds after the last scripted note release; actual five-second captures completed at 5.04–5.05 seconds. Native present traces show a mean above 60 during the measured 20-second interval on every run. The table counts visible notes and completion, not fps gains or exact song duration.
How to reproduce: teach an eight-note Scarecrow song as young Link at a 60 cap, keep that save, then play it as adult Link on a PC actually running above 60 with a 240 cap. Compare the note sheet after five and twenty seconds. Run python -I -B tests/test_scarecrow_timing.py --cc gcc; its --source option accepts the main patch file for the negative control.

main @ d4a69456 this PR @ 57696d0c
Visible notes at the five-second checkpoint, run 1 / run 2 1 / 1 8 / 8, with the success message
Median visible notes 1 8
Completed by that checkpoint, run 1 / run 2 No / No Yes / Yes
Main's twenty-second checkpoint Still the first note in both runs Already completed in both runs
Real-source fixture failures, two interleaved runs 8 / 8 of 9 combinations 0 / 0 of 9 combinations
Median fixture failures 8 0

The fixture's main execution used d725b363, whose runtime source is identical to the measured main. Afterward main gained only the DLSS model-settle runtime change and documentation/tooling/tests; DLSS was off in these runs, and the song code and fixture inputs are unchanged. The measured commits above are retained rather than relabelled as the current branch.

Actual fixture output, repeated identically in both pairs:

main:
record 60, play 120: FAIL (stored 216 ticks, playback 1200 updates)
record 240, play 240: FAIL (stored 0 ticks, playback 0 updates)
branch:
record 60, play 120: PASS (stored 216 ticks, playback 432 updates)
record 240, play 240: PASS (stored 216 ticks, playback 864 updates)
PASS: short-song recording and playback share a fractional clock
Run order Build Wall time Load start → end Result
1 main d4a69456 85.7 s 2.84 → 4.81 First note at both checkpoints
2 branch 57696d0c 85.7 s 4.81 → 5.34 Eight notes and success by 5.04 s
3 main d4a69456 85.8 s 5.34 → 6.65 First note at both checkpoints
4 branch 57696d0c 85.8 s 6.65 → 6.90 Eight notes and success by 5.04 s

Every game had the intended bounded exit 124, not a claimed graceful quit. Fatal, device-loss and kernel-fault counts were zero, with complete journal coverage. The fourth run had five virtual-core escapes around present 7530, before controllable play at frame 7905; the others had none. Those lines are retained, not discarded to make the result look cleaner.

Earlier ordinary attempts first hit the game's first-use ocarina tutorial, so their notes were not accepted and their pictures were rejected. The corrected ordinary main warm-up completed the song under shared load, without establishing the above-60 failing condition. Those attempts are retained, not used as before/after proof.

Testing

Tested: native Linux Longshot build and the four adult-field playback runs above, using matching release-profile builds and copied saves. Both pairs' source images and the final panel were inspected. The same real-source fixture failed on main and passed here in two interleaved pairs. The complete game-free suite ran on the exact branch state above. One C translation unit compiles for the Windows target with strict warnings: zero failures and zero unjudged files; not run on Windows.

Historical Hookshot evidence, not a new Longshot run: a fresh child recording at a 240 cap retained all eight notes, including the last note; that recording played at caps 60/120/240. The original correctly recorded song also completed at adult Bonooru. The original three-site fix was included in the combined successful GitHub Windows/Ubuntu check; the renamed branch is not claimed to have new Windows execution.

Not tested: the new Longshot runs replay an existing song recorded historically as young Link at a 60 cap; they do not newly teach it as young Link, traverse the age change, or confirm the scarecrow's answer. The separate mode-13 recording loop is unchanged and untested. No Windows/AMD/Intel gameplay, physical controller or microphone, audible output, real-window presentation, long-session coverage or general above-60 gameplay guarantee. The changed fixture was not executed on Windows after the rename; target compilation is not Windows runtime evidence.

Needs other testers:

  • Any PC that runs above 60 fps (the reporter's case): teach the Scarecrow's Song as young Link, return as adult Link and play it back, expecting the notes to be recorded and played at normal speed and the scarecrow to answer (test request T-LZ6Y)

Screenshots

New Longshot proof, same five-second checkpoint: main @ d4a69456 shows only the first note in both runs; this PR @ 57696d0c shows all eight and “You played your Scarecrow Song!” in both. All sources and the JPG were inspected.

Historical Hookshot proof at code 56ac5e77 with a local read-only observer: a fresh child recording made with a 240 cap plays all eight notes at 60, 120 and 240. This is not a new Longshot recording run.

Risk and rollback

A wrong patch-site match could leave a song counter uncorrected; the existing patch-count warning and authored three-site fixture make that visible. The separate recording mode is deliberately untouched. No save is rewritten. Reverting these entries restores the old behavior; a cap of 60 remains the workaround until a fixed build is used. Songs whose old recording lost its rhythm must be recorded again.

Reviewer notes

Start with the three whole-frame entries and the shared counter key. The third entry is necessary: the earlier two-site attempt lost the final note and was rejected. No generic timing abstraction, global minimum step or stored-song migration is introduced. The current publication remains a draft for the requested end-to-end player scenario.

Issues

Fixes #87

Original report: https://github.com/NotRexed/Hookshot/issues/2668.

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported.
  • 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.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Screenshots for a change that shows on screen.
  • A row in docs/tested-configurations.md for each measured run.
  • A game-free test added for the fix, failing on main and passing here.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Docs updated.

New on this forge; not opened on GitHub.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra [email protected]

## Why Scarecrow's Song can lose its recorded rhythm or appear stuck on its first note when the game runs above 60 fps. This keeps fractional time for all three updates of the eight-note song's elapsed counter, without changing existing saves. The run-wide diagnostic median was lower on the branch, but it is not a speed comparison: the fixed song returns to gameplay while main stays in the note screen. ## What changed - `nx2/oot_patches.py`: add playback, eight-note recording and final-note completion to the existing whole-frame carry table. They share the same elapsed counter; no new helper, forced minimum step or frame cap. - `tests/test_scarecrow_timing.py`: compile authored conversion shapes with the real patch and fraction helper. Cover all nine recording/playback combinations at 60, 120 and 240, the final note, counter resets, shared remainders, idempotence and the untouched other recording branch. - `README.md`: explain that already-lost rhythm cannot be recovered; re-record an affected song after updating. Correctly recorded old songs need no re-teaching. - `docs/tested-configurations.md`: keep a row for each of the four new Longshot correctness runs, including the fourth run's pre-play core escapes. ## Flags and settings None. No setting, menu choice, environment variable or saved format is added or changed. The existing above-60 warning still applies to other gameplay. ## Measurements <!-- branch-state --> Branch state: on main `f41db065cf6667ec638a10aa6990099392d10dca`; head `5dd2ee53566bbf9f918bac55eca3e33dc2a6cbcb`; whole game-free suite on this head: `510 passed, 0 failed, 19 skipped` (main `f41db065cf6667ec638a10aa6990099392d10dca`: `509 passed, 0 failed, 19 skipped`); the commits are unchanged by rebases apart from the results table. <!-- /branch-state --> main: d4a69456c9de5131589af1fcf9a09adba4c403d1 (2026-11-12) this PR: fix/oot-scarecrow-song-timing @ 57696d0c21178b439ca35f9ddda27728da2f1de0 Hardware: RTX 4090 24 GB, NVIDIA 615.78.08; Core i9-13900K, 96 GB installed (93 GB reported by the game); Arch Linux 7.2.15-1-omarchy Scene: adult Link in Hyrule Field at the same saved spot, the same correctly recorded eight-note song; 1280x720 game output, 1920x1080 offscreen renderer surface, Custom preset, Game's own upscaler at 50%, frame generation off, cap 240, precompile off, no window Method: warmed both builds, then main / branch / main / branch in one isolated 344-second correctness session. Four 85.7–85.8-second processes. Captures at five and twenty seconds after the last scripted note release; actual five-second captures completed at 5.04–5.05 seconds. Native present traces show a mean above 60 during the measured 20-second interval on every run. The table counts visible notes and completion, not fps gains or exact song duration. How to reproduce: teach an eight-note Scarecrow song as young Link at a 60 cap, keep that save, then play it as adult Link on a PC actually running above 60 with a 240 cap. Compare the note sheet after five and twenty seconds. Run `python -I -B tests/test_scarecrow_timing.py --cc gcc`; its `--source` option accepts the main patch file for the negative control. | | main @ d4a69456 | this PR @ 57696d0c | |---|---|---| | Visible notes at the five-second checkpoint, run 1 / run 2 | 1 / 1 | 8 / 8, with the success message | | Median visible notes | 1 | 8 | | Completed by that checkpoint, run 1 / run 2 | No / No | Yes / Yes | | Main's twenty-second checkpoint | Still the first note in both runs | Already completed in both runs | | Real-source fixture failures, two interleaved runs | 8 / 8 of 9 combinations | 0 / 0 of 9 combinations | | Median fixture failures | 8 | 0 | The fixture's main execution used d725b363, whose runtime source is identical to the measured main. Afterward main gained only the DLSS model-settle runtime change and documentation/tooling/tests; DLSS was off in these runs, and the song code and fixture inputs are unchanged. The measured commits above are retained rather than relabelled as the current branch. Actual fixture output, repeated identically in both pairs: ```text main: record 60, play 120: FAIL (stored 216 ticks, playback 1200 updates) record 240, play 240: FAIL (stored 0 ticks, playback 0 updates) branch: record 60, play 120: PASS (stored 216 ticks, playback 432 updates) record 240, play 240: PASS (stored 216 ticks, playback 864 updates) PASS: short-song recording and playback share a fractional clock ``` | Run order | Build | Wall time | Load start → end | Result | |---|---|---:|---|---| | 1 | main d4a69456 | 85.7 s | 2.84 → 4.81 | First note at both checkpoints | | 2 | branch 57696d0c | 85.7 s | 4.81 → 5.34 | Eight notes and success by 5.04 s | | 3 | main d4a69456 | 85.8 s | 5.34 → 6.65 | First note at both checkpoints | | 4 | branch 57696d0c | 85.8 s | 6.65 → 6.90 | Eight notes and success by 5.04 s | Every game had the intended bounded exit 124, not a claimed graceful quit. Fatal, device-loss and kernel-fault counts were zero, with complete journal coverage. The fourth run had five virtual-core escapes around present 7530, before controllable play at frame 7905; the others had none. Those lines are retained, not discarded to make the result look cleaner. Earlier ordinary attempts first hit the game's first-use ocarina tutorial, so their notes were not accepted and their pictures were rejected. The corrected ordinary main warm-up completed the song under shared load, without establishing the above-60 failing condition. Those attempts are retained, not used as before/after proof. ## Testing **Tested:** native Linux Longshot build and the four adult-field playback runs above, using matching release-profile builds and copied saves. Both pairs' source images and the final panel were inspected. The same real-source fixture failed on main and passed here in two interleaved pairs. The complete game-free suite ran on the exact branch state above. One C translation unit compiles for the Windows target with strict warnings: zero failures and zero unjudged files; not run on Windows. Historical Hookshot evidence, not a new Longshot run: a fresh child recording at a 240 cap retained all eight notes, including the last note; that recording played at caps 60/120/240. The original correctly recorded song also completed at adult Bonooru. The original three-site fix was included in the combined successful GitHub Windows/Ubuntu check; the renamed branch is not claimed to have new Windows execution. **Not tested:** the new Longshot runs replay an existing song recorded historically as young Link at a 60 cap; they do not newly teach it as young Link, traverse the age change, or confirm the scarecrow's answer. The separate mode-13 recording loop is unchanged and untested. No Windows/AMD/Intel gameplay, physical controller or microphone, audible output, real-window presentation, long-session coverage or general above-60 gameplay guarantee. The changed fixture was not executed on Windows after the rename; target compilation is not Windows runtime evidence. **Needs other testers:** - Any PC that runs above 60 fps (the reporter's case): teach the Scarecrow's Song as young Link, return as adult Link and play it back, expecting the notes to be recorded and played at normal speed and the scarecrow to answer (test request T-LZ6Y) ## Screenshots ![New Longshot proof, same five-second checkpoint: main @ d4a69456 shows only the first note in both runs; this PR @ 57696d0c shows all eight and “You played your Scarecrow Song!” in both. All sources and the JPG were inspected.](https://git.magicbean.dev/attachments/a64f7ab1-1ab1-436e-88b4-b3f835b46d9c) ![Historical Hookshot proof at code 56ac5e77 with a local read-only observer: a fresh child recording made with a 240 cap plays all eight notes at 60, 120 and 240. This is not a new Longshot recording run.](https://git.magicbean.dev/attachments/589f641d-dfd8-4313-941c-682fe3d4e8bd) ## Risk and rollback A wrong patch-site match could leave a song counter uncorrected; the existing patch-count warning and authored three-site fixture make that visible. The separate recording mode is deliberately untouched. No save is rewritten. Reverting these entries restores the old behavior; a cap of 60 remains the workaround until a fixed build is used. Songs whose old recording lost its rhythm must be recorded again. ## Reviewer notes Start with the three whole-frame entries and the shared counter key. The third entry is necessary: the earlier two-site attempt lost the final note and was rejected. No generic timing abstraction, global minimum step or stored-song migration is introduced. The current publication remains a draft for the requested end-to-end player scenario. ## Issues Fixes #87 Original report: https://github.com/NotRexed/Hookshot/issues/2668. ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [x] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. - [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. ### If applicable - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] Screenshots for a change that shows on screen. - [x] A row in `docs/tested-configurations.md` for each measured run. - [x] A game-free test added for the fix, failing on `main` and passing here. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [x] Docs updated. --- New on this forge; not opened on GitHub. Assisted-by: GPT-6 Astra Co-Authored-By: GPT-6 Astra <[email protected]>
Carry fractional frame steps for the eight-note recording and playback counters through the existing whole-frame mechanism. Keep the separate mode-13 recording branch and saved melodies unchanged.

Exercise cross-rate recording and playback with authored C fixtures at 60, 120 and 240 updates per second, including shared remainders and the untouched recording branch.

Refs: #2668

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Define the project's CRT policy before the fixture's first include so its bounded memcpy calls compile with the same strict warning flags for the Windows target.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Recording hands the eighth note's elapsed counter to its completion routine. Carry that routine's identical fractional cut too so it saves a positive final duration instead of dropping the last note.

Model the completion phase explicitly in the cross-rate fixture. It fails with the intermediate two-site fix on GCC and Clang, passes with all three counter updates, and preserves the separate mode-13 recording branch.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Keep all four measured runs, bounded exits and the fourth run’s pre-play core escapes visible. Main is still on the first note at twenty seconds while both fixed runs show all eight notes and success by the five-second checkpoint.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Author
First-time contributor

The test request for the reporter's scenario is open (T-LZ6Y). Rebased onto main f41db065c with the patches unchanged; whole suite on the new head 5dd2ee535: 510 passed, 0 failed, 19 skipped. Ready for review.

The test request for the reporter's scenario is open (T-LZ6Y). Rebased onto main f41db065c with the patches unchanged; whole suite on the new head 5dd2ee535: 510 passed, 0 failed, 19 skipped. Ready for review.
mooki force-pushed fix/oot-scarecrow-song-timing from f3281c7c7d to 5dd2ee5356
Some checks are pending
Linux AppImage / appimage (pull_request) Blocked by required conditions
Windows builder / Longshot Builder zip (pull_request) Blocked by required conditions
Windows builder / Windows Vulkan back end (llvm-mingw) (pull_request) Blocked by required conditions
Windows builder / dav1d for Windows (llvm-mingw) (pull_request) Blocked by required conditions
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / Python source checks (windows-2022, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (windows-2022, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
Vulkan back end with NVIDIA DLSS / Windows Vulkan back end with NVIDIA DLSS and AMD FSR 4 (llvm-mingw, link check) (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 2s
2026-11-13 02:31:01 +00:00
Compare
mooki changed title from WIP: fix(oot): preserve Scarecrow's Song recording and playback timing to fix(oot): preserve Scarecrow's Song recording and playback timing 2026-11-13 02:31:02 +00:00
Some checks are pending
Linux AppImage / appimage (pull_request) Blocked by required conditions
Windows builder / Longshot Builder zip (pull_request) Blocked by required conditions
Windows builder / Windows Vulkan back end (llvm-mingw) (pull_request) Blocked by required conditions
Windows builder / dav1d for Windows (llvm-mingw) (pull_request) Blocked by required conditions
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / Python source checks (windows-2022, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (windows-2022, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
Vulkan back end with NVIDIA DLSS / Windows Vulkan back end with NVIDIA DLSS and AMD FSR 4 (llvm-mingw, link check) (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 2s
This pull request doesn't have enough approvals yet. 0 of 1 approvals granted.
Some workflows are waiting to be reviewed.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'fix/oot-scarecrow-song-timing:mooki-fix/oot-scarecrow-song-timing'
git switch 'mooki-fix/oot-scarecrow-song-timing'
Sign in to join this conversation.
No description provided.