Skip to content

AV1 pictures by the movie's clock: a game slower than its movie skips late pictures instead of playing the movie slowly - #1379

Open
MetaIX wants to merge 3 commits into
seedds:mainfrom
MetaIX:pr/av1-movie-clock
Open

MetaIX wants to merge 3 commits into
seedds:mainfrom
MetaIX:pr/av1-movie-clock

Conversation

@MetaIX

@MetaIX MetaIX commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #1377: this branch includes its commit. Only the last commit (AV1 pictures by the movie's clock…) is this PR's; I'll rebase once #1377 is in.

What and why

Symptom: Ocarina of Time's player (0400486024DEC000 v131072, profile in #1299) shows one picture per game frame and sends packets until the decoder refuses them. All 26 of its movies are 60 fps. On a Mac that renders the title below 60 fps, the movie plays slowly and out of step with its sound: at 28 frames a second (an M1 Max, under the title's NDVE filter) it ran at 0.44×.

Change: the decoder hands out pictures by the movie's clock, as a video player does.

  • It compares the pictures' timestamps (ns, from the first picture) with the guest clock (guest_ns, which does not run while the Mac sleeps, an iPhone app is in the background or DM_DETCLOCK steps it).
  • When the game gives a buffer back, it gets the newest waiting picture that is due; the late ones before it are dropped.
  • A picture counts as late only once the next one is 50 ms overdue. Without that slack, a game running at the movie's rate but a frame behind its clock after a hitch went on skipping every other picture.
  • A timestamp going back (a loop, a new stream) restarts the clock; Reset restarts it too. So does a picture over a second late when the game held the movie: it presented 6 or more frames without taking a picture (a pause menu, a load screen), or its window lost the focus.
  • After a stall in the host with no frames presented (a pipeline built on first use, a long GPU wait), the late pictures are skipped instead, so the picture catches up with the sound, which went on. The log says when the clock restarts and why, and when a stall is caught up.
  • A picture that ends the stream is never dropped.
  • A game that keeps up drops nothing.
  • AV1 only (the only codec whose timestamps I checked); DM_CV_LATE=0 turns it off.

Checked

Docs

  • The finding is in its docs/reference/movies.md entry, and python3 dm/tools/docs_symptoms.py was run (the symptom index)
  • DM_CV_LATE is in docs/env_switches.tsv (test_env_switches passes on this branch)
  • No game files, keys or data extracted from a game in the change or the attachments

🤖 Generated with Claude Code

MetaIX and others added 2 commits November 5, 2026 14:35
…'s timestamp, outside the decoder lock

One frame at a time (max_frame_delay 1, 2 threads) took 9-20 ms a packet of Ocarina of Time's 1080p60 title movie on an M1 Max, single
packets up to 134 ms; more threads on one frame did not help. 8 threads and 8 frames in flight: at most 26 ms. A picture then comes some
packets after its own: mav1_decode_ts carries the packet's timestamp through dav1d (Dav1dData.m), the wrapper queues pictures
(mav1_next) and gives the frames still decoding at the end of the stream (mav1_drain), asking dav1d once per input, since a second
ask drains. cv_dec_submit decodes AV1 under a decode lock of its own, so Release and Acquire do not wait on dav1d; a packet may add up
to CV_BURST pictures past CV_HOLD, where packets are still refused. t_codec_video: a lone frame's picture comes at the end of the
stream; 20 packets give 20 pictures in order with their timestamps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… late pictures instead of playing the movie slowly

Ocarina of Time's player shows one picture a frame and sends packets until refused; its movies are 60 fps. At 28 frames a second (an M1
Max's GPU under the title's NDVE filter) the title movie ran at 0.44x, its sound ahead. The decoder now gives pictures by their
timestamps against the host clock: of the waiting pictures, the newest that is due, the late ones before it dropped, a picture late
once the next is 50 ms overdue. A stall over a second or a timestamp going back starts the clock again; Reset too. AV1 only;
DM_CV_LATE=0 turns it off. t_codec_video checks the rules on a clock it sets. Movie time against host time: 66.03 s in 66.05 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the host The Horizon host: SVCs, services, threads, memory (dm/host) label Nov 5, 2026
gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 5, 2026
…lel and pictures follow the movie's clock
gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 5, 2026
…switch table

The watch job reads "skip" lines from .github/upstream-watch.tsv (number and
reason) and lists those pull requests as left out instead of counting them.
Three are: seedds#1311 (Ocarina's play reports already complete offline, from seedds#1299;
this is a whole prepo:u service on new CMIF not taken upstream yet), seedds#1357
(NXForge compiles again with a deeper bracket limit only when Metal asks, so the
shader chain stays the same) and seedds#1365 (upstream's agent charter, docs only).

