Host: crash report never waits forever on stderr; Engage demo crash fixed

- crash.c: before printing a crash or time-limit report, the handler
  tries the stderr lock for 1 s. If another thread holds it, it writes
  one line (signal, pc / lr as module offsets) with write() and exits,
  instead of hanging with the process and leaving an empty log. This
  is for the intermittent early stalls, where even the SIGALRM report
  never appeared.
- Engage's idle attract-demo crash (about 220 s) was the same alias
  address-space shortage as the battle crash: the particle index
  buffers got no mapping past 2 MB. With the node-area cut (fe9bab9) an
  850 s idle run is clean, with no corrupted texture objects; with the
  old SDK the same run dies at 159 s in Unity's heap allocator.
- Docs: the 04-graphics entry is marked fixed with the cause; 03-bringup
  and the README Engage row updated. Mario regression t65 clean; the
  iOS project compiles.
This commit is contained in:
seedds committed 2026-10-15 02:05:23 +08:00
1 parent 4ab6d84190
commit 3ab0ea16cd
4 files changed
+19 -4

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@ The project is built from the generic parts of [d2r-mobile](../d2r-mobile) (MIT)
| Game | Profile | State |
|---|---|---|
| Super Mario Bros. Wonder (base game v0, update 1.2.1) | `smbw` | **Playable on the iPhone with sound through several levels**: an 11-minute session at 60 fps with no crash and no audio dropouts. Scripted Mac runs reach the opening course. Notes are in the [Super Mario Bros. Wonder](#super-mario-bros-wonder-smbw) section |
| Fire Emblem Engage | `fe_engage` | Bring-up (Unity 2020.3 IL2CPP, SDK 12.3.7): **on the Mac it reaches the title screen** and, left idle, its 3D attract demo. `shader_split.py` reads the shaders out of the Unity bundles (9,268 of 9,269 compile). Movies play through a host decoder (VP9 with libvpx). Scripted input starts a new game and plays into the prologue battle (a 12-minute run ends without a crash). Not yet: a crash in the attract demo, iOS (the profile now fits iOS's 6 GB reservation) |
| Fire Emblem Engage | `fe_engage` | Bring-up (Unity 2020.3 IL2CPP, SDK 12.3.7): **on the Mac it reaches the title screen** and, left idle, its 3D attract demo. `shader_split.py` reads the shaders out of the Unity bundles (9,268 of 9,269 compile). Movies play through a host decoder (VP9 with libvpx). Scripted input starts a new game and plays into the prologue battle (a 12-minute run ends without a crash). Not yet: iOS (the profile fits iOS's 6 GB reservation) |
## Status
+15
View File
@@ -148,6 +148,21 @@ static void on_crash(int sig, siginfo_t *si, void *uc_) {
for (int i = 0; i < g_nfault_hook; i++) if (g_fault_hook[i](si->si_addr, is_write, uc)) return;
}
crash_gate(sig);
// stderr 的锁可能被别的线程拿着(它卡住了、或被挂起了):下面的 fprintf 会跟着卡死,时限的 SIGALRM 也收不了尾,日志里一个字没有。
// 等 1 s 还拿不到就只用 write 打一行(pc / lr 换成模块偏移)再退出。本线程自己拿着锁时 ftrylockfile 照样成功(文件锁可重入),不影响
// The stderr lock may be held by another thread (stuck or suspended): the fprintf below would hang with it, the time limit's SIGALRM could not end the run, and the log would get nothing.
// If it still cannot be taken after 1 s, write a single line with write() (pc / lr as module offsets) and exit. When this thread itself holds the lock, ftrylockfile still succeeds (file locks are recursive), unaffected
for (int i = 0; ftrylockfile(stderr); i++) {
if (i < 100) { usleep(10000); continue; }
char b[256]; int n = snprintf(b, sizeof b, "[dm] CRASH sig=%d(stderr 被占用 1 s,现场不打 | stderr held for 1 s, no dump)", sig);
uint64_t at[2] = {uc->uc_mcontext->__ss.__pc, uc->uc_mcontext->__ss.__lr};
for (int k = 0; k < 2; k++) { module_t *m = module_of((void *)at[k]);
n += m ? snprintf(b + n, sizeof b - n, " %s=%s+%#llx", k ? "lr" : "pc", m->name, at[k] - (uint64_t)m->base) : snprintf(b + n, sizeof b - n, " %s=%#llx", k ? "lr" : "pc", at[k]); }
n += snprintf(b + n, sizeof b - n, "\n");
if (write(2, b, (size_t)n) < 0) {}
_exit(128 + sig);
}
funlockfile(stderr);
fprintf(stderr, "[dm] CRASH sig=%d", sig);
off("addr", (uint64_t)si->si_addr); off("pc", uc->uc_mcontext->__ss.__pc); off("lr", uc->uc_mcontext->__ss.__lr);
fprintf(stderr, " sp=%#llx\n", uc->uc_mcontext->__ss.__sp);
+2 -2
View File
@@ -52,7 +52,7 @@ Each entry: symptom → root cause → fix (where the fix lives). Check these fi
- **Per-core job queues never drained** → a `SetThreadCoreMask` that pins a thread to other cores (ideal core −3 = keep) must move the ideal core into the new mask, as Horizon does, so `GetCurrentProcessorNumber` reports a core the thread can run on.
- **Garbage-collected titles (Unity / IL2CPP): `SetThreadActivity` (0x32) and `GetThreadContext3` (0x33) must really pause and report registers** → the Boehm collector pauses each thread, reads its registers for roots and scans its stack. As placeholders that only reported success, threads kept running and the context was empty: roots held only in registers were missed and live objects were freed, with their finalizers releasing native resources. In `hle_thread.c` a thread inside an SVC counts as paused at once, with the gate frame as its context (`thr_svc_enter` / `thr_svc_leave` around every gate SVC; it waits at the SVC exit while paused). A thread in guest code gets SIGUSR2, and the handler saves the ucontext and waits there. A thread in host code is retried every 100 µs. The context is Horizon's 0x320-byte `ThreadContext`.
- **`SignalToAddress` type 2 (`SignalAndModifyByWaitingCountIfEqual`) with count ≤ 0 and waiters present** → the new value is `value − 2`, not `value − 1` (mesosphere `KAddressArbiter`; Ryujinx and yuzu agree). The host had −1. Fixing it did not change Engage's demo crash below, but it is the specified behaviour.
- **`SignalToAddress` type 2 (`SignalAndModifyByWaitingCountIfEqual`) with count ≤ 0 and waiters present** → the new value is `value − 2`, not `value − 1` (mesosphere `KAddressArbiter`; Ryujinx and yuzu agree). The host had −1. Fixing it did not change Engage's demo crash (that was the address-space shortage above), but it is the specified behaviour.
- **Several threads crash at once and the dumps interleave, then the process hangs instead of exiting** → Unity's job threads tend to crash together. `crash.c` lets only the first crashing thread print (`crash_gate`); the others stop in `pause()`, and a 15 s alarm ends the process if the dump itself stalls.
- **Crash indexing a per-core table with a huge offset (index −2)** → threads created with ideal core −2 (*use process default*) were recorded as core −2 and `GetCurrentProcessorNumber` returned −2 → resolve −2 to the process default core (0) and never return a negative core (`hle_thread.c`). Seen in Mario's font engine (`main+0x107dbc0`).
@@ -82,7 +82,7 @@ Each entry: symptom → root cause → fix (where the fix lives). Check these fi
- **`GetCommandMemoryUsed` must count everything the recording used**, including the parts that went into host segments (plus 16 bytes for the jump). Mario marks a recorded block as empty when the count is 0 and then skips calling it (see the next entry for how that happened).
- **Frames stop, no crash, the main thread sits in `nvnSyncWait(sync, -1)`, and the watchdog's wait-state line shows the main fence one short (`已提交 N / 已完成 N-1`)** → a GPU command buffer never completes. Check GPU load from outside: `ioreg -r -d 1 -c IOAccelerator | grep 'Device Utilization'` at 100 % means a shader is looping forever. Find the shaders first used in the last frames (`DM_NVN_PASSDUMP`, compare the `K` keys) and look at their `.metal` for an empty `do {} while (true);` with no `return` (see [04](04-graphics.md) section 1: the compute program start).
- **Metal aborts on a texture descriptor with an absurd size (`height (1869367126) greater than … 16384`)** → a command record kept a pointer to the game's `NVNtexture` and read it at replay. NVN commands do not depend on the API object once recorded, so the host now copies the texture object into the record (`REC_SNAP` / `tex_snap` in `hle_nvn.c`: render targets, texture copies and clears). Replay rejects objects without the texture magic and sizes beyond Metal's limits, with a red line instead of an abort. The first red lines name the call site and the object's first bytes; in Engage those bytes were 32-bit triangle indices, i.e. another write had already overrun into the texture object (see the Engage entry in [04](04-graphics.md)).
- **Metal aborts on a texture descriptor with an absurd size (`height (1869367126) greater than … 16384`)** → a command record kept a pointer to the game's `NVNtexture` and read it at replay. NVN commands do not depend on the API object once recorded, so the host now copies the texture object into the record (`REC_SNAP` / `tex_snap` in `hle_nvn.c`: render targets, texture copies and clears). Replay rejects objects without the texture magic and sizes beyond Metal's limits, with a red line instead of an abort. The first red lines name the call site and the object's first bytes; in Engage those bytes were 32-bit triangle indices, i.e. another write had already overrun into the texture object; that stopped once the alias region had address space left (see the Engage entry in [04](04-graphics.md)).
- **Shaders reported missing at random, and their dumped control sections are garbage (Unity)** → NVN lets the caller free a program's control section once `nvnProgramSetShaders` returns; Unity does, and the host used to hash it at the first draw. The keys are now computed inside `SetShaders` (`hle_nvn.c` `prog_key`). Mario calls `SetShaders` for some programs with a code address that is not mapped yet, so a stage whose code is not in a registered memory pool at that moment is hashed at its first draw, as before.
- **Video frames, or a game's built-in default textures, are all zero** → the texture is block-linear, has no packaged layout and is written from the CPU with `nvnTextureWriteTexels` (Engage's video frames: Y plane `R8` + UV plane `RG8`, every frame; Mario's and Unity's 4×4 fallback textures, which hold black, white, transparent and red texels). The host uploaded only packaged textures from guest memory and made everything else a GPU-only texture. `WriteTexels` now records the texture, and it resolves like a packaged one: uploaded from guest memory, re-uploaded when its pages are dirtied (`mtl_tex_cpu_written`). Mario's frames got slightly brighter where materials sample the default white texture.
- **Menus and dialogue boxes draw but contain no text; images and pictogram digits are fine** → the game rasterizes glyphs on the CPU into a *linear* texture in a CPU-visible pool (Mario: 2048×8192 R8, flags IMAGE|LINEAR). A private Metal texture never sees those writes. Linear textures in CPU-visible pools are now created on the pool buffer (`tex_linear` in `nvn_mtl.m`). `DM_NVN_TEXCPU=1` lists non-packaged textures in CPU-visible pools.
+1 -1
View File
@@ -72,7 +72,7 @@ Consequence: any optimization that infers "which textures a draw reads" from bou
- **Video**: Unity's `Hidden/VideoDecode` samples luma as `.w` of an `R8` texture with the swizzle `(0, 0, 0, R)` (Unity's Alpha8) and chroma from an `RG8` texture at half size. To check the movie path apart from the decoder, fill the frames with a gradient in `hle_movie.c`; it should appear on screen.
- **One geometry shader** (in `unity_builtin_extra`): Metal has no geometry stage, so it fails to compile (`unknown main0_out`); nothing in the opening scenes uses it.
- **Open: a crash a few minutes into the prologue battle (scripted run, about 357 s).** It writes to `NULL + 0x1fffe0` at `main+0x9a4664`. Unity's TLSF heap allocator (`main+0x9a45d0`) asks its low-level virtual allocator (`[allocator + 0x168]`) for a new 2 MB pool, and the block it gets back has a NULL pointer. The host's memory SVCs all succeed up to that point, and only about 0.5 GB is mapped, so Unity's own reserved address range is the suspect. Separately, about one run in four stalls early (no log output, not even the time-limit report). `hangwatch.sh`-style sampling (`sample <pid>` when the log stops growing) has not caught one yet.
- **Open: the idle attract demo crashes after about 220 s.** Unity's particle geometry jobs write through a NULL index pointer, on several job threads at once (`main+0xdb0ad4`, `0xdaea58`, `0xdadb5c`). The scheduler (`main+0xd91e0c`) takes its vertex and index chunks from a fallback that creates and maps a fresh buffer each time (`main+0x7b9c64`; Unity's Switch renderer always takes it, as on hardware). The job (`main+0xd92eb0` → `0xd93194` → the index writers `0xdb0100` / `0xdad270` / `0xdae0c0`) then copies mesh indices per particle. Earlier, from the demo's scene load at about 180 s, those 32-bit mesh indices already appear inside Unity's texture objects (the bad render targets in [03](03-bringup.md)): the job writes more indices than the main thread sized for. By about 223 s the requested index buffers pass 2 MB, the map returns NULL, and the next write crashes. Why the main thread's count and the job's disagree on the host is not found yet; the arbiter fix in [03](03-bringup.md) did not change it. Reproduce: `boot.sh <log> 245` with no input.
- **Fixed: the idle attract demo crashed after about 220 s.** Unity's particle geometry jobs wrote through a NULL index pointer on several job threads at once (`main+0xdb0ad4`, `0xdaea58`, `0xdadb5c`). The scheduler (`main+0xd91e0c`) takes its vertex and index chunks from a fallback that creates and maps a fresh buffer each time (`main+0x7b9c64`; Unity's Switch renderer always takes it, as on hardware), and from about 180 s 32-bit mesh indices also turned up inside Unity's texture objects. The cause was the alias region running out of address space, as in the battle crash ([03](03-bringup.md), "A null-based write minutes into play"): past 2 MB the index buffers got no mapping. With the node-area cut an 850 s idle run is clean, with no corrupted texture objects; with the old SDK the same run dies at 159 s in Unity's heap allocator. Reproduce the old state: `boot.sh <log> 245` with no input on an SDK rewritten without the cut.
## 7. Diagnostic switches (host)