Skip to content

A GPU whose access macOS revoked stops the game instead of leaving it running with nothing drawn - #1332

Closed
PucaVaz wants to merge 1 commit into
seedds:mainfrom
PucaVaz:gpu-lost-failfast
Closed

PucaVaz wants to merge 1 commit into
seedds:mainfrom
PucaVaz:gpu-lost-failfast

Conversation

@PucaVaz

@PucaVaz PucaVaz commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Checked in the game (Ocarina of Time, M4 24 GB, macOS 27.2; details in the update comment): the revoke line once, the picture stops while the game keeps running, the alert, and Quit exits with 0. Rebased on 865cc35f.

Refs #1326

What and why

In #1326 (The Legend of Zelda: Ocarina of Time, 0400486024DEC000 v131072, on a Mac), a GPU hang (run 1, full screen) or GPU page faults (run 2, windowed) were followed by every later upload, segment, submission and display command buffer coming back MTL4CommandQueueErrorAccessRevoked ("Ignored (for causing prior/excessive GPU errors)", kIOGPUCommandBufferCallbackErrorSubmissionsIgnored). The host logged them and went on:

  • in run 2, nothing was drawn while the game's sound and logic kept running for about 70 s, until the game aborted itself;
  • in run 1, in full screen, WindowServer froze for 40 s and its watchdog killed it.

Cause. macOS revokes a process's GPU access after GPU errors it caused, and from then on runs none of its command buffers. They come back at once with that error, so their feedback still arrives. But the GPU no longer signals the shared event that a game's SyncWait waits on (for up to an hour, mtl_wait_at), and every present takes a drawable that nothing draws.

The change (nvn_mtl.m, front.h, front_mac.m). gpu_lost_check looks for the revoked access in each command buffer's error. The first one:

  • stops presents (present_now);
  • raises the shared event from the CPU to the last fence whose feedback has come back. It does so again at each later submission's feedback (gpu_lost_release), so no SyncWait waits for a signal that will not come;
  • tells the front end once (front_gpu_lost). The Mac app leaves full screen, says why in an alert that points to the log and scripts/bug_report.py, and quits as ⌘Q does. Without a window, the run ends with a FATAL line and an abort (the weak default in nvn_mtl.m).

Why the access being revoked, and not the first timeout or page fault. The two #1326 logs show that the GPU recovers from those on its own:

  • Run 2 had innocent-victim errors (code 1, timeout) from 219 s, and the game recovered each time and kept playing. The page fault (code 2) came at fences 110102 and 110108. From fence 110114, two frames later, every command buffer came back code 5 until the game aborted.
  • Run 1 had the hang (code 1) in two segment command buffers, then code 5 on every command buffer after them.