docs/env_switches.tsv gets DM_CV_LATE, which seedds#1379 reads: env_switches.py
--check failed on this branch since that merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g
@reefbluesky

Copy link
Copy Markdown

Tested on a MacBook Pro 14" (M2 Pro, no hardware AV1, 32 GB), macOS 27.2: main 865cc35 plus #1299, #1312, #1300, #1331 and this PR with #1377.

Before: the title movie ran at 22 fps (GPU 36 ms a frame), a scene's sound played before its picture, and when the picture reached the scene its sound had already ended (silence).

With this PR the log shows AV1 decoder 1: late pictures skipped to keep the movie on its clock and the sound now follows the picture. The movie shows fewer pictures a second, as expected. There was one slip in the middle of the sequence, where the narration and the picture went briefly out of step; afterwards the picture caught up with the scenes. The audio device is fine (0 underruns over 50,000 callbacks).

… the clock again, and the clock is the guest's

A picture over a second late started the movie clock again. That suits a pause, but on a stall in the host (a pipeline built on
first use, a long GPU wait) the game's sound goes on, and the picture then stayed that far behind the narration until the next
stream (reported on an M2 Pro). Now the clock starts again only when the game held the movie: it presented 6 frames or more
without taking a picture (a pause menu, a load screen), or its window lost the focus meanwhile. Otherwise the late pictures are
skipped until the movie is back on its clock. The clock is guest_ns, which does not run while the Mac sleeps, an iPhone app is in
the background or DM_DETCLOCK steps it. The log says when the clock starts again and why, and when a stall is caught up.
t_codec_video adds a pause, a stall and a focus change. DM_CV_LATE gets its row in docs/env_switches.tsv (test_env_switches
listed it as a switch without a meaning), and docs/reference/movies.md an entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WLejRwCykjPcsPU4Hdenf
@MetaIX

MetaIX commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for testing on the M2 Pro. The slip matches a rule in the first version: a picture over a second late restarted the clock. That was meant for a pause, but on a stall in the host (a pipeline built on first use, a long GPU wait) the narration goes on, so the picture stayed behind until the next stream started.

The PR now restarts the clock only when the game held the movie (it presented frames without taking a picture, or the window lost the focus), and otherwise skips the late pictures to catch up. Reproduced here by stopping dmhost for 1.6 s during the title movie: before, the picture stayed 1.72 s behind for the rest of the movie; now it is back within 0.06 s of the movie's clock. If you rerun it, the log says the movie clock starts again … : <why> or … behind after the game stood still where a slip would have been.

Also, your build has #1331: with DM_NVN_NDVE_MOVIE_DIRECT=1 the title movie runs at 60 fps with nothing to skip (about 1 ms of GPU a frame on our M1 Max, against 35–42 ms with the network).

Drafted with Claude; reviewed by me.

@github-actions github-actions Bot added the docs Documentation label Nov 6, 2026
gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 6, 2026
…ll in the host is caught up instead of starting the clock again, and the clock is the guest's

Resolved: cv_dec keeps NXForge's refused count beside seedds#1379's catch-up fields; docs/env_switches.tsv keeps DM_CURSOR_HIDE and takes seedds#1379's DM_CV_LATE line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g
gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 6, 2026
) replaces the pull request heads

Upstream landed Ocarina of Time in main (chain adec7d81d970: "holds 60 fps in
Kokiri Forest, 4 GB less") and closed the pull requests it folded in, with
reworded and partly different code. Mixing a pull request head with its landed
version hunk by hunk silently broke things (two GlobalToStorage tables, a
duplicate AF2ip, two mtk_pso definitions), so the rule here is one version per
feature:

- Pull requests upstream closed (seedds#1299, seedds#1300, seedds#1312, seedds#1343, seedds#1345, seedds#1353,
  seedds#1354, seedds#1359, seedds#1360, seedds#1373, seedds#1428, seedds#1430, seedds#1434): their hunks are taken out
  of our side before the three-way merge, so upstream's landed version stands
  alone. Gone with them: bindless pool table patching (reg_bump), the ghost
  table's per-submission hold, the growing raw pool table and its write-log
  sizing, seedds#1428's pool_buf spares (upstream's pool_table_take is the same
  idea), the ixc-based indirect mesh draw (upstream uses mdx), and their tests
  (t_pool_table, t_mesh_indirect, t_gmem_raw, finalized_bindless_pool,
  test_bnsh_nvn2, test_ndve).
- Open pull requests that upstream's landing supersedes: seedds#1394 (upstream's
  mtl_pool_addressed) and seedds#1393 pool_skip_retired (upstream's budgeted pool
  walk; the profile does not use it) are dropped with their tests and docs.
  seedds#1368's mesh pipeline prewarm is dropped too: upstream kept the background
  build in nvn_geometry.inc, and the prewarm's mtk_build / mtk_claim /
  g_warm_mps went with it.
- Kept, as upstream does not have them: NXForge's own features (GPU watchdog,
  present filters and anti-aliasing, aniso floor, quick settings, pipeline
  archive compaction, upload breakdown, memory report lines) and the still
  open pull requests seedds#1304, seedds#1305, seedds#1308, seedds#1320, seedds#1331, seedds#1332, seedds#1355, seedds#1356,
  seedds#1369, seedds#1376, seedds#1377, seedds#1379, seedds#1382, seedds#1390, seedds#1391, seedds#1392, seedds#1414, seedds#1420,
  seedds#1436, seedds#1439, seedds#1446, seedds#1453.
- Combined where both sides changed the same lines: GW_N 1 << 16 with seedds#1320's
  wide-write intervals (GW_PAGES = SBW_WPAGES, 128 slots), the CPU upload log
  with upstream's linear upload route, arch_open_end before the compaction
  limit, cbn_prog in geo_gs_one (seedds#1382).
- Translator: upstream's sources and binaries, plus seedds#1414's
  InstEmitAmpereMem diff and the LDSM SubgroupBarrier in
  InstEmitAmpereTensor.cs. The binaries and dmsh_chain.lock are upstream's
  until the Mac rebuilds them (dotnet publish, tool_lock --update-lock,
  dmsh_chain --header); dmsh_chain.h is regenerated for now.
- RED_ONCE keys: upstream took 153 and 154, so the present anti-aliasing lines
  move to 156 and 157 and seedds#1420's packaged-entry line to 158 (array 159).
- Host: hle_net.c and t_net_eventfd.c are upstream's (its eventfd poll waits
  out the whole timeout, which our 5d7920c fixed another way).
- Kept ours: dm/tools/romfs_image.py and its test, so the 26 GB RomFS image is
  not packed again.

dm/tools tests on Linux: same failures as on 875449e (Linux-only quirks),
plus test_nlg4 and test_dmsh_pool_arrays, which fail on upstream main too.
Not yet built on the Mac.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g
@Amost-

Amost- commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

Tested #1377 and #1379 on an M2 Pro (MacBook Pro, 19-core GPU, 32 GB, macOS 27.2, no hardware AV1) with #1331's direct mode on (DM_NVN_NDVE_MOVIE_DIRECT=1), since your note says the title movie has nothing to skip there.

Build: main 863bc6b7 plus my local picks (#1331, #1176, #1585 and a few host-side changes), app chain 3f146103c47b. Three hosts built the same way: as is, + #1377 (d234d4f6), and + #1377 + #1379 (3b588ca6, 7ee81e91). All cherry-picked cleanly and t_codec_video passes. Runs are headless at the title with no input. DM_NVN_REGLUM records a patch of moving grass in the title movie on every present; a present whose patch is identical to the previous one counts as a repeat (near-black frames in the loop's fade are left out). Window: 60 s from 55 s after the first present.

host presents repeats the movie
main + picks 60/s 19.0% about 48.5 pictures/s, 0.8×
+ #1377 60/s 0.1% every picture: end of stream at 97.8 s for 5,947 pictures
+ #1377 + #1379 60/s 0.1% the same; 11 late pictures skipped over the loop (one log line)

Control: the baseline reaches the same loop end, the game's fade to black, at 120.4 s, against 97.8 s with #1377 and 97.6 s with #1379, so the same 5,947 pictures take 1.23× as long (0.81×). The fade, about 5.6 s, is there in all three. A second #1377 run gave 0.1% again.

So with the direct mode the game does present at 60, but without #1377 its player takes a new picture on only about 4 frames in 5. The decoder itself keeps up: dav1d with the host's settings (2 threads, frame delay 1) decodes this movie at 133 fps on this Mac, and the queue is full (a packet refused with 12 pictures waiting). An earlier trace of the output buffers on this build showed the player's release intervals at one frame 58% of the time with a tail of 20–50 ms (movie clock 0.78–0.82×), and the on-screen repeats lined up with the frames where it took no picture. In cutscenes that is the sound running ahead of the picture. #1377 removes it here; I have not separated its two parts (frames in flight, decode outside the lock).

The 5.5 s with an unchanged patch at 97.8 s in the #1377 runs is the game's fade to black between two loops (luminance 1.2 → 0.15 → 0 → back up), not a stall.

Fasen24-AI added a commit to Fasen24-AI/swordboiMacNative that referenced this pull request Nov 10, 2026
…han its movie skips late pictures instead of playing the movie slowly

Upstream pull requests applied in this commit (their authors' work, brought onto this tree with a three-way merge; the description is in each PR):
- seedds#1379 by MetaIX (open): AV1 pictures by the movie's clock: a game slower than its movie skips late pictures instead of playing the movie slowly
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation host The Horizon host: SVCs, services, threads, memory (dm/host)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants