Skip to content

Keep automatic frame CPU budgets per frame and per thread - #2137

Closed
thehobbit85 wants to merge 1 commit into
seedds:mainfrom
thehobbit85:codex/nvn-frame-thread-rt-budget
Closed

thehobbit85 wants to merge 1 commit into
seedds:mainfrom
thehobbit85:codex/nvn-frame-thread-rt-budget

Conversation

@thehobbit85

Copy link
Copy Markdown
Contributor

What and why

The automatic real-time CPU budget was measured on every texture acquire, so multiple acquires split a frame's CPU work and underdeclared its budget. Global sampling and policy state also mixed different threads' CPU clocks: a replacement acquire thread could skip its initial policy and subtract another thread's clock.

Sample the first acquire after a present and keep the CPU samples, declared budget and policy period per thread. Preserve the existing estimation window, margins, cap, fixed-budget mode and disabled mode. A synthetic test exercises the real acquire path while replacing only the operating-system CPU clock and policy submission; CI runs its automatic, fixed and disabled modes without a GPU.

Related to #1985. These are reproduced scheduling defects; whether either explains OoT's Castle Town slowdown remains unverified. Finding: budgets sampled per acquire or across threads, linked from the OoT note.

Checked

  • Apple M2 Max, macOS 26.6; synthetic inputs only, no game files and no actual real-time policy granted to tests.
  • Full standalone graphics suite: make -j4 -C dm/gfx/tests GAME=0400486024DEC000 run — passed.
  • make -C dm/gfx/tests GAME=010015100B514000 -W t_rt_comp.c check_t_rt_comp also passed with CI's Wonder profile.
  • New regression: 735 automatic-mode checks, 243 fixed-mode checks and 243 disabled-mode checks passed. Seven assertions failed with the original implementation. A 6 ms frame split across two acquires now declares 7.5 ms instead of 3.75 ms; this is a controlled CPU-clock test, not a gameplay speed measurement.
  • Tool/documentation suite: python3 -m unittest discover -s dm/tools/tests — final run: 1,435 tests; 1,411 passed, 23 skipped, one inherited failure. The inherited ABI-literal-ratchet failure in unchanged nvn_mtl.m also reproduces from pristine base revision 976fbd57f5cbee1f374453c2450c92ffd9f20dbc.
  • The first full tool run also failed the unchanged test_triage_report.Report.test_main_load stdout assertion; it passed both alone and in the full rerun.
  • Independent review completed. Compilation database generated; clang-tidy was unavailable locally. git diff --check passed.
  • scripts/regress.py impact BASE..HEAD was run. Usage coverage is unknown; no gameplay regression run was possible without game files. Intended checks: OoT (0400486024DEC000, reported scheduling slowdown), Pokopia (0400A3A01EE4C000, NVN2), Monster Hunter Rise (0100B04011742000, engine frame limiter), Fire Emblem Engage (0100A6301214E000, Unity), and Super Mario Bros. Wonder (010015100B514000, Nintendo NVN). None is claimed gameplay-validated by this PR.
  • Remaining estimation limitation: CPU work a thread performs during a handoff can inflate its next sample when it returns, within the existing cap. No speculative reset heuristic is added.

Docs

  • Durable reference finding, game-note link, and regenerated symptom index.
  • No shader-chain change; no game files, keys, extracted game data or agent work logs in this change.

@github-actions github-actions Bot added nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) docs Documentation ci Continuous integration and repository settings (.github) game: 0400486024DEC000 A game labels Nov 10, 2026
seedds added a commit that referenced this pull request Nov 13, 2026
The acquire thread's time-constraint record was registered once, on the first thread that
acquired, with that thread's port. A game that acquires from another thread had its CPU time
subtracted from the first thread's samples and its declaration set on the first thread's port.
Each thread that acquires now has its own slot (rt_acquire). An acquire thread that has not
acquired for a window of 120 frames goes back to the standard policy (rt_retire, only while its
port still names that thread), and declares at once what it had if it acquires again. The
thread policy line reads the declarations atomically.

OCTOPATH TRAVELER II acquires from two threads: its first is retired after its first frames.
t_rt_comp (from PR #2137, run in its auto, fixed and off modes, now in CI with t_thread_rt)
drives the acquire path: a frame acquired in two calls declares its whole 6 ms as 7.5 ms, a
second thread learns its own 1.25 ms, and an ended thread is retired with no call on its port.

Mac regression check: Mario Kart World and OCTOPATH TRAVELER II pass. Octopath had GPU stalls
(one command buffer 120-160 ms) in 2 of 9 runs with the frame threads declaring, within half an
hour of each other, and in none of the next 7 nor of 18 runs without; whether the declarations
play a part is recorded as open in docs/reference/frame-rate.md.

Co-authored-by: Eliran Zach <4012041+thehobbit85@users.noreply.github.com>
@seedds

seedds commented Nov 13, 2026

Copy link
Copy Markdown
Owner

Thank you: the diagnosis holds and is on main in b06f4e0 (you are credited as co-author), on top of PR #2569 (4c71083), which rewrote this code. Sampling once per presented frame came with #2569. The per-thread part is now a slot per acquiring thread, so each thread's CPU clock and policy are its own, and a thread that stops acquiring for 120 frames goes back to the standard policy. Your acquire-path test is in as dm/gfx/tests/t_rt_comp.c, adapted to the new code: it runs its auto, fixed and off modes in one call and is in CI's CPU-only list. Whether this played a part in #1985's Castle Town slowdown is still open, so that issue stays open.

@seedds seedds closed this Nov 13, 2026
iDams pushed a commit to iDams/nxnative that referenced this pull request Nov 13, 2026
…dds#2137)

The acquire thread's time-constraint record was registered once, on the first thread that
acquired, with that thread's port. A game that acquires from another thread had its CPU time
subtracted from the first thread's samples and its declaration set on the first thread's port.
Each thread that acquires now has its own slot (rt_acquire). An acquire thread that has not
acquired for a window of 120 frames goes back to the standard policy (rt_retire, only while its
port still names that thread), and declares at once what it had if it acquires again. The
thread policy line reads the declarations atomically.

OCTOPATH TRAVELER II acquires from two threads: its first is retired after its first frames.
t_rt_comp (from PR seedds#2137, run in its auto, fixed and off modes, now in CI with t_thread_rt)
drives the acquire path: a frame acquired in two calls declares its whole 6 ms as 7.5 ms, a
second thread learns its own 1.25 ms, and an ended thread is retired with no call on its port.

Mac regression check: Mario Kart World and OCTOPATH TRAVELER II pass. Octopath had GPU stalls
(one command buffer 120-160 ms) in 2 of 9 runs with the frame threads declaring, within half an
hour of each other, and in none of the next 7 nor of 18 runs without; whether the declarations
play a part is recorded as open in docs/reference/frame-rate.md.

Co-authored-by: Eliran Zach <4012041+thehobbit85@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci Continuous integration and repository settings (.github) docs Documentation game: 0400486024DEC000 A game nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants