Repository navigation
Ocarina of Time's movie player takes up to two pictures a frame, so a movie no longer falls behind its sound when frames take two vsyncs and its last scenes no longer play in silence (issue #1501) - #2456
Open
PucaVaz wants to merge 1 commit into
Conversation
… movie no longer falls behind its sound when frames take two vsyncs and its last scenes no longer play in silence (issue seedds#1501) ## What and why Issue seedds#1501: parts of Ocarina of Time's opening (`0400486024DEC000`, v131072) play with no sound, the renderer's mix silent for about a minute with no underrun and no red line. **Cause.** The picture is behind the sound, and the sound ends first. The movie's sound is a stream of the game's sound engine and keeps real time. The picture comes from the game's movie player, which is limited to one picture a frame: - once a frame the player's update (`main+0x3b5ca60`) adds the frame's time to a count of pictures due and advances that many (`main+0x3b61ea0`), but at most the limit at the player's `+0x20` (`main+0x3b5cb2c`); the rest stays in the count; - the constructor (`main+0x3b5cec0`) sets the limit to 1, or to 2 for a last argument of 2 or more, and the queue to twice the limit; the game's one caller passes 1 (`main+0x3b5c55c`). The console holds one vsync a frame in its movies. Here the frame step hands the game steps of two and three vsyncs, so each such frame leaves the picture one further behind for the rest of the movie. Read in play with `DM_PROBE=main:3b5cb28`, `DM_PROBE_ALL=1` (`x8` the pictures due, `x9` the limit): the limit 1, and the count at 1,350 pictures after 45 s at a 30 fps frame step. **Change.** One function-matched cut in the profile (`MATCH_CUTS` in `dm/games/0400486024DEC000/patches.py`): the constructor takes its own value for 2 whatever its caller passes (`cinc w8, w28, ge` → `mov w8, seedds#2`: limit 2, queue 4). A frame of two vsyncs then shows every second picture, and a movie that fell behind catches up at two pictures a frame. It is a cut of the game's code because the limit is the game's: the host's decoder gives every picture it is asked for. 09a91c5 (the decoder on its own thread) does not change this: with its host the 30 fps runs below are the "before". Not 3, the frame step's largest hold: it is no value of the constructor's, and forced, the game stopped at the movie's start with a copy to a null pointer in `sdk` (cause not looked into). Frames held three vsyncs still lose a picture each until shorter frames catch up. Reference entry: [A player limited to one picture a frame falls behind its sound](docs/reference/movies.md#a-player-limited-to-one-picture-a-frame-falls-behind-its-sound); the numbers are in the game note (Frame rate). Relation to open PRs: seedds#1379 (AV1 pictures by the movie's clock) drops late pictures in the host's decoder for the same symptom; it conflicts with `main` since 09a91c5. This change needs no clock in the host. seedds#1661 (a trace of the mix) is independent. The issue also reports item and chest jingles missing in play. Not reproduced, not looked into, and no movie plays there: this PR does not claim it. ## Checked - Tests: `python -m unittest dm/tools/tests/test_patches.py`: 23 pass; the new `test_ocarina_movie_player_takes_two_pictures_a_frame` fails without the profile change (1 error of 23). `test_docs.py`, `test_game_profile.py` pass; `docs_patches.py --check` current. The Python tests file by file (`dm/tools/tests`, `dm/ios/tests`, `dm/gfx/tests`, `DM_GAME=0400486024DEC000`): all pass but `test_tree_tar.py` (`test_packs_a_quiet_folder_and_keeps_its_attributes`), which fails the same on an unchanged tree. - The rewriter on the game's `main` (v131072): `function patch "movie pictures a frame: …": matched at 0x3b5d084`; the patched module differs from the one without the cut in those 4 bytes only. - Games run: The Legend of Zelda: Ocarina of Time `0400486024DEC000` v131072, headless, from an empty save through the title into the opening's first movie (`Dm_Kokiri_0000`: 9,254 pictures at 60 a second, 154.2 s), `DM_KEYS=50:A~0.2`. The host is 7867152's (the chain of the libraries on this Mac) with 09a91c5's decoder files and two log lines: one per 60 pictures given (stream time, host time) and one per 200 renderer frames (how many not silent). A second counts as silent with 4 or fewer of its 200 frames not silent. | run | 154.00 s of the movie took | silent seconds in the movie | |---|---|---| | 30 fps target, without the cut, 3 runs | 307.97, 307.95, 307.95 s (0.500) | 153, 153, 153 | | 30 fps target, with the cut, 3 runs | 154.03, 153.97, 153.97 s (1.000) | 0, 0, 0 | | 60 fps target, 7867152 as it is, the Mac busy (hold between 1 and 2), 1 run | 216.85 s (0.710) | 59 | | 60 fps target, without the cut, the Mac idle, 1 run | 153.98 s | 0 | | 60 fps target, with the cut, the Mac idle, 1 run | 153.84 s | 0 | Game time was 1.000 of real time in the 30 fps runs on both sides. With the cut at the 60 fps target the title movie, 9.88 s behind after its first 36 s without it (the long frames of the start), was level with its clock, and after the opening's movie the game went on into the Great Deku Tree scene (screenshot). The red lines are the same on both sides (those of a partial shader library). - Devices and OS: Mac, Apple M4 (10-core GPU, 24 GB), VideoToolbox AV1. - `scripts/regress.py`: not run. The change is one profile's `patches.py`, a test and docs; no host, chain or shared tool file changes, and the rule is scoped to this profile. - Not run: a window with sound listened to (the measurements are the renderer's counters); an iPhone or iPad; a Mac that decodes AV1 with dav1d; the opening's later movies and the game's other 24; a host built from `main`'s head (its chain's libraries are not on this Mac; the host files between 7867152 and 09a91c5 differ in the decoder, taken here, and in `hle_oe.c`, `loader.c` and `probe.c`). Lint not run (ruff, pyright and LLVM are not installed here). - Open: frames held three vsyncs (above); whether other games of this engine have the same player. ## Docs - [x] The finding is in its game note and its `docs/reference/movies.md` entry, and `python3 dm/tools/docs_symptoms.py` was run (the symptom index); `docs/agents/game-patches.md` regenerated - [ ] A chain tool changed: no - [x] No game files, keys or data extracted from a game in the change or the attachments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
Issue #1501: parts of Ocarina of Time's opening (
0400486024DEC000, v131072) play with no sound, the renderer's mix silent for about a minute with no underrun and no red line.Cause. The picture is behind the sound, and the sound ends first. The movie's sound is a stream of the game's sound engine and keeps real time. The picture comes from the game's movie player, which is limited to one picture a frame:
main+0x3b5ca60) adds the frame's time to a count of pictures due and advances that many (main+0x3b61ea0), but at most the limit at the player's+0x20(main+0x3b5cb2c); the rest stays in the count;main+0x3b5cec0) sets the limit to 1, or to 2 for a last argument of 2 or more, and the queue to twice the limit; the game's one caller passes 1 (main+0x3b5c55c).The console holds one vsync a frame in its movies. Here the frame step hands the game steps of two and three vsyncs, so each such frame leaves the picture one further behind for the rest of the movie. Read in play with
DM_PROBE=main:3b5cb28,DM_PROBE_ALL=1(x8the pictures due,x9the limit): the limit 1, and the count at 1,350 pictures after 45 s at a 30 fps frame step.Change. One function-matched cut in the profile (
MATCH_CUTSindm/games/0400486024DEC000/patches.py): the constructor takes its own value for 2 whatever its caller passes (cinc w8, w28, ge→mov w8, #2: limit 2, queue 4). A frame of two vsyncs then shows every second picture, and a movie that fell behind catches up at two pictures a frame. It is a cut of the game's code because the limit is the game's: the host's decoder gives every picture it is asked for. 09a91c5 (the decoder on its own thread) does not change this: with its host the 30 fps runs below are the "before".Not 3, the frame step's largest hold: it is no value of the constructor's, and forced, the game stopped at the movie's start with a copy to a null pointer in
sdk(cause not looked into). Frames held three vsyncs still lose a picture each until shorter frames catch up.Reference entry: A player limited to one picture a frame falls behind its sound; the numbers are in the game note (Frame rate).
Relation to open PRs: #1379 (AV1 pictures by the movie's clock) drops late pictures in the host's decoder for the same symptom; it conflicts with
mainsince 09a91c5. This change needs no clock in the host. #1661 (a trace of the mix) is independent.The issue also reports item and chest jingles missing in play. Not reproduced, not looked into, and no movie plays there: this PR does not claim it.
Checked
Tests:
python -m unittest dm/tools/tests/test_patches.py: 23 pass; the newtest_ocarina_movie_player_takes_two_pictures_a_framefails without the profile change (1 error of 23).test_docs.py,test_game_profile.pypass;docs_patches.py --checkcurrent. The Python tests file by file (dm/tools/tests,dm/ios/tests,dm/gfx/tests,DM_GAME=0400486024DEC000): all pass buttest_tree_tar.py(test_packs_a_quiet_folder_and_keeps_its_attributes), which fails the same on an unchanged tree.The rewriter on the game's
main(v131072):function patch "movie pictures a frame: …": matched at 0x3b5d084; the patched module differs from the one without the cut in those 4 bytes only.Games run: The Legend of Zelda: Ocarina of Time
0400486024DEC000v131072, headless, from an empty save through the title into the opening's first movie (Dm_Kokiri_0000: 9,254 pictures at 60 a second, 154.2 s),DM_KEYS=50:A~0.2. The host is 7867152's (the chain of the libraries on this Mac) with 09a91c5's decoder files and two log lines: one per 60 pictures given (stream time, host time) and one per 200 renderer frames (how many not silent). A second counts as silent with 4 or fewer of its 200 frames not silent.Game time was 1.000 of real time in the 30 fps runs on both sides. With the cut at the 60 fps target the title movie, 9.88 s behind after its first 36 s without it (the long frames of the start), was level with its clock, and after the opening's movie the game went on into the Great Deku Tree scene (screenshot). The red lines are the same on both sides (those of a partial shader library).
Devices and OS: Mac, Apple M4 (10-core GPU, 24 GB), VideoToolbox AV1.
scripts/regress.py: not run. The change is one profile'spatches.py, a test and docs; no host, chain or shared tool file changes, and the rule is scoped to this profile.Not run: a window with sound listened to (the measurements are the renderer's counters); an iPhone or iPad; a Mac that decodes AV1 with dav1d; the opening's later movies and the game's other 24; a host built from
main's head (its chain's libraries are not on this Mac; the host files between 7867152 and 09a91c5 differ in the decoder, taken here, and inhle_oe.c,loader.candprobe.c). Lint not run (ruff, pyright and LLVM are not installed here).Open: frames held three vsyncs (above); whether other games of this engine have the same player.
Docs
docs/reference/movies.mdentry, andpython3 dm/tools/docs_symptoms.pywas run (the symptom index);docs/agents/game-patches.mdregenerated