fix(vi): pace at 240 while a movie plays, so its player stops losing pictures #488

Merged
HumanBean merged 2 commits from omgitsgela/Longshot:movie-pacing into main 2026-11-15 06:38:14 +00:00
Contributor

Why

The title's opening movie loses a fifth to a quarter of its pictures on a slow PC: on a Steam Deck OLED at the console's frame rate cap it skipped 1,182 of its 5,947 pictures on one play (issue #73). On screen the picture twitches: the same frame repeats while the rest of the picture renders fine. (#73 has the full Deck measurement series.)

The player takes at most one picture per update, and its updates follow the frame loop: at cap 60 the Deck acquired only 48.3 pictures/s for a movie that needs 59.95/s, and the skip count equals the shortfall exactly. Raising the frame rate cap raises the acquisition rate; the game's own clock stays real time at every cap.

What changed

  • vi: hos_movie_frames() in host/nn_vi.c now stamps the wall time of the movie player's latest update at its entry - before the zero-frame-time early return, because at cap 60 a movie's normal path is that return (measured). The lift already calls it once per update of a movie player (nx2/oot_patches.py, f_83b7c710).
  • vi: fps_refresh() reads that stamp once per present and, while a movie is updating (a quarter second's grace after the last update), raises the pacing, the game's limiter and its time step to 240 (FPS_MOVIE), then returns to the game's own cap when the movie stops. One decision per present keeps fps_max() steady through the present, which the limiter's raise and restore read more than once. A cap the player or HOS_FPS_MAX set above 60 is left alone. The stamp is written with __atomic_store_n (relaxed): it crosses the presentation thread and the movie thread.
  • tests: the two text-extracted fixtures (tests/test_movie_frame_count.py, tests/test_frame_rate_cap_step.py) gain the host clock (hos_now_ns() stub, movie_seen_at) and two new modes, movie pace (activation at the console's cap, limiter raised to 240, expiry back to 60) and movie pace off (HOS_MOVIE_PACE=0); the movie fixture also checks the stamp lands on the zero-bits path.
  • docs: HOS_MOVIE_PACE in the switch table, and two docs/tested/ run files for the Deck A/B.

Flags and settings

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
HOS_MOVIE_PACE=0 Leave the pacing at the Game frame rate cap row while a movie plays, instead of raising it to 240 for the movie's pictures unset: the movie pacing is on on: measured to cut the Deck's intro-movie skips by half with no clock drift; the switch is for bisection and weak hardware machines under the movie's picture rate at the game's cap (Deck OLED, an under-specced PC) machines that already reach ~60 acquisitions/s at the cap: nothing to gain; settings that pin the base frame below the cap (dukenheimer's Deck run: frame generation 3x at 60 Hz output holds the base at 20/s, so on/off skipped the same, about 68.6% of the intro either way; the pacing lines fired and the picture's worst lag behind the sound improved, 1.31/1.16 s to 0.95/0.97 s) Windows, other GPUs

Only the Game frame rate cap row at 60 changes: a row at 90/120/144/240/Uncapped, or HOS_FPS_MAX, is left exactly as set.

Measurements

main: cf605c85 (2026-11-13)
this PR: movie-pacing @ abf2227f (2026-11-14; the change commit is 87353955 on main 26a2eab0, with the docs commit abf2227f on top; the pack measured first was d3a75bf4 on that same main, and 87353955's pack reran on the rebased base, run03)
Hardware: Steam Deck OLED: AMD Custom GPU 0405 (RADV VANGOGH), Mesa 26.1.2.221562, driver 0x6801063, 4c/8t APU 0405, 16 GB, SteamOS holo (kernel 6.18.50-valve2)
Scene: the title screen's looped intro movie (5,947 pictures at 59.95/s) and the title's later movies (1,800 each); Game frame rate cap row at 60 (frame_rate_cap=0), vsync off, FSR 3.1, HOS_VIDEO_INPUT_LIMIT=0, no HOS_FPS_MAX
Method: two runtimes built from one base by scripts/dev_packs.py pack (only longshot/longshot_vk.so differ), launched in turn on the same Deck with the same save-free title loop; the skip counts are the game's own log line (HOS_VIDEO_LOG=1: a movie of 5947 pictures ... N skipped to keep up with its sound); 2 rounds, 2-4 plays per launch
How to reproduce: build a renderer pack with this commit and one without (both over one cf6 base), run each at the cap row 60 with HOS_VIDEO_LOG=1 and HOS_VIDEO_INPUT_LIMIT=0, sit at the title screen while the intro movie loops, and read the a movie of lines. Run files: docs/tested/2026-11-14-movie-pace-main-omgitsgela-run01.md, ...-pr-omgitsgela-run02.md.

main @ cf605c85 this PR @ abf2227f
intro movie, skipped of 5,947 (round 1, 2 plays each) 1,238, 1,091 626, 464
intro movie, skipped (round 2, warm relaunch, 4 / 2 plays) 1,182, 1,074, 1,012, 1,028 551, 460
intro movie, median of all measured plays 1,091 (Δ -53%) 506
the picture vs its sound (acquire line, late plays) behind by 0.00 s behind by -0.02 s
title's later movies, skipped of 1,800 12, 51, 85 (already rare) 0, 31, 36, 109

Runs on this branch: run02 measured the pack at d3a75bf4 (the change at 87353955's first pack on main 26a2eab0); run03 reran the pack at 87353955 on the rebased main; the branch head is abf2227f (87353955 + docs). The main column is the matched runtime at cf605c85; main 26a2eab0 touched none of this change's files (git diff of host/nn_vi.c and both fixtures is empty), which is why the numbers hold across the base move.

main:  [hos MovieOperateThread] video: a movie of 5947 pictures on libdav1d: 3.3 ms to decode each (worst 190.0), one shown every 16.7 ms; 1,182 skipped to keep up with its sound; ...
this PR: [hos Presentation Thread] vi: a movie is playing: the frame rate goes to 240 for it (HOS_MOVIE_PACE=0 leaves it)
         [hos MovieOperateThread] video: a movie of 5947 pictures on libdav1d: 3.6 ms to decode each (worst 129.5), one shown every 16.7 ms; 551 skipped to keep up with its sound; ...
         [hos Presentation Thread] vi: the movie is over: back to the game's frame rate cap 60

The game's clock stays real time through the change: at the cap raised to 240 by hand, HOS_FPS_LOG=1 windows read speed 0.998-1.005 and the picture held within 34 ms of the movie's clock (#73 comments 17784/17803 measure why 240 and not a small raise: middle caps measured worse than both ends, cap 73 skipped 1,216).

Residual, stated honestly: about 8% of the intro's pictures still skip (460-551 of 5,947; a steady title loop settles near 440, #73 comment 17820). The player still takes one picture per update; this PR raises the update rate, it does not batch the pictures. The later title movies already skipped rarely on main; those counts are within each other's spread.

Rebased onto 26a2eab0: the eight commits between cf605c85 and 26a2eab0 touch no file this PR changes (git diff --name-only cf605c85..26a2eab0 has no host/nn_vi.c, no test here, no nx2/oot_patches.py). A pack of the rebased branch built on the same Deck and its first plays are in the Testing section below.

Testing

Tested: Deck game-free suite python -I -B tests/run_all.py on the Deck at the rebased branch: 494 passed, 2 failed, 66 skipped. Both failures fail identically on plain main 26a2eab0 (test_module_ids.py: ValueError: substring not found in its own fixture; test_pad_layout.py: unknown type name 'HoldToggle') - upstream, not from this change. test_frame_rate_cap_step.py and test_movie_frame_count.py pass standalone and in the suite. The new modes fail on main by construction: the fixture referencing movie_seen_at/hos_now_ns does not compile against main's source (-Werror, RC=1) and passes here.
In the game: the title screen's movie loop on a Deck OLED, two interleaved rounds over two runtimes differing only in this change (the Measurements table and the two docs/tested/ files); the pacing log lines fired on every movie and back between them (10 in, 9 out in one launch). A first launch of the new runtime re-prepared its shader cache, which inflated that launch's first play (626); later plays at warm caches measured 460-551.
Checked but not measured: the boost's scope (only the row at 60) and HOS_MOVIE_PACE=0 by the game-free fixture, not by an in-game run.
Rebased-branch run: the pack of the rebased branch (movie-pace-v2, v3.0.0-rc.3-69-g70297a11) built on the same Deck and ran at the title screen: 538 / 436 pictures skipped, the pacing lines in and out (8 returns), the picture behind by -0.04 s (docs/tested/2026-11-14-movie-pace-pr-omgitsgela-run03.md) - the change works the same on 26a2eab0's source.

Not tested: Windows (the change is plain C with __atomic builtins, which the CI's llvm-mingw win-x64 job compiles; no local Windows build); gameplay while a movie ends and the cap returns mid-game (the movie path ends between scenes in OoT; no in-game cutscene A/B run); other GPUs and desktop machines; power draw cost of a 240 loop during movies on battery (measured on AC).

Needs other testers:

  • Windows or Linux desktop: sit at the title screen through the intro movie at the Game frame rate cap row 60, expecting a vi: a movie is playing: the frame rate goes to 240 line and fewer picture repeats than main (T-X76W: claimable once this PR gets the testing-wanted label).

Screenshots

Nothing on screen a still can show: the change is the absence of picture repeats across frames, which the game's own log lines above count. (A frozen frame looks identical on both sides; the evidence is the skip counts and the pacing lines.)

Risk and rollback

The pacing decision moves only while the Game frame rate cap row is exactly 60 and a movie player has updated in the last quarter second; every other game keeps the cap it asked for. Within that window the frame loop, the guest limiter and the time step run at 240, as the row 240 and HOS_FPS_MAX=240 already do today (documented at the Insert menu's row as tested for cutscenes): more CPU and power during movies. HOS_MOVIE_PACE=0 turns it off; reverting 87353955 is enough otherwise. If a game's long interactive movie dislikes the faster loop, the row or the switch is the lever.

Reviewer notes

fps_refresh() is the only decision point, once per present - deliberately, because the limiter's raise and restore read fps_max() several times inside one present and must see one value. The stamp sits before the !bits early return in hos_movie_frames because at cap 60 the frame bits are zero for movies: that return is a movie's normal path, and after the return the stamp would never be set when it matters (the first version had that bug, caught in review). The full 59.95 pictures/s is not reached: the game-side fix is to let the player take more than one picture per update; this PR deliberately stays host-side (the movie update is reached through a virtual-dispatch table in the lifted code, not a static call site).

Issues

See #73 (keeps it open: the residual ~8% intro skips and the one-picture-per-update ceiling are the issue's remaining half).

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.
  • Flags noted, and the switch reference updated (docs/development.md, the README).
  • A file in docs/tested/ for each new measured run (format in docs/tested-configurations.md; no appended row or assembled view).
  • 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.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (Linux: the Deck pack built and ran from this tree. Windows: not run yet - the fork's workflows for a first-time contributor wait on a maintainer's approval; the win-x64 job will show it. Until then this box stays unticked and the PR stays WIP.)
  • Docs updated.
## Why The title's opening movie loses a fifth to a quarter of its pictures on a slow PC: on a Steam Deck OLED at the console's frame rate cap it skipped 1,182 of its 5,947 pictures on one play (issue #73). On screen the picture twitches: the same frame repeats while the rest of the picture renders fine. ([#73](https://git.magicbean.dev/MagicBeanDev/Longshot/issues/73) has the full Deck measurement series.) The player takes at most one picture per update, and its updates follow the frame loop: at cap 60 the Deck acquired only 48.3 pictures/s for a movie that needs 59.95/s, and the skip count equals the shortfall exactly. Raising the frame rate cap raises the acquisition rate; the game's own clock stays real time at every cap. ## What changed - vi: `hos_movie_frames()` in `host/nn_vi.c` now stamps the wall time of the movie player's latest update at its entry - before the zero-frame-time early return, because at cap 60 a movie's normal path *is* that return (measured). The lift already calls it once per update of a movie player (`nx2/oot_patches.py`, `f_83b7c710`). - vi: `fps_refresh()` reads that stamp once per present and, while a movie is updating (a quarter second's grace after the last update), raises the pacing, the game's limiter and its time step to 240 (`FPS_MOVIE`), then returns to the game's own cap when the movie stops. One decision per present keeps `fps_max()` steady through the present, which the limiter's raise and restore read more than once. A cap the player or `HOS_FPS_MAX` set above 60 is left alone. The stamp is written with `__atomic_store_n` (relaxed): it crosses the presentation thread and the movie thread. - tests: the two text-extracted fixtures (`tests/test_movie_frame_count.py`, `tests/test_frame_rate_cap_step.py`) gain the host clock (`hos_now_ns()` stub, `movie_seen_at`) and two new modes, `movie pace` (activation at the console's cap, limiter raised to 240, expiry back to 60) and `movie pace off` (`HOS_MOVIE_PACE=0`); the movie fixture also checks the stamp lands on the zero-bits path. - docs: `HOS_MOVIE_PACE` in the switch table, and two `docs/tested/` run files for the Deck A/B. ## Flags and settings | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | `HOS_MOVIE_PACE=0` | Leave the pacing at the Game frame rate cap row while a movie plays, instead of raising it to 240 for the movie's pictures | unset: the movie pacing is on | on: measured to cut the Deck's intro-movie skips by half with no clock drift; the switch is for bisection and weak hardware | machines under the movie's picture rate at the game's cap (Deck OLED, an under-specced PC) | machines that already reach ~60 acquisitions/s at the cap: nothing to gain; settings that pin the base frame below the cap (dukenheimer's Deck run: frame generation 3x at 60 Hz output holds the base at 20/s, so on/off skipped the same, about 68.6% of the intro either way; the pacing lines fired and the picture's worst lag behind the sound improved, 1.31/1.16 s to 0.95/0.97 s) | Windows, other GPUs | Only the Game frame rate cap row at 60 changes: a row at 90/120/144/240/Uncapped, or `HOS_FPS_MAX`, is left exactly as set. ## Measurements main: cf605c85 (2026-11-13) this PR: movie-pacing @ abf2227f (2026-11-14; the change commit is 87353955 on main 26a2eab0, with the docs commit abf2227f on top; the pack measured first was d3a75bf4 on that same main, and 87353955's pack reran on the rebased base, run03) Hardware: Steam Deck OLED: AMD Custom GPU 0405 (RADV VANGOGH), Mesa 26.1.2.221562, driver 0x6801063, 4c/8t APU 0405, 16 GB, SteamOS holo (kernel 6.18.50-valve2) Scene: the title screen's looped intro movie (5,947 pictures at 59.95/s) and the title's later movies (1,800 each); Game frame rate cap row at 60 (`frame_rate_cap=0`), vsync off, FSR 3.1, `HOS_VIDEO_INPUT_LIMIT=0`, no `HOS_FPS_MAX` Method: two runtimes built from one base by `scripts/dev_packs.py pack` (only `longshot`/`longshot_vk.so` differ), launched in turn on the same Deck with the same save-free title loop; the skip counts are the game's own log line (`HOS_VIDEO_LOG=1`: `a movie of 5947 pictures ... N skipped to keep up with its sound`); 2 rounds, 2-4 plays per launch How to reproduce: build a renderer pack with this commit and one without (both over one cf6 base), run each at the cap row 60 with `HOS_VIDEO_LOG=1` and `HOS_VIDEO_INPUT_LIMIT=0`, sit at the title screen while the intro movie loops, and read the `a movie of` lines. Run files: `docs/tested/2026-11-14-movie-pace-main-omgitsgela-run01.md`, `...-pr-omgitsgela-run02.md`. | | main @ cf605c85 | this PR @ abf2227f | |---|---|---| | intro movie, skipped of 5,947 (round 1, 2 plays each) | 1,238, 1,091 | 626, 464 | | intro movie, skipped (round 2, warm relaunch, 4 / 2 plays) | 1,182, 1,074, 1,012, 1,028 | 551, 460 | | intro movie, median of all measured plays | 1,091 (Δ -53%) | 506 | | the picture vs its sound (`acquire` line, late plays) | behind by 0.00 s | behind by -0.02 s | | title's later movies, skipped of 1,800 | 12, 51, 85 (already rare) | 0, 31, 36, 109 | *Runs on this branch: run02 measured the pack at d3a75bf4 (the change at 87353955's first pack on main 26a2eab0); run03 reran the pack at 87353955 on the rebased main; the branch head is abf2227f (87353955 + docs). The main column is the matched runtime at cf605c85; main 26a2eab0 touched none of this change's files (git diff of host/nn_vi.c and both fixtures is empty), which is why the numbers hold across the base move.* ``` main: [hos MovieOperateThread] video: a movie of 5947 pictures on libdav1d: 3.3 ms to decode each (worst 190.0), one shown every 16.7 ms; 1,182 skipped to keep up with its sound; ... this PR: [hos Presentation Thread] vi: a movie is playing: the frame rate goes to 240 for it (HOS_MOVIE_PACE=0 leaves it) [hos MovieOperateThread] video: a movie of 5947 pictures on libdav1d: 3.6 ms to decode each (worst 129.5), one shown every 16.7 ms; 551 skipped to keep up with its sound; ... [hos Presentation Thread] vi: the movie is over: back to the game's frame rate cap 60 ``` The game's clock stays real time through the change: at the cap raised to 240 by hand, `HOS_FPS_LOG=1` windows read speed 0.998-1.005 and the picture held within 34 ms of the movie's clock (#73 comments 17784/17803 measure why 240 and not a small raise: middle caps measured worse than both ends, cap 73 skipped 1,216). Residual, stated honestly: about 8% of the intro's pictures still skip (460-551 of 5,947; a steady title loop settles near 440, #73 comment 17820). The player still takes one picture per update; this PR raises the update rate, it does not batch the pictures. The later title movies already skipped rarely on main; those counts are within each other's spread. Rebased onto 26a2eab0: the eight commits between cf605c85 and 26a2eab0 touch no file this PR changes (`git diff --name-only cf605c85..26a2eab0` has no `host/nn_vi.c`, no test here, no `nx2/oot_patches.py`). A pack of the rebased branch built on the same Deck and its first plays are in the Testing section below. ## Testing **Tested:** Deck game-free suite `python -I -B tests/run_all.py` on the Deck at the rebased branch: 494 passed, 2 failed, 66 skipped. Both failures fail identically on plain main 26a2eab0 (`test_module_ids.py`: `ValueError: substring not found` in its own fixture; `test_pad_layout.py`: `unknown type name 'HoldToggle'`) - upstream, not from this change. `test_frame_rate_cap_step.py` and `test_movie_frame_count.py` pass standalone and in the suite. The new modes fail on main by construction: the fixture referencing `movie_seen_at`/`hos_now_ns` does not compile against main's source (`-Werror`, RC=1) and passes here. In the game: the title screen's movie loop on a Deck OLED, two interleaved rounds over two runtimes differing only in this change (the Measurements table and the two `docs/tested/` files); the pacing log lines fired on every movie and back between them (10 in, 9 out in one launch). A first launch of the new runtime re-prepared its shader cache, which inflated that launch's first play (626); later plays at warm caches measured 460-551. Checked but not measured: the boost's scope (only the row at 60) and `HOS_MOVIE_PACE=0` by the game-free fixture, not by an in-game run. Rebased-branch run: the pack of the rebased branch (`movie-pace-v2`, `v3.0.0-rc.3-69-g70297a11`) built on the same Deck and ran at the title screen: 538 / 436 pictures skipped, the pacing lines in and out (8 returns), the picture behind by -0.04 s (`docs/tested/2026-11-14-movie-pace-pr-omgitsgela-run03.md`) - the change works the same on 26a2eab0's source. **Not tested:** Windows (the change is plain C with `__atomic` builtins, which the CI's llvm-mingw win-x64 job compiles; no local Windows build); gameplay while a movie ends and the cap returns mid-game (the movie path ends between scenes in OoT; no in-game cutscene A/B run); other GPUs and desktop machines; power draw cost of a 240 loop during movies on battery (measured on AC). **Needs other testers:** - Windows or Linux desktop: sit at the title screen through the intro movie at the Game frame rate cap row 60, expecting a `vi: a movie is playing: the frame rate goes to 240` line and fewer picture repeats than main (T-X76W: claimable once this PR gets the testing-wanted label). ## Screenshots Nothing on screen a still can show: the change is the absence of picture repeats across frames, which the game's own log lines above count. (A frozen frame looks identical on both sides; the evidence is the skip counts and the pacing lines.) ## Risk and rollback The pacing decision moves only while the Game frame rate cap row is exactly 60 *and* a movie player has updated in the last quarter second; every other game keeps the cap it asked for. Within that window the frame loop, the guest limiter and the time step run at 240, as the row 240 and `HOS_FPS_MAX=240` already do today (documented at the Insert menu's row as tested for cutscenes): more CPU and power during movies. `HOS_MOVIE_PACE=0` turns it off; reverting 87353955 is enough otherwise. If a game's long interactive movie dislikes the faster loop, the row or the switch is the lever. ## Reviewer notes `fps_refresh()` is the only decision point, once per present - deliberately, because the limiter's raise and restore read `fps_max()` several times inside one present and must see one value. The stamp sits before the `!bits` early return in `hos_movie_frames` because at cap 60 the frame bits are zero for movies: that return is a movie's normal path, and after the return the stamp would never be set when it matters (the first version had that bug, caught in review). The full 59.95 pictures/s is *not* reached: the game-side fix is to let the player take more than one picture per update; this PR deliberately stays host-side (the movie update is reached through a virtual-dispatch table in the lifted code, not a static call site). ## Issues See #73 (keeps it open: the residual ~8% intro skips and the one-picture-per-update ceiling are the issue's remaining half). ## 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] Flags noted, and the switch reference updated (docs/development.md, the README). - [x] A file in `docs/tested/` for each new measured run (format in `docs/tested-configurations.md`; no appended row or assembled view). - [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. - [ ] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (Linux: the Deck pack built and ran from this tree. Windows: not run yet - the fork's workflows for a first-time contributor wait on a maintainer's approval; the win-x64 job will show it. Until then this box stays unticked and the PR stays WIP.) - [x] Docs updated.
The movie player takes at most one picture per update and its updates follow the
frame loop: measured on a Deck OLED, 48.3 acquisitions/s at the console's cap
skips 1,055-1,156 of the intro movie's 5,947 pictures. fps_refresh now raises
the pacing, the game's limiter and the time step to 240 while a movie's player
is updating (hos_movie_frames stamps the update; the lift calls it per update,
f_83b7c710), a quarter second's grace after the last update, HOS_MOVIE_PACE=0
off. Skips fall to 526-585; the title's looped movies 4-86 of 1,800; game clock
0.998-1.005. One decision per present: the cap stays steady through the present.
Tests: movie and cap-step fixtures gain the host clock and two new modes.

Assisted-by: GPT-6 Astra
docs(vi): HOS_MOVIE_PACE in the switch table, and the two measured runs (issue #73)
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 3s
Website / Website on Cloudflare (pull_request_target) Has been skipped
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
Website / Website on Cloudflare (pull_request) Has been cancelled
70297a1192
Assisted-by: GPT-6 Astra
omgitsgela force-pushed movie-pacing from 70297a1192
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 3s
Website / Website on Cloudflare (pull_request_target) Has been skipped
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
Website / Website on Cloudflare (pull_request) Has been cancelled
to abf2227fd9
Some checks failed
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 / 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 / The same as upstream (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request_target) Has been skipped
PR description / Description follows the template (pull_request_target) Failing after 14s
beanstalk/approval approved by HumanBean (reviewed)
beanstalk/any 0/1 pass, 0 running, 0 queued
2026-11-14 17:13:52 +00:00
Compare
Author
Contributor

Rebased onto main 26a2eab0 (no file overlap; the game-free suite reran on the rebased tree: 494 pass, the 2 failures named reproduce on plain main). Added docs/tested run03: the rebased pack ran on the Deck, 538/436 intro skips with the pacing lines firing both ways, same as d3a75bf4's 460-551. Kept the Windows-build box unticked honestly: this fork's workflows wait on maintainer approval (first-time contributor), so the win-x64 job has not compiled it yet - the PR stays WIP until CI runs, then that box gets ticked and pr ready follows in the same session.

Rebased onto main 26a2eab0 (no file overlap; the game-free suite reran on the rebased tree: 494 pass, the 2 failures named reproduce on plain main). Added docs/tested run03: the rebased pack ran on the Deck, 538/436 intro skips with the pacing lines firing both ways, same as d3a75bf4's 460-551. Kept the Windows-build box unticked honestly: this fork's workflows wait on maintainer approval (first-time contributor), so the win-x64 job has not compiled it yet - the PR stays WIP until CI runs, then that box gets ticked and pr ready follows in the same session.
Author
Contributor

Two asks for a maintainer, both routine: (1) approve the fork's workflow run so CI (including the win-x64 llvm-mingw compile) can check this PR - first-time contributor, so nothing runs yet; (2) add the testing wanted label so test request T-X76W can be claimed. When the win-x64 job passes I will tick the Windows box and mark the PR ready; the change is measured in-game on a Steam Deck OLED (measurements and three docs/tested run files above).

Two asks for a maintainer, both routine: (1) approve the fork's workflow run so CI (including the win-x64 llvm-mingw compile) can check this PR - first-time contributor, so nothing runs yet; (2) add the testing wanted label so test request T-X76W can be claimed. When the win-x64 job passes I will tick the Windows box and mark the PR ready; the change is measured in-game on a Steam Deck OLED (measurements and three docs/tested run files above).
Author
Contributor

Wording only: named the branch head (abf2227f = change 87353955 plus the docs commit) so the Measurements header matches the pushed branch exactly; no code or numbers changed.

Wording only: named the branch head (abf2227f = change 87353955 plus the docs commit) so the Measurements header matches the pushed branch exactly; no code or numbers changed.
Contributor

Steam Deck OLED test, with this PR's movie pacing on and off (dukenheimer's Deck)

  • Deck: Steam Deck OLED: AMD Custom APU 0932 (RADV VANGOGH), 16 GB; SteamOS 3.9.5 build 20261023.100, kernel 7.2.7-valve2,
    Mesa 26.2.0-devel git-5c65d4fc31. Stock clocks. Game Mode, docked: the game gets a 1280x720 surface.
  • Build: a Deck user build: main 543846eb + #227 0fc2e8c9 (with #225) + #223 52a61f40 + #232 3ccb48a8 + #303 995aab15, with
    #384 a9b0c880, #468 e5889e76, #488 abf2227f and #496 7fb4e2c1 merged on top. All four merged clean. Linux suite: 544 passed,
    main's 5 known failures. Built with x86-64-v3 and the release play profile, LLVM 23.1.5 (glibc 2.35).
  • Settings (the Deck owner's own): handheld profile (window crop 1536x864), output 1280x800, Render resolution 100%, the game's own
    upscaler, frame generation 3x, frame rate cap 60, vsync on. RX_VK_READBACK_EARLY=1 (#303) and HOS_VIDEO_LOG=1.

Results for this PR (#488)

Same build in every launch; the pacing on (its default) or off (HOS_MOVIE_PACE=0), order on, off, off, on. The title screen's movies
played with no input. Each launch played the intro (5,947 pictures), the title's 1,800-picture movie three times, then the intro again.

intro: skipped of 5,947 (2 plays a launch) title movie: skipped of 1,800 (3 plays) picture most behind, s behind at each end, s pacing lines (raised / back)
on (runs 1 and 4) 4,075, 4,083; 4,068, 4,063 1,214, 1,203, 1,199; 1,202, 1,215, 1,198 0.95; 0.97 0.010-0.032 5 / 5; 6 / 5
off (runs 2 and 3) 4,087, 4,079; 4,068, 4,070 1,201, 1,264, 1,210; 1,199, 1,216, 1,197 1.31; 1.16 0.011-0.050 none
  • Decoding: libdav1d at 6.7 to 6.9 ms a picture (worst 179 to 188 ms) for the intro, the same in both arms.
  • No errors or faults in any of the 4 launches.

Reading. At these settings the pacing doesn't change how many pictures are skipped: about 68.6% of the intro either way. The
game's own frames ran at 20.0 a second during the movies in all four launches (the median of the 600-present lines while a movie
showed). Frame generation 3x on a 60 Hz output holds the base frames at 60 / 3 = 20, so the player gets about 20 updates a second
whatever the cap. The only difference between the arms is that the picture's worst lag behind the sound was a little smaller with the
pacing on (0.95 and 0.97 s against 1.31 and 1.16 s): 2 launches each.

The pacing worked as described:

  • the raise and its return showed at every movie (vi: a movie is playing: the frame rate goes to 240, then
    vi: the movie is over: back to the game's frame rate cap 60);
  • the route runs, all of which have this PR with the pacing on, played at 12.8 to 13.5 real fps.

Your runs were at vsync off with frame generation not mentioned; these are vsync on with frame generation 3x.

**Steam Deck OLED test, with this PR's movie pacing on and off** (dukenheimer's Deck) - **Deck:** Steam Deck OLED: AMD Custom APU 0932 (RADV VANGOGH), 16 GB; SteamOS 3.9.5 build 20261023.100, kernel 7.2.7-valve2, Mesa 26.2.0-devel git-5c65d4fc31. Stock clocks. Game Mode, docked: the game gets a 1280x720 surface. - **Build:** a Deck user build: main 543846eb + #227 0fc2e8c9 (with #225) + #223 52a61f40 + #232 3ccb48a8 + #303 995aab15, with #384 a9b0c880, #468 e5889e76, #488 abf2227f and #496 7fb4e2c1 merged on top. All four merged clean. Linux suite: 544 passed, main's 5 known failures. Built with x86-64-v3 and the release play profile, LLVM 23.1.5 (glibc 2.35). - **Settings (the Deck owner's own):** handheld profile (window crop 1536x864), output 1280x800, Render resolution 100%, the game's own upscaler, frame generation 3x, frame rate cap 60, vsync on. `RX_VK_READBACK_EARLY=1` (#303) and `HOS_VIDEO_LOG=1`. **Results for this PR (#488)** Same build in every launch; the pacing on (its default) or off (`HOS_MOVIE_PACE=0`), order on, off, off, on. The title screen's movies played with no input. Each launch played the intro (5,947 pictures), the title's 1,800-picture movie three times, then the intro again. | | intro: skipped of 5,947 (2 plays a launch) | title movie: skipped of 1,800 (3 plays) | picture most behind, s | behind at each end, s | pacing lines (raised / back) | |---|---|---|---|---|---| | on (runs 1 and 4) | 4,075, 4,083; 4,068, 4,063 | 1,214, 1,203, 1,199; 1,202, 1,215, 1,198 | 0.95; 0.97 | 0.010-0.032 | 5 / 5; 6 / 5 | | off (runs 2 and 3) | 4,087, 4,079; 4,068, 4,070 | 1,201, 1,264, 1,210; 1,199, 1,216, 1,197 | 1.31; 1.16 | 0.011-0.050 | none | - Decoding: libdav1d at 6.7 to 6.9 ms a picture (worst 179 to 188 ms) for the intro, the same in both arms. - No errors or faults in any of the 4 launches. **Reading.** At these settings the pacing doesn't change how many pictures are skipped: about 68.6% of the intro either way. The game's own frames ran at 20.0 a second during the movies in all four launches (the median of the 600-present lines while a movie showed). Frame generation 3x on a 60 Hz output holds the base frames at 60 / 3 = 20, so the player gets about 20 updates a second whatever the cap. The only difference between the arms is that the picture's worst lag behind the sound was a little smaller with the pacing on (0.95 and 0.97 s against 1.31 and 1.16 s): 2 launches each. The pacing worked as described: - the raise and its return showed at every movie (`vi: a movie is playing: the frame rate goes to 240`, then `vi: the movie is over: back to the game's frame rate cap 60`); - the route runs, all of which have this PR with the pacing on, played at 12.8 to 13.5 real fps. Your runs were at vsync off with frame generation not mentioned; these are vsync on with frame generation 3x.
Author
Contributor

Flags table: added the frame-generation interaction from dukenheimer's independent Deck test (below). Their A/B: same build, pacing on/off four launches, frame generation 3x at a 60 Hz output. The base frame sits at 20/s no matter the cap, so the raise cannot add updates there and the skipped count is unchanged at about 68.6% of the intro either way - which matches the arithmetic (about 20 updates/s against the 59.95/s the movie needs is a two-thirds shortfall). The raise still worked as designed (lines at every movie) and the worst picture lag behind sound improved, 1.31/1.16 s to 0.95/0.97 s. Nothing harmful; the gain needs settings where the base frame can actually rise (this Deck: vsync off, FSR 3.1, cap row 60: 1,091 to 506).

Flags table: added the frame-generation interaction from dukenheimer's independent Deck test (below). Their A/B: same build, pacing on/off four launches, frame generation 3x at a 60 Hz output. The base frame sits at 20/s no matter the cap, so the raise cannot add updates there and the skipped count is unchanged at about 68.6% of the intro either way - which matches the arithmetic (about 20 updates/s against the 59.95/s the movie needs is a two-thirds shortfall). The raise still worked as designed (lines at every movie) and the worst picture lag behind sound improved, 1.31/1.16 s to 0.95/0.97 s. Nothing harmful; the gain needs settings where the base frame can actually rise (this Deck: vsync off, FSR 3.1, cap row 60: 1,091 to 506).
Owner

@Beanstalk

@Beanstalk
Member

Beanstalk test farm

Commit abf2227 · tier reviewed · approved by HumanBean · dashboard

What will be tested · AI plan (gpt-6.1-terra)

The plan will compare the title-screen intro movie on main and the pull request at the 60 and 120 fps caps, covering automatic movie pacing and the unchanged higher-cap path. It will record movie skip reports, frame times and process memory, failing on a missing movie report, a crash or a substantial memory regression; skip counts are recorded but not automatically compared. The run takes about 17 minutes per tester and will wait for a volunteer because none is online now. No tester online now matches any: that target waits for one.

  • Where: any (any PC, 1 report).
  • Matrix: 2 cells, each on main and the PR: frame_rate_cap 0 or 2.
  • title-movie: from the title screen (settings vsync=2, frame_generation=0; switches HOS_VIDEO_INPUT_LIMIT=0), then measure movie for 2 min 30 s (after 0 s warm-up); 1 run on each side for each of 2 cells (1 game start a run).
  • Measured: crashes, frame times, stutters and hitches, memory and CPU time, the game's log.
  • The PR fails if: it crashes; or peak memory is more than 20% above main; or forbidden log lines is above 0; or expected log lines missing is above 0.
  • Time: about 20 min of game runs per tester (2 game starts), plus the two builds.

Done. Comment @Beanstalk to run it again.

Target Reports Crash Frame p99 (ms) vs main VRAM peak (MB) vs main Result
any 0/1 — — — queued
Scenario · matrix cell Target Frame p99 (ms) vs main Result
title-movie · frame_rate_cap=0 any — —
title-movie · frame_rate_cap=2 any — —
<!-- beanstalk:sticky --> ### Beanstalk test farm Commit `abf2227` · tier **reviewed** · approved by HumanBean · [dashboard](https://beanstalk.magicbean.dev/prs/488) **What will be tested** · AI plan (`gpt-6.1-terra`) The plan will compare the title-screen intro movie on main and the pull request at the 60 and 120 fps caps, covering automatic movie pacing and the unchanged higher-cap path. It will record movie skip reports, frame times and process memory, failing on a missing movie report, a crash or a substantial memory regression; skip counts are recorded but not automatically compared. The run takes about 17 minutes per tester and will wait for a volunteer because none is online now. No tester online now matches any: that target waits for one. - **Where:** any (any PC, 1 report). - **Matrix:** 2 cells, each on main and the PR: frame_rate_cap `0` or `2`. - **title-movie:** from the title screen (settings vsync=2, frame_generation=0; switches `HOS_VIDEO_INPUT_LIMIT=0`), then measure movie for 2 min 30 s (after 0 s warm-up); 1 run on each side for each of 2 cells (1 game start a run). - **Measured:** crashes, frame times, stutters and hitches, memory and CPU time, the game's log. - **The PR fails if:** it crashes; or peak memory is more than 20% above main; or forbidden log lines is above 0; or expected log lines missing is above 0. - **Time:** about 20 min of game runs per tester (2 game starts), plus the two builds. Done. Comment `@Beanstalk` to run it again. | Target | Reports | Crash | Frame p99 (ms) vs main | VRAM peak (MB) vs main | Result | | --- | --- | --- | --- | --- | --- | | any | 0/1 | — | — | — | queued | | Scenario · matrix cell | Target | Frame p99 (ms) vs main | Result | | --- | --- | --- | --- | | title-movie · `frame_rate_cap=0` | any | — | — | | title-movie · `frame_rate_cap=2` | any | — | — |
Owner

Also should probably default to on no?

Also should probably default to on no?
Author
Contributor

Description check: the Measurements table column now names the branch head (abf2227f); run provenance moved to a note under the table (run02 @ d3a75bf4 pack, run03 reran @ 87353955 on the rebased base, cf605c85-vs-26a2eab0 file diff empty, so the matched-main column still holds). No numbers changed.

Description check: the Measurements table column now names the branch head (abf2227f); run provenance moved to a note under the table (run02 @ d3a75bf4 pack, run03 reran @ 87353955 on the rebased base, cf605c85-vs-26a2eab0 file diff empty, so the matched-main column still holds). No numbers changed.
Author
Contributor

Reply to "should probably default to on": it does - unset means on; the flag table's Default column says exactly that ("unset: the movie pacing is on"). HOS_MOVIE_PACE=0 exists only for bisection and weak hardware; nobody has to set anything to get the fix.

Reply to "should probably default to on": it does - unset means on; the flag table's Default column says exactly that ("unset: the movie pacing is on"). HOS_MOVIE_PACE=0 exists only for bisection and weak hardware; nobody has to set anything to get the fix.
Author
Contributor

Table blank-line fix after the provenance note; no numbers changed.

Table blank-line fix after the provenance note; no numbers changed.
HumanBean changed title from WIP: fix(vi): pace at 240 while a movie plays, so its player stops losing pictures to fix(vi): pace at 240 while a movie plays, so its player stops losing pictures 2026-11-15 06:38:07 +00:00
HumanBean deleted branch movie-pacing 2026-11-15 06:38:15 +00:00
Some checks failed
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 / 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 / The same as upstream (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request_target) Has been skipped
PR description / Description follows the template (pull_request_target) Failing after 14s
beanstalk/approval approved by HumanBean (reviewed)
beanstalk/any 0/1 pass, 0 running, 0 queued
Some workflows are waiting to be reviewed.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
4 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!488
No description provided.