ac-render (2026-10-25): Animal Crossing 3.0.3 now runs its main loop and draws (frames > 0); AC v0 crash fixed¶
All runs: Mac, headless, scripted (nobody played). ACNH data built from Testing Material: base + 3.0.3 update + 3 DLC, napple build ... --updates latest --dlc all --data-only.
Root causes¶
- Main thread spun forever in
nn::hidNpad state read (verified). The SDK's lifo reader (AC 3.0.3 sdk+0x5087a0) treats an odd outer entry sampling number as "entry being written" and retries, and then requires consecutive inner (state) sampling numbers (difference 1).input/src/hid_shm.cwrote outer = inner = 0,1,2,3..., so every odd entry sent the reader round again forever (draws 0, frames 0, main thread 100 % in the reader, the 0x71/0x72 "loader ping-pong" was the game idling on top of it). Now outer = 2*n, inner = n (all lifos). Older SDKs (LAS, SMO, v0) never checked and still run. Found with the new PC profiler (NAPPLE_PCPROF=1), live PCs in the SIGUSR1 thread dump and the register samples of the reader (x17 odd at the tbnz, x8 = 16 states). nvnProgramSetShaderscrash (SIGSEGV in gfx_shader_register, fault 0xe00). After 5772 goodProgramSetShadersExtprograms the game calls the 16-byte-strideProgramSetShaderswith code address 0xe00 / 0x1200 (a pool-relative offset, the pool base the game added was 0). Not understood on the game side; the program is now rejected with a warning (api_program.c) instead of dereferencing it.- v0 crash (SIGSEGV in
EndRecording,h->rec_chunk[h->nrecs] = lowith rec_chunk NULL while recs was set; earlier run:GetCommandMemoryUsedon a null command buffer, fault 0x70). Consistent with two threads touching one command buffer's recording list (one between the two reallocs). The recording lists are now updated under one lock (g_recs_mu: EndRecording, drop_recs, Finalize) and every command-buffer entry point tolerates a null/finalized buffer (cbh_get, scratch host object, guarded Begin/End/rec_op/IsRecording). Not proven as the cause: v0 went from about 1 crash in 3 runs to 0 in 9 runs. TextureInitialize: storage out of range(v0, 256x256x6 fmt 0x3f target 9, 9 437 184 bytes vs a 2 117 632-byte pool).CUBEMAP_ARRAYlayout multiplieddepthby 6 although the game passes 6 = layer-faces (one cube). Layout (tex/napple_layout.c) and the Metal texture creation (mtl_tex.m, one line) now treat depth as the number of layer-faces. The error no longer appears.
Result (ACNH 3.0.3, 45 s headless)¶
frames ~1150-1200, draws ~24 000, exit 0, 0 CRASH. v0: frames ~1200, draws ~165 000, exit 0, 0 CRASH in 9 runs. LAS 2290 frames, SMO 2232 frames, exit 0.
Update 2026-10-26: AC 3.0.3 now gets through its boot sequence (more root causes, with hints from nxnative)¶
After the HID fix the game still sat on a black loading screen with no file reads after the first 2 s. The nxnative project (/Users/neto/Documents/Napple/nxnative, a sibling that runs AC to the island) pointed to the causes; all verified here (scripted headless runs):
5. The game's clock read epoch 0 (ToCalendarTime(0) in the IPC log): the SDK's AdjustableUserSystemClock::GetCurrentTime (a MonotonicSystemClock, sdk+0x4421cc) returned 0, the boot event flow System_DateTimeNg (date/time invalid) never ended. services/time.c now overrides the two SDK entry points by symbol (time_register_overrides, called next to movie_register_overrides in main.c) with the host clock, as nxnative's hle_time.c does. After it the boot sequence loads its event flows, mounts save data (OpenSaveDataFileSystem) and goes on.
6. olsc:u (cloud backup) cmd 13 DeleteUserSaveDataInSyncArea failed, the game then ended the process with a Break (result 0x8ae6e). olsc:u now answers its device-side commands with success and zeroed output (backup off); the cloud controllers stay offline. nxnative: same finding (hle_sf.c, README of 01006F8002326000).
7. Conditional rendering (gfx/src/mtl_replay.m): AC wraps its clears in SetRenderEnableConditional; the condition is now evaluated at replay (counters at addr and addr + 16, a timestamp report replayed earlier counts as 0, an occlusion result keeps drawing) and applies to clears as well as draws; timestamp reports zero the counter field at replay so the next frame's condition sees the in-order GPU state. Without it the window textures kept stale content (a white quarter-screen block appeared with culling disabled). Idea and semantics from nxnative's render_cond (docs/reference/rendering.md).
8. Kernel: SignalToAddress type 2 with count <= 0 and waiters writes value-2 (was value-1); WaitForAddress type 3 (64-bit wait if equal). From the nxnative review (K1/K2), semantics of mesosphere's address arbiter.
9. Shader guard: the pool-relative offsets reach 0x10d00, so ProgramSetShaders rejects code or control addresses below 4 GiB (no host pointer is below the page-zero of an arm64 Mac process).
Result (3.0.3, 150 s headless): the game runs the full pipeline every frame (shadow maps, G-buffer, lighting, bloom chain, tonemap, UI), 93 draws a frame, 330 pipelines, and streams the airport-counter / tour-demo sounds; the presented picture is still black. What we know: the window texture gets exactly one draw a frame (a 500x110 text label or the 96x96 plane icon, quad vs 26466b1441e9b1dc / fs 45f6c2cb492eaba9, cull back, front CCW), culled under the host's winding rule. SMO's final window copy is the same quad, drawn visible under the same rule, so the winding convention was left as it is (inverting it for upper-left origin turns SMO black); AC's quad has a -y in its vertex shader, which makes it visually clockwise. Whether that screen is meant to show anything is open. The opening movie file (Movie/OpeningMovie.webm) is not read yet in 90 s.
Update 2026-10-26 (2): the picture. AC 3.0.3 shows its game on the window (scripted, headless screenshots of the presented image)¶
With the boot fixes above the scripted run (A pressed every 3 s from 25 s) goes through the opening: Timmy's dialogue, the Nook Inc. "Deserted Island Getaway Package" counter scene (3D, lit, with the sign and the counter), then the Nook Inc. name-entry keyboard. Evidence: screenshots of the presented image (NAPPLE_SCREENSHOT), the composite target shown by NAPPLE_SHOT_COMPOSITE=1.
- What the op stream shows (NAPPLE_OPLOG_FRAME=<n>: every replayed op of that frame with depth, SET_RT targets, binds and draws): one command buffer per frame; the 3D scene, tone map and UI are all drawn into a 1920x1080 RGB10A2 target (composite); after two conditional clear-only passes on it the window pass sets its render target, vertex state and issues ONE draw with the program, uniform buffer and texture it inherits from the previous draw (a 500x110 text label). No op ever samples the composite (no copy, no DrawTexture, no downsample, no bind). nxnative shows this game's picture, so something on the way from the composite to the window is still unidentified (the sprite draw's inherited state, or a binding we lose); I did not find it in the ops, the texture pools (no slot overwritten, no stale handle, no empty child recording that should not be) or the render condition (NAPPLE_RC_OFF=1 makes no difference there).
- Stopgap in place (gfx/src/mtl_replay.m, mtl_present.m): mtl_frame_present_pick: when a frame's largest render target (at least 80 % of the window area) was drawn into, was never sampled by any draw, and the window texture received at most two draws, the present path shows that target instead of the window texture (warning logged once). NAPPLE_NO_ORPHAN=1 turns it off. LAS and SMO draw to the window with a full-screen draw that samples their composite, so they never trigger it (to be re-verified, see Verified below).
- More about the open root cause (found 2026-10-26): the window pass is built by one guest function (main+0x3802394, called from main+0x37b8f44; it binds the window texture with a view of format 0x38 when a global flag is set, then draws one quad); the composite target is registered in texture pool slot 1634 (fmt 0x3d, RT flag) and the G-buffer targets in slots 1828-1831, but no BindTexture op of the frame names any of those slots (the 232 binds of a frame are all small UI/model textures), so the scene textures are not reached through BindTexture(s): bindless handles read by the shaders from constant buffers or another path we do not record are the next thing to check (NAPPLE_REGTRACE=1 lists the registrations of >= 1280-wide textures, NAPPLE_WINTRACE=1 the guest stack where the window texture is bound, NAPPLE_EMPTYTRACE=1 the empty recordings).
- Winding: the quad of the window draw is culled by the host's rule; the rule was left unchanged (SMO's window copy is the same quad and needs it).
- Other fixes of this round: prepo:u 10104/10105 (SDK 20 play reports aborted the game after the title), a use-after-free in mtl_addr (the pool table snapshot is now copied under the mutex), VMNMX in the shader translator (AC v0 shader 23e43e94e45bb1d5, word 3b34056060470100 = VMNMX.U32.U32.MX.MAX R0, R1, R4, R10, after Ryujinx's InstEmitVideoMinMax; golden check in t_xlate).
- Debug aids: NAPPLE_OPLOG_FRAME, NAPPLE_SHOT_COMPOSITE, NAPPLE_RCLOG, NAPPLE_CLEARLOG, NAPPLE_UBOLOG, NAPPLE_TEXLOG; window textures are logged at creation.
Update 2026-10-26 (3): MK8D graphics, command-list lifetime (final state of this session)¶
- MK8D 46 -> 60 fps (headless, verified:
NAPPLE_GFX_PROF=1shows 60.0 presents/s, GPU 5 ms a frame): the headless pacing slept inside Metal's completion handler (which runs every command buffer's completion on one thread), so the next frame's draw completion and fences waited behind the sleep; the wait is now a timer (mtl_present.m, dispatch_after on a serial queue; a late frame is not followed by a burst). Headful pacing was not changed. - Stale command handles (MK8D 65 000+ a minute, SMO exploded geometry per the nx-shader note): root cause confirmed in
api_cmd.c: the graveyard released recordings by idle time (30 s) although games call old lists again minutes later. Now nothing is released by idle time: finalized lists are kept until the recordings' memory exceedsNAPPLE_REC_BUDGET_MB(default 1024), then the least recently used of the 1024 oldest are released (safety cap 2M lists). Memory is tracked in bytes (g_rec_bytes). Verified: MK8D still logs stale handles after the budget is reached (it records about 4 000 lists a second and never finalizes them, 625 000 live lists = 1 GB in 160 s), so the real remedy for MK8D is to store lists compactly or to stop keeping lists that cannot be called again (a list recorded into reused command memory); not done. Not verified: the SMO repro (SMO data was deleted for disk); no host regression test for the budget was written. - Still open for MK8D:
TextureInitialize storage out of rangedid not appear in my 160 s runs; block-compressed render target view (format 0x44, not copied back) andvertex stream ... not inside a memory poolstill warn once. - MK8D data for this work:
napple build ... --data-only --update-romfs in-placefrom origin/claude/mk8d-64-2026-10-25-titles (268 MB), built in a scratch worktree with that branch merged locally (never pushed).
Earlier: the presented image is black¶
The game renders into an offscreen 1920x1080 target (fmt 90): its render-target dump at frame 400 is the beige dotted Animal Crossing background (verified, NAPPLE_DRAWLOG=400 NAPPLE_DRAWLOG_RT=<dir>), but the final pass into the window texture (fmt 71, 1920x1080, upper-left origin) comes out black, so every screenshot is black.
- That last pass is one indexed quad (count 6) with cull = back, front = CCW, no blend, same vs/fs hashes as the other quads (vs 26466b1441e9b1dc, fs 45f6c2cb492eaba9), UBO with a projection (1.545 / 2.747), one SSBO, viewport/scissor 1920x1080.
- Disabling culling, or flipping the winding (setFrontFacingWinding in mtl_replay.m apply_raster_state), makes it non-black but wrong (a cream rectangle in the top-left quarter plus small shapes), so there are at least two problems: winding for upper-left origin (SMO is upper-left too but culls almost nothing at its title) and what the shader/SSBO/UBO path then outputs. Both are in gfx/src/mtl_replay.m / gfx/src/shader/, outside this session's paths.
- One unknown Maxwell instruction in v0: shader 23e43e94e45bb1d5 (stage 1) unknown instruction 3b34056060470100 at 0x3e8 (translator territory).
Debug aids added¶
runtime/src/threads.c: SIGUSR1 thread dump now also logs the live guest PC and a 24-frame guest call chain of every running thread;NAPPLE_PCPROF=1samples running guest threads every ms and logs the hottest PCs every 5 s;NAPPLE_PCPROF_REGS=1also prints x0..x22 of thread 0x51.runtime/src/services/fs.c:NAPPLE_FS_TRACE=alllogs every IStorage::Read; out-of-range/rejected reads are always logged.gfx/src/api_device.c:NAPPLE_GFX_CMDTRACE=1also logs QueueSubmitCommands.gfx/src/api_memory.c: warnings for GetBufferAddress on a pool/buffer without storage.gfx/src/api_texture.c: the storage-out-of-range error names pool state, pool size and the texture dimensions.
Files¶
Napple/input/src/hid_shm.c, Napple/input/tests/test_hid.c, Napple/runtime/src/threads.c, Napple/runtime/src/services/fs.c, Napple/gfx/src/api_{cmd,device,memory,program,texture}.c, Napple/gfx/src/tex/napple_layout.c, Napple/gfx/src/mtl_tex.m (one line).