fix(vi): pace at 240 while a movie plays, so its player stops losing pictures #488
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
beanstalk: approved @ 1697603
beanstalk: approved @ 2c5a62e
beanstalk: approved @ 4fd82b7
beanstalk: approved @ 64d6d09
beanstalk: approved @ 6f91fae
beanstalk: approved @ 70a78f9
beanstalk: approved @ 8d361dc
beanstalk: approved @ 9d8356e
beanstalk: approved @ abf2227
beanstalk: approved @ d82f202
beanstalk: approved @ e5f1e40
beanstalk: approved @ ee8ab5f
ci: approved
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/macos
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
regression
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
4 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!488
Loading…
Reference in a new issue
No description provided.
Delete branch "omgitsgela/Longshot:movie-pacing"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
hos_movie_frames()inhost/nn_vi.cnow 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).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 keepsfps_max()steady through the present, which the limiter's raise and restore read more than once. A cap the player orHOS_FPS_MAXset above 60 is left alone. The stamp is written with__atomic_store_n(relaxed): it crosses the presentation thread and the movie thread.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) andmovie pace off(HOS_MOVIE_PACE=0); the movie fixture also checks the stamp lands on the zero-bits path.HOS_MOVIE_PACEin the switch table, and twodocs/tested/run files for the Deck A/B.Flags and settings
HOS_MOVIE_PACE=0Only 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 is87353955on main26a2eab0, with the docs commitabf2227fon 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, noHOS_FPS_MAXMethod: two runtimes built from one base by
scripts/dev_packs.py pack(onlylongshot/longshot_vk.sodiffer), 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 launchHow 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=1andHOS_VIDEO_INPUT_LIMIT=0, sit at the title screen while the intro movie loops, and read thea movie oflines. Run files:docs/tested/2026-11-14-movie-pace-main-omgitsgela-run01.md,...-pr-omgitsgela-run02.md.cf605c85abf2227facquireline, late plays)Runs on this branch: run02 measured the pack at d3a75bf4 (the change at 87353955's first pack on main
26a2eab0); run03 reran the pack at87353955on the rebased main; the branch head isabf2227f(87353955+ docs). The main column is the matched runtime atcf605c85; main26a2eab0touched 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.The game's clock stays real time through the change: at the cap raised to 240 by hand,
HOS_FPS_LOG=1windows 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 betweencf605c85and26a2eab0touch no file this PR changes (git diff --name-only cf605c85..26a2eab0has nohost/nn_vi.c, no test here, nonx2/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.pyon the Deck at the rebased branch: 494 passed, 2 failed, 66 skipped. Both failures fail identically on plain main26a2eab0(test_module_ids.py:ValueError: substring not foundin its own fixture;test_pad_layout.py:unknown type name 'HoldToggle') - upstream, not from this change.test_frame_rate_cap_step.pyandtest_movie_frame_count.pypass standalone and in the suite. The new modes fail on main by construction: the fixture referencingmovie_seen_at/hos_now_nsdoes 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=0by 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
__atomicbuiltins, 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:
vi: a movie is playing: the frame rate goes to 240line 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=240already do today (documented at the Insert menu's row as tested for cutscenes): more CPU and power during movies.HOS_MOVIE_PACE=0turns it off; reverting87353955is 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 readfps_max()several times inside one present and must see one value. The stamp sits before the!bitsearly return inhos_movie_framesbecause 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
type(scope): description).main.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above).third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
Assisted-by:/Co-Authored-By:trailers.docs/tested/for each new measured run (format indocs/tested-configurations.md; no appended row or assembled view).mainand passing here.tests/that reads changed source updated in this PR, not weakened.70297a1192abf2227fd9Rebased 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.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).
Wording only: named the branch head (
abf2227f= change87353955plus the docs commit) so the Measurements header matches the pushed branch exactly; no code or numbers changed.Steam Deck OLED test, with this PR's movie pacing on and off (dukenheimer's Deck)
Mesa 26.2.0-devel git-5c65d4fc31. Stock clocks. Game Mode, docked: the game gets a 1280x720 surface.
543846eb+ #2270fc2e8c9(with #225) + #22352a61f40+ #2323ccb48a8+ #303995aab15, with#384
a9b0c880, #468e5889e76, #488abf2227fand #4967fb4e2c1merged 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).
upscaler, frame generation 3x, frame rate cap 60, vsync on.
RX_VK_READBACK_EARLY=1(#303) andHOS_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 moviesplayed with no input. Each launch played the intro (5,947 pictures), the title's 1,800-picture movie three times, then the intro again.
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:
vi: a movie is playing: the frame rate goes to 240, thenvi: the movie is over: back to the game's frame rate cap 60);Your runs were at vsync off with frame generation not mentioned; these are vsync on with frame generation 3x.
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).
@Beanstalk
Beanstalk test farm
Commit
abf2227· tier reviewed · approved by HumanBean · dashboardWhat 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.
0or2.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).Done. Comment
@Beanstalkto run it again.frame_rate_cap=0frame_rate_cap=2Also should probably default to on no?
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 @87353955on the rebased base, cf605c85-vs-26a2eab0 file diff empty, so the matched-main column still holds). No numbers changed.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.
Table blank-line fix after the provenance note; no numbers changed.
WIP: fix(vi): pace at 240 while a movie plays, so its player stops losing picturesto fix(vi): pace at 240 while a movie plays, so its player stops losing pictures