Stopping at the first fault would have ended run 2 at 219 s while it was still playable. (My first comment on #1326 said "timeout, page fault or access-revoked"; this is the narrower trigger.)

Two changes from the first commit of this PR:

  • The error is looked for underneath. Metal 4 reports a commit's error as MTL4CommandQueueErrorTimeout with no text at the top, and the command buffer's own error under NSUnderlyingErrorKey / NSMultipleUnderlyingErrorsKey. Every logged command buffer error in both Ocarina of Time (0400486024DEC000 v131072, Mac): GPU hang about 10 minutes into play takes WindowServer down #1326 logs has that shape. The check walks those, in MTL4CommandQueueErrorDomain only: an innocent victim's IOGPU error has code 5 too.
  • The event is raised at feedback, not at commit. The first commit raised it to g_fence when a submission was committed. A SyncWait could then mark a fence done (post_early moves g_done) before Metal had handed the command buffer back. The next submission could then reset a command allocator Metal still held, because mtl_sub_begin reuses allocators whose fence is at or below g_done. Raising it to g_done at each feedback keeps g_done tied to Metal's feedback.

iPhone. The weak default stays (a FATAL line in log.txt and an abort). The app has no quit of its own to follow, and I cannot try it on a device. Before this, the game ran on with nothing drawn.

Not covered. The Vulkan and OpenGL layers keep their own queues and waits. Their presents go through present_now, though, so a revoked display command buffer still tells the front end.

This complements #1275's GPU hang defences rather than replacing them. Its watchdog fires when a fence has not completed after 4 s. Here the ignored command buffers complete at once, with an error, so it never fires.

Rendering reference entry. The game note dm/games/0400486024DEC000/README.md is not on main (the profile is in #1299), so the entry links the issue, and the note gets no "Found on this game" line here.

Checked

  • Tests (Apple silicon, Metal):
    • make -C dm/gfx/tests GAME=010015100B514000 t_gpu_lost && MTL_DEBUG_LAYER=1 ./t_gpu_lost: 14 checks, 0 failed, five times, and three more with DM_NVN_ENC_THREAD=1. It checks:

      • the error's shape: the revoked access is found under Metal's top-level timeout; an innocent victim's, a page fault's or a hang's error is not taken for it;
      • front_gpu_lost is called exactly once over four revoked submissions;
      • a SyncWait on a revoked fence returns at its feedback (1–6 ms), for the later submissions too;
      • a present after it no longer asks for the layer, and its ticket completes.

      With the event release taken out, the four SyncWait checks fail (each waits out its 2 s timeout). With the first commit's top-level-only check, 7 of 14 fail.

    • make -C dm/gfx/tests GAME=010015100B514000 -j4 run: every test passed (exit 0). t_refl skipped 2 checks for sample files that are not on this Mac. This ran before the rebase onto 7249ee9f, which changes only tools and docs. t_gpu_lost was built and run again after it.

    • python3 -m unittest dm/tools/tests/test_docs.py dm/tools/tests/test_env_switches.py: 12 tests OK; python3 dm/tools/env_switches.py --check: OK.

    • front_mac.m compiled with the host's flags (the strong front_gpu_lost is in its object). The full host was not built, and clang-tidy ran in CI: the two findings in t_gpu_lost.m are fixed; the one left is in t_int_float_view.m from main (fixed by Tests check the Metal device, textures, encoders and functions they make before using them #1338).

  • Games run: The Legend of Zelda: Ocarina of Time (0400486024DEC000, v131072), on lunks's 574a38f with this change, windowed, with DM_TEST_GPU_LOST=1500 (the imitation; no real revoked access was reproduced). In two runs the [RED] GPU access revoked line came once (at 15.3 s and 8.2 s), the picture stopped while the game kept running at its normal frame times, and the alert came; Quit exited with 0. The alert was hidden behind a background-opened or DM_WINDOW_TOP game window; 750bda24 activates the app and raises the alert above it (compiled; not yet re-run in the game). Leaving full screen was not exercised: both runs were windowed.
  • Devices and OS: M4 (24 GB), macOS 27.2, Xcode 27.2.
  • scripts/regress.py check --games: not run. My only local game build is busy, and the change's reach is narrow: the new code runs only on a command buffer error, plus one early return in present_now once the access is revoked. On a run without a revoked access, the new work is a flag test per submission and, on a command buffer error, a walk of the errors under it.

Front ends

No Front-ends: line. After the alert, a Mac app exits with 0, as for ⌘Q. Before, a run like this ended in the game's own abort (the launcher returned 134) or in WindowServer's kill. docs/frontends.md documents no exit status for a game run. If a non-zero status would be better for launchers (#1275's watchdog uses 70), that is a small change.

Docs

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

Drafted with Claude; reviewed by me.

🤖 Generated with Claude Code

@github-actions github-actions Bot added the nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) label Nov 5, 2026
@PucaVaz

PucaVaz commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Status: I'm building Ocarina of Time (0400486024DEC000, v131072 from a NUP, with #1299 and #1312) on an M4 (24 GB), macOS 27.2, Xcode 27.2, to check this change against the real game. I'll post an update here once the build is done and I've played it.

Drafted with Claude; reviewed by me.

@PucaVaz
PucaVaz marked this pull request as ready for review November 5, 2026 14:29
@github-actions github-actions Bot added host The Horizon host: SVCs, services, threads, memory (dm/host) mac The Mac front end and app (dm/mac, front_mac.m) docs Documentation labels Nov 5, 2026
@PucaVaz

PucaVaz commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

clang-tidy: the two findings in this PR's t_gpu_lost.m are fixed (setvbuf, and a NOLINT block for the texture serial_put keeps, as in t_attachment_capacity.m). The one left is in dm/gfx/tests/t_int_float_view.m from main's f083873 (NullablePassedToNonnull), which this PR doesn't touch; #1343's clang-tidy check fails on the same finding.

Drafted with Claude; reviewed by me.

@PucaVaz

PucaVaz commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Update after building and playing, as promised: checked in Ocarina of Time on an M4 (24 GB), macOS 27.2.

Setup: lunks's 574a38f (the pool-table fix branch) with this PR's change on top, windowed, DM_TEST_GPU_LOST=1500 so the revocation happens during the title movie.

  • [RED] GPU access revoked: the submission command buffer was ignored (fences back 1499 of 1501); … appears once, followed by front end: the GPU no longer runs the game's work; telling the player and quitting. It came at 15.3 s in one run and 8.2 s in the other.
  • After it the picture stops while the game keeps running at its normal frame times, so no SyncWait stayed blocked.
  • The alert appears. Clicking Quit goes through terminate: and the app exits with code 0.

One fix from the test, now in the PR (750bda24): when the app was opened in the background the alert stayed behind other apps, and with DM_WINDOW_TOP=1 the floating game window covered it. The alert now activates the app and sits above the game window. This last change is compiled but not yet re-run in the game.

On the previous head the build check, host tests, sanitizers and iOS app passed (coverage and tool tests were still running); the one clang-tidy finding left is in t_int_float_view.m from main, which #1338 fixes.

Drafted with Claude; reviewed by me.

fpedrolucas95 pushed a commit to fpedrolucas95/nxnative that referenced this pull request Nov 5, 2026
…rings upstream main to 7249ee9) into main

The translator binaries, its lock and dmsh_chain.h are kept from main here and rebuilt from the merged source after the Ocarina merges.
fpedrolucas95 pushed a commit to fpedrolucas95/nxnative that referenced this pull request Nov 5, 2026
…pstream's VOTEU, the growing raw pool table): chain c7de80c08992

The merges of seedds#1332 and the pool table commit kept main's translator binaries; this publishes Ryujinx.Graphics.Shader, Ryujinx.Common, Spv.Generator and dmsh_xlate
(osx-arm64, framework-dependent, the universal launcher kept) from the merged source, with the lock and dmsh_chain.h written again. RawGlobal's test takes its own storage
slot setup instead of AtomgCas's, a class of a Star Fox commit not in this tree. Every test_dmsh_*.py passes against it (DMSH_TEST_XLATE), and t_gmem_raw, t_pool_chunks,
t_mesh_indirect, t_deferred_release, t_image_usage, t_image_alias, t_gpu_lost, t_sbw, t_ubp_running and t_unwritten_store pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 5, 2026
… the game instead of leaving it running with nothing drawn

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

# Conflicts:
#	dm/gfx/nvn/nvn_mtl.m
gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 6, 2026
…d of freezing the Mac

A session of Ocarina of Time ended in a GPU out-of-memory and a stuck game, and seedds#1326 reports a Mac frozen until the power button: GPU memory is wired, IOGPUScheduler's freeToWireMemory fails at recommendedMaxWorkingSetSize, and the submit thread then waits in the kernel where the process cannot be killed. Nothing in the host looked at the working set.

res_commit now reads the larger of the set's allocatedSize and the device's currentAllocatedSize before it commits; the set's size leaves out what was never committed (measured: a new 64 MB texture adds 0 to it until the commit), the device's counts it from its creation. From 80% of the working set the submit thread lets go of what can go: idle sharing entries nothing holds, finished deferred releases, spares and the upload slots' staging. From 95%, if that was not enough, the game stops with a [RED] report of what the set holds, by kind. A commit that adds to the set is not made at the working set itself. A 100 ms timer reads the sizes too, as a stuck submit thread never comes back to look.

The stop reaches the launcher as a normal exit: the Mac app closes the graphics gate, leaves full screen, shows an alert and quits as Cmd-Q does, the path PucaVaz's PR seedds#1332 takes for a revoked GPU (front_stop_alert is written so front_gpu_lost can call it); without a window the host takes its graceful exit.

DM_NVN_RES_LIMIT_MB and the report's distinct pages and texture formats come from JioChoi's PR seedds#1319, moved from after the commit to before it. iOS only evicts unless the switch sets a limit: no iPhone has tried the stop.

t_res_guard covers the levels, the commit refused and removals passing, what stays (a live object, the parent of a live sRGB view, a recent lookup), the second look at the stop level, and the timer with the lock held.
gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 6, 2026
…cy commit stops the game instead of freezing the Mac

front_mac.m: seedds#1332's front_gpu_lost now goes through this pull request's front_stop_alert; alias_let_go keeps the submission's finalized serials (g_ghost_active); the memory line keeps NXForge's GPU allocation by kind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g
AnkMister added a commit to AnkMister/nxnative that referenced this pull request Nov 6, 2026
Revised brief: keep only PR seedds#1434 (9eeacbb), seedds#1332 (7f5f2fc), seedds#1417 (1c1d453), and seedds#1428 (f537147). Remove all other prior ports and their follow-ups while preserving published history. Other glass and Mac agents own those items. Update the scoped report and references; exclude disabled SDK hooks from hit diagnostics.

Python tools match the baseline failure set; 12 docs/environment checks pass. Portable SDK gate and diagnostic checks pass GCC ASan/UBSan. Mac and Metal checks remain open.
gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 6, 2026
… layers, seedds#1345 and seedds#1373 landed)

Upstream's c83c48f makes tex_fit's views from layer counts, which covers the cube read as a 2D array, so NXForge's own version of that (f17b3e7) is reverted before this merge. hle_audren.c and its notes take upstream's landed seedds#1345. The failed command buffer report keeps both seedds#1382's segment names (cbn_close) and upstream's shader keys (cbs_feedback), and seedds#1332's gpu_lost_check. The present keeps NXForge's sidebar inset on upstream's present_fit. The translator binaries, lock and chain header are upstream's until the translator is rebuilt on the Mac.

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
Aeonitis pushed a commit to Aeonitis/nxnative that referenced this pull request Nov 6, 2026
…er and shader chain, the branch's host services, movie clock, packing and Ocarina of Time additions

Upstream reached about 60 fps in Kokiri Forest with its own versions of most of the branch's merged PRs, so where both changed the same code upstream's is kept:
- The shader chain is upstream's (translator, lock, dmsh tools, Ryujinx sources), plus the branch's bracket-depth flattening (dmsh_depth.py, test_dmsh_depth.py); chain c732699d4fdb.
  Not carried: the LDSM subgroup fence (seedds#1376, needs a translator rebuild; upstream's HMMA/LDSM rework reports clean movies) and the shared raw-memory lookups (seedds#1405, superseded by upstream's last-entry lookups).
- The renderer (dm/gfx/nvn, dm/gfx/msl) and its tests are upstream's. Not carried: seedds#1390-seedds#1394, seedds#1404, seedds#1408, seedds#1409, seedds#1417, seedds#1418, seedds#1420, seedds#1427, seedds#1320, seedds#1331, seedds#1332, seedds#1368, seedds#1285 renderer parts, pool_skip_retired, and the branch's warning fixes and prewarm progress there. They stay on oot-integration.
- Ocarina of Time's profile is upstream's, with the branch's 12 movie compute rows in pso_manifest.tsv, PLAN.md and PR-AUDIT.md.
- shader_split.py is upstream's plus the BAHS record check (test_split_bahs_records.py).
- Host: upstream's EventFd, PlayReport send, audio lock and vibration answers; the branch's service sessions (hle_sf, hle_prepo), sync waits, AV1 movie clock and parallel decode, diag_abort frame fix and focus control are kept.
- Scripts: shader.noindex, tmp.noindex, notes outside the shader key, bench and regress changes.

Checked: the Mac host builds (GAME=0400486024DEC000); host unit tests pass; the Python tool tests pass on Python 3.13 except test_dmsh_pool_arrays, test_nro_pack, test_nsp_dump and test_nsp_incremental, which fail on upstream main as well. Not run: the full shader build on this chain, gameplay.
… running with nothing drawn

In issue seedds#1326 (Ocarina of Time on a Mac), a GPU hang in full screen and, in a second run, GPU page faults in a window were followed by every upload, segment, submission and display command buffer coming back MTL4CommandQueueErrorAccessRevoked ("Ignored (for causing prior/excessive GPU errors)", kIOGPUCommandBufferCallbackErrorSubmissionsIgnored). The host logged them and went on. In the windowed run nothing was drawn while the game's sound and logic kept running for about 70 s, until the game aborted itself. In full screen, WindowServer froze for 40 s and its watchdog killed it.

macOS revokes a process's GPU access after GPU errors it caused, and from then on runs none of its command buffers. Their feedback still comes, at once, but the GPU no longer signals the shared event a game's SyncWait waits on, and every present takes a drawable that nothing draws. Metal 4 reports a commit's error as MTL4CommandQueueErrorTimeout with no text, with the command buffer's own error under NSUnderlyingErrorKey and NSMultipleUnderlyingErrorsKey; every logged error in both seedds#1326 logs has that shape.

gpu_lost_check looks for the revoked access in each command buffer's error and the errors under it, in MTL4CommandQueueErrorDomain only (an innocent victim's IOGPU error has code 5 too). The first one:
- stops presents;
- raises the shared event from the CPU to the last fence whose feedback has come back, and again at each later submission's feedback, so g_done still follows Metal's feedback and no command allocator is reset before Metal hands its command buffer back;
- calls front_gpu_lost (front.h) once. The Mac app leaves full screen, says why in an alert that points to the log and scripts/bug_report.py, and quits as ⌘Q does. Without a window, and on the iPhone (the weak default in nvn_mtl.m), the run ends with a FATAL line and an abort.

A hang or a fault alone does not stop the game. The second run recovered from several innocent-victim timeouts, then page-faulted at fences 110102 and 110108, and its command buffers were ignored from fence 110114, two frames later. DM_TEST_GPU_LOST=<n> imitates the revoked access from the nth submission on.

Checked on an M4 (24 GB), macOS 27.2, Xcode 27.2:
- t_gpu_lost, a new test: the error's shape, one call to the front end, SyncWaits on revoked fences returning at their feedback, and presents stopped. It fails with the event release taken out, and with only the top-level code compared.
- make -C dm/gfx/tests GAME=010015100B514000 run.
- The docs and switch-table tests.

No real revoked access was reproduced, and regress.py was not run: the change reaches only error paths, and one early return in present_now once the access is revoked.
@PucaVaz

PucaVaz commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main b2d46b86. One conflict, with f956ffb's failed-command-buffer shader report: both feedback handlers now call cbs_feedback first, then gpu_lost_check, so the report is in the log before a windowless abort. The rendering entry now links Ocarina of Time's note, which is on main now, and the note lists it under "Found on this game".

Checked on an M4 (24 GB), macOS 27.2: t_gpu_lost 14 checks, 0 failed (5 runs, plus 3 with DM_NVN_ENC_THREAD=1); with the event release taken out, 4 fail. t_feedback 193/0, t_cbs_fault 14/0, t_present_alloc 13/0. Of the Metal suite, 109 of 110 check targets pass; t_fuse_check fails 2 of 10 the same way with main's nvn_mtl.m, and t_nvn and t_gfx_suspend don't link (dm_vi_frame_queued), which this change doesn't touch. The Mac host builds.

The reports since show the state this PR handles:

This PR ends the game at the first of those ignored command buffers. It does not fix the hangs and faults that lead there, and the reports don't show whether stopping earlier avoids every later Mac freeze or panic.

#1391's front_stop_alert is this PR's alert code made into a helper. If you'd rather have it shared, I have a commit that makes front_gpu_lost call it, with the helper copied unchanged, so whichever lands second keeps one copy.

seedds added a commit that referenced this pull request Nov 7, 2026
… running with nothing drawn (PR #1332)

In issue #1326 (Ocarina of Time on a Mac), a GPU hang in full screen and, in a second run, GPU page faults in a window were followed by every upload, segment, submission and display command buffer coming back MTL4CommandQueueErrorAccessRevoked ("Ignored (for causing prior/excessive GPU errors)", kIOGPUCommandBufferCallbackErrorSubmissionsIgnored). The host logged them and went on. In the windowed run nothing was drawn while the game's sound and logic kept running for about 70 s, until the game aborted itself. In full screen, WindowServer froze for 40 s and its watchdog killed it. Reports in #1265 and #1326 show the same state ending in a frozen Mac or a kernel panic.

macOS revokes a process's GPU access after GPU errors it caused, and from then on runs none of its command buffers. Their feedback still comes, at once, but the GPU no longer signals the shared event a game's SyncWait waits on, and every present takes a drawable that nothing draws. Metal 4 reports a commit's error as MTL4CommandQueueErrorTimeout with no text, with the command buffer's own error under NSUnderlyingErrorKey and NSMultipleUnderlyingErrorsKey.

gpu_lost_check looks for the revoked access in each command buffer's error and the errors under it, in MTL4CommandQueueErrorDomain only (an innocent victim's IOGPU error has code 5 too). The first one:
- stops presents;
- raises the shared event from the CPU to the last fence whose feedback has come back, and again at each later submission's feedback, so g_done still follows Metal's feedback and no command allocator is reset before Metal hands its command buffer back;
- calls front_gpu_lost (front.h) once. The Mac app leaves full screen, says why in an alert that points to the log and scripts/bug_report.py, and quits with exit status 70, so a launcher or script sees a failed run (⌘Q's is 0). Without a window, and on the iPhone (the weak default in nvn_mtl.m), the run ends with a FATAL line and an abort.

A hang or a fault alone does not stop the game: the GPU recovers from those. DM_TEST_GPU_LOST=<n> imitates the revoked access from the nth submission on; t_gpu_lost checks the error's shape, the single call, the released waits and the stopped presents. The hangs and faults that lead to the revoked access stay open.

Checked on an M4 Pro: t_gpu_lost 14 checks, 0 failed (also with DM_NVN_ENC_THREAD=1); the Mac host builds; the docs and switch-table tests pass. No real revoked access was reproduced here; a reporter's run with the PR (#1326) showed the alert and a clean quit. regress.py was not run: the new code runs only on command buffer errors, plus one flag test per present.

Refs #1326

Co-authored-by: Puca Vaz <65235669+PucaVaz@users.noreply.github.com>
@seedds

seedds commented Nov 7, 2026

Copy link
Copy Markdown
Owner

Landed as 5636e7a with you as co-author. Thank you, this is good work: the error shape under the top-level timeout, raising the event at feedback rather than at commit, and the test.

One change: after the alert the Mac app now quits with exit status 70 instead of 0 (exit(70) in place of [NSApp terminate:nil]), so a launcher or script that runs it sees a failed run. exit() runs the same atexit handlers as ⌘Q's terminate; the one will-terminate observer, haptics shutdown, is also registered with atexit. docs/frontends.md now lists the Mac app's exit statuses.

Checked on an M4 Pro: t_gpu_lost passed 14 of 14 (also with DM_NVN_ENC_THREAD=1), the Mac host builds, and the docs and switch-table tests pass. regress.py was not run because the new code runs only on command buffer errors.

@seedds seedds closed this Nov 7, 2026
john62036203-ops added a commit to john62036203-ops/NXNativeWindows that referenced this pull request Nov 7, 2026
…ir storage by GPU-written range, PR seedds#1604; a revoked GPU stops the game, PR seedds#1332) into switch2

The fork's own GPU-written range tracking (DM_NVN_GPUW_RANGE) gives way to upstream's gws_* spans, which do the same for Ocarina's terrain; the NDVE scale marks its target through mtl_tex_gpu_written. front_gpu_lost uses the fork's front_stop_alert beside front_gpu_memory_stop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013SzykpcKm2eZPvPjGPKx1C
Know-It-All-Brother pushed a commit to Know-It-All-Brother/nxnative that referenced this pull request Nov 9, 2026
… running with nothing drawn (PR seedds#1332)

In issue seedds#1326 (Ocarina of Time on a Mac), a GPU hang in full screen and, in a second run, GPU page faults in a window were followed by every upload, segment, submission and display command buffer coming back MTL4CommandQueueErrorAccessRevoked ("Ignored (for causing prior/excessive GPU errors)", kIOGPUCommandBufferCallbackErrorSubmissionsIgnored). The host logged them and went on. In the windowed run nothing was drawn while the game's sound and logic kept running for about 70 s, until the game aborted itself. In full screen, WindowServer froze for 40 s and its watchdog killed it. Reports in seedds#1265 and seedds#1326 show the same state ending in a frozen Mac or a kernel panic.

macOS revokes a process's GPU access after GPU errors it caused, and from then on runs none of its command buffers. Their feedback still comes, at once, but the GPU no longer signals the shared event a game's SyncWait waits on, and every present takes a drawable that nothing draws. Metal 4 reports a commit's error as MTL4CommandQueueErrorTimeout with no text, with the command buffer's own error under NSUnderlyingErrorKey and NSMultipleUnderlyingErrorsKey.

gpu_lost_check looks for the revoked access in each command buffer's error and the errors under it, in MTL4CommandQueueErrorDomain only (an innocent victim's IOGPU error has code 5 too). The first one:
- stops presents;
- raises the shared event from the CPU to the last fence whose feedback has come back, and again at each later submission's feedback, so g_done still follows Metal's feedback and no command allocator is reset before Metal hands its command buffer back;
- calls front_gpu_lost (front.h) once. The Mac app leaves full screen, says why in an alert that points to the log and scripts/bug_report.py, and quits with exit status 70, so a launcher or script sees a failed run (⌘Q's is 0). Without a window, and on the iPhone (the weak default in nvn_mtl.m), the run ends with a FATAL line and an abort.

A hang or a fault alone does not stop the game: the GPU recovers from those. DM_TEST_GPU_LOST=<n> imitates the revoked access from the nth submission on; t_gpu_lost checks the error's shape, the single call, the released waits and the stopped presents. The hangs and faults that lead to the revoked access stay open.

Checked on an M4 Pro: t_gpu_lost 14 checks, 0 failed (also with DM_NVN_ENC_THREAD=1); the Mac host builds; the docs and switch-table tests pass. No real revoked access was reproduced here; a reporter's run with the PR (seedds#1326) showed the alert and a clean quit. regress.py was not run: the new code runs only on command buffer errors, plus one flag test per present.

Refs seedds#1326

Co-authored-by: Puca Vaz <65235669+PucaVaz@users.noreply.github.com>
rafaeliga pushed a commit to rafaeliga/kid-with-sword-remake that referenced this pull request Nov 11, 2026
…d of freezing the Mac

A session of Ocarina of Time ended in a GPU out-of-memory and a stuck game, and seedds#1326 reports a Mac frozen until the power button: GPU memory is wired, IOGPUScheduler's freeToWireMemory fails at recommendedMaxWorkingSetSize, and the submit thread then waits in the kernel where the process cannot be killed. Nothing in the host looked at the working set.

res_commit now reads the larger of the set's allocatedSize and the device's currentAllocatedSize before it commits; the set's size leaves out what was never committed (measured: a new 64 MB texture adds 0 to it until the commit), the device's counts it from its creation. From 80% of the working set the submit thread lets go of what can go: idle sharing entries nothing holds, finished deferred releases, spares and the upload slots' staging. From 95%, if that was not enough, the game stops with a [RED] report of what the set holds, by kind. A commit that adds to the set is not made at the working set itself. A 100 ms timer reads the sizes too, as a stuck submit thread never comes back to look.

The stop reaches the launcher as a normal exit: the Mac app closes the graphics gate, leaves full screen, shows an alert and quits as Cmd-Q does, the path PucaVaz's PR seedds#1332 takes for a revoked GPU (front_stop_alert is written so front_gpu_lost can call it); without a window the host takes its graceful exit.

DM_NVN_RES_LIMIT_MB and the report's distinct pages and texture formats come from JioChoi's PR seedds#1319, moved from after the commit to before it. iOS only evicts unless the switch sets a limit: no iPhone has tried the stop.

t_res_guard covers the levels, the commit refused and removals passing, what stays (a live object, the parent of a live sRGB view, a recent lookup), the second look at the stop level, and the timer with the lock held.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation game: 0400486024DEC000 A game host The Horizon host: SVCs, services, threads, memory (dm/host) mac The Mac front end and app (dm/mac, front_mac.m) 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