Repository navigation
Release 0.13.0: performance release - #37
Merged
Merged
Conversation
…leaves the files a shader build writes alone After a full Ocarina of Time build, mds_stores stayed at 100-156% CPU for more than 40 minutes with nothing else running, and mdfind found the build folder's .metal sources. Spotlight indexes every file under the home folder and reads the text of the .metal sources. The shader step writes a file per shader variant into shader/cache/b-<chain>/ before it packs them into one: 195,157 for Ocarina of Time, 42,850 of them .metal. That build was made before the libraries and the compile cache became one file each, so its folder kept them, with 311,903 more files in the cache. On macOS 27 only a folder whose name ends in .noindex keeps Spotlight out; a .metadata_never_index file inside does not (lunks, PR seedds#1355, which names the temporary folder so). The shader folder is SHADER = 'shader.noindex'. A shader/ from before moves there before any step reads it. A rename keeps every file and its times, and no step's key holds the folder's path, so the split, the libraries and the compile cache are reused and nothing is translated again. The first build after the move packs the Mac app again, as pack_app.py's key names its inputs' paths. When both names exist (an older checkout made shader/ again), shader/ stays and a [NOTE] line names it. bench.py, regress.py and bug_report.py read either name. Measured on a MacBook Air M4, macOS 27.2, with APFS clones of that build folder's subfolders in a folder Spotlight indexes, as the CPU of mds, mds_stores and mdworker_shared above idle until it settled: - a sixteenth of b-<chain>/ (11,973 files): 152 s, at up to 772%; about 2,500 s for the whole folder a shader build writes. Deleting them: under 1 s. - a sixteenth of the old compile cache (19,839 files): 210 s; about 3,300 s for the whole. - pso/ (1,162 files) 33 s; patched_*/, profile/, tools/, font/ and app/ (45 files) 4 s; logs/ (15 files, one of 277 MB) 3 s; extract/ (a 26 GB RomFS.img) 2 s. So the shader folder is where the indexing is, and the other folders keep their names: logs/ and profile/ are also paths front ends read. A game benchmark ran in another session during most of this. Checked: CI's tool-test loop passes but for test_tree_tar.py's test_packs_a_quiet_folder_and_keeps_its_attributes, which fails the same on main on this Mac; lint_python.py has no new finding. The shader package and keys of that build, cloned: pkg.outputs.key matches split_output_key before and after the move. No build was run with this change. Front-ends: interface: the build folder's shader folder is shader.noindex/ instead of shader/ (cache/, ios/, vk/, gl/, pkg/ and the keys inside it, and the Mac run command's DM_MSL_DIR); a shader/ from before moves there at the next build. _Drafted with Claude; reviewed by me._
… deadlocks against the zstd command's pipes Feeding the zstd CLI through stdin while capturing stdout deadlocks on macOS once a frame is larger than the pipe buffers: the worker's blocking write waits for zstd to read, while zstd's output writer waits for the worker to drain stdout. Ocarina of Time's sound archives hit it at 88% of the split (164,256 of 186,587 files), and the build hung for good. compression.zstd (Python 3.14, backports.zstd before) is already used for dictionary frames; it decompresses frames without one too. test_split_zstd.py covers files and bounded RomFS entries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
wake() signalled the woken thread's condition while the waker held G, so the woken thread woke into G still held and slept again on it (_pthread_mutex_firstfit_lock_slow under its cond_wait: 5-7% of Breath of the Wild's main thread on the Mac). wake() now lists the thread (up to 8 per waker) and g_unlock signals it after the unlock; a thread that waits on a condition with G signals its list first, and thr_free waits out a signal in flight. BotW, Mac, three A/B pairs: lock slow path 51-70 -> 18 samples, guest threads 503-512 -> 494-498 ms/s of CPU (main thread 112-114 -> 108-110), CPU energy 1.21 -> 1.18 W. DM_WAKE_UNLOCKED=0 restores the old order. Discord: https://discord.com/channels/1491224015434678394/1524915513606803549
tex_mtl_'s stale sRGB / linear view check asked conformsToProtocol:MTLTexture of every serial-table object; it walks the class's protocol lists. obj_is_tex keeps the answer for the last 8 classes (the serial table holds NvPkg and a few texture classes). Also mtl_tex_gpu_owned, the GPU-owned test and tex_upgrade's holder check.
With no segment on the GPU and DM_NVN_PASS_SPLIT draws (256) encoded since the last cut, the open pass is ended (attachments stored by rp_end as at every other mid-pass end), the segment committed, and the next draw opens the pass again with loads; at most DM_NVN_PASS_SPLIT_MAX (2) splits a pass. A pass reopened by a split does not run the dispatch-hoist scan again.
…epare after one look; the tile plan looks textures up only for attachments fb_prepare and fb_tile_plan looked up every fragment texture binding of every draw (a lock and a texture each, with a retain and release in the plan). A first pass over the bindings' serials under one lock returns for draws that sample none of the pass's attachments, and the plan takes a texture only for a binding that names one. Mac, BotW, snapshots on: about 12,020 -> 11,930 instructions a draw (11,030 with DM_NVN_FB_SNAP=0).
…e those slots 72% of the argument tables BotW writes follow only a uniform buffer rebind on that stage. A per-stage generation of every other argument change (g_args_ogen) lets such a table be copied from the shader's last one in the pass, with only its uniform buffer slots made again (not for shaders that read handles or pointers out of a uniform buffer, cb_only). The geometry and mesh paths that force a stage's table again move the generation too. DM_NVN_CB_CHECK=1 writes the full table as well and logs a difference; DM_NVN_CB_FAST=0 turns the path off. Measured on the Xipzer fork (BotW, Mac, encode thread on): 11,090 -> 10,670 instructions a draw (-4%).
Each returns an object something else already holds: the serial table (tex_put, or the NvPkg in it) for tex_mtl, rp->view for tex_by_id, g_enc for rp_begin_ex. Their returns go through void * and a macro bridges them back, so no retain / autorelease / claim per call. Objects replaced in those tables are released after the GPU is done with them (defer_obj_release), so a returned pointer outlives the replay that asked. Measured on the Xipzer fork (Mac, BotW at 130%, encode thread on): tex_mtl about -2% instructions a draw, tex_by_id and rp_begin_ex 10,565 -> about 10,440.
…na of Time's 2,241 raw-memory shaders take about 2 ms less GPU a frame at the Kokiri Forest spawn point (chain cfcc3a82c78b, PR seedds#1464) Each 32-bit word a shader reads or writes through an address traced to no storage buffer found its pool on its own: the hint of its 2 MiB chunk, and on a miss the 13-step search of the pool table. A shader reads a structure word after word from one pool, so all but the first lookup of a run chose the entry the first one chose. GlobalToStorage.TryGenerateRawStorageOp: each helper keeps the entry it chose last in a private word of the invocation (ResourceManager.AddRawLastEntryMemory, zero at the start through a SPIR-V null initializer, MemoryDefinition.ZeroInit). An address inside that entry's range takes it without the hint or the search; a hint or a search records its choice. Loads, stores and atomics all do it. The table's ranges are disjoint and the host binds one table per draw or dispatch, so the search would choose the same entry; entry 0 is empty, so an invocation's first lookup searches. dmsh_meshstage.py keeps thread variables spirv-cross declares ahead of the stage memory in dm_stage_body (98 mesh and 11 task shaders of Ocarina of Time failed the mesh stage without it). PR seedds#1464 did the same as a text rewrite of spirv-cross's MSL (dmsh_wide.py raw_ranges), loads only, and silent when the generator's output changes shape; this is the same cache where the helper is generated. Ocarina of Time on an M6 Mac, main's host, the 2,241 shaders of this chain laid over main's libraries (the 2,592 other shaders built with them came out with the same MSL; the reflection differs only in its chain field), the scripted walk from save_ref with warmed pipeline archives, two runs a side: GPU span p50 at the spawn point (same view, about 2,500 draws) 16.53 / 16.41 -> 14.11 / 14.57 ms, over the walk 15.7 -> 14.0 ms. Screenshots at the spawn point and red lines match. Open: frame time p95 over the walk 19 -> 26 ms (runs ended in different places, the game's dynamic resolution took the headroom, 8 GB of swap in use); the PR's register-held range not compared on this Mac. Check: dm/gfx/tests/test_dmsh_raw_last.py (new: the helper's shape, and its reads on the GPU; a helper without the cached entry's end check fails 768 of 1,024), test_dmsh_meshstage.py, the translator tests (DMSH_TEST_XLATE: ampere_alu, ampere_mem, control_flow, maxwell_ftz, geometry, image_atomic_bounds, txq_type), test_nsp_chain.py, test_xlate_anycpu.py, test_docs.py. Regression: Ocarina of Time only, as the A/B above (regress.py would translate its 42,853 shaders for both chains); Donkey Kong Bananza, the second game chosen (raw stores and the write log), was not run: its drive was unmounted during the session. Co-authored-by: Nimesh Ambepitiya <10327965+TheekshanaA@users.noreply.github.com>
…shaders: Ocarina of Time's 222 such pools, 334 MB, which the GPU never reads keys_hash (hle_nvn.c) checks that a shader's code lies in a memory pool before it hashes it, and asked mtl_addr, which also puts the pool into the residency set. The shaders run translated, so the GPU never reads that code. mtl_pool_holds is the same pool lookup without the residency; a pool the GPU reads through a binding or an address is still made resident there. t_pool_holds checks the lookup and that asking makes no pool resident. The reference entry is in docs/reference/memory.md, with its line in the symptom index and Ocarina of Time's note.
…d of a compare-and-swap loop A task or mesh shader writes its primitive indices and count as bytes, which the translator's shared memory byte store turned into a compare-and-swap loop on the word. The 32 lanes of a workgroup write neighbouring bytes, so four lanes contend for each word and the loop ran about four times a store, with a divergent retry each time. Ocarina of Time's foliage mesh shaders have ten such stores a lane over tens of thousands of workgroups a frame: 8.3-8.6 ms of GPU a frame on an M3 Pro, a third of the frame, whatever the resolution. GenerateSharedStore now clears the field with an atomic AND and sets it with an atomic OR. Lanes writing other bytes of the word commute with both, so the word ends as the loop left it, without retries. Two lanes writing the same byte at once get a mix of their values where the loop gave one of the two; the hardware's order between them is undefined as well. The helper also serves 16-bit stores and the shared memory of compute shaders. A chain change (7b8c971669dc): translator rebuilt, lock and header updated, the StageMemory test checks the new form. DM_NVN_SKIP_MESH=1 drops every task / mesh draw, as DM_NVN_SKIP_FS drops draws by fragment shader: how the cost was isolated. Measured on an M3 Pro (18 GB), Kokiri Forest, the pass holding the foliage: 8.3-8.6 ms -> 3.5-4.0 ms a frame; the game's resolution controller then holds 1440x810 at 48 fps where it held 854x480 at 36-40 fps.
…ng walked whole: registrations, memory pools added or removed, retired IDs at a new submission and storage takeovers each leave a note pool_gpu walked a table of handles traced to no slot whole at any change of g_reg_gen or g_ser_gen, though each change reaches only a few of its IDs. On main (6121fd7), TheekshanaA measured about 53 s of a seven-minute walk through Ocarina of Time's Kokiri Forest spent on 61,000 such builds of its 33,025-entry texture table (PR seedds#1405, seedds#1464). The game registers something between most of its draws, its streamed textures map and unmap virtual pool runs (vrun_set) dozens of times a second, finalized descriptors stay registered, and textures take over each other's storage (alias_take_over, about 8,400 times in six minutes), which bumped g_ser_gen with no note and so also sent every texture ID down the slow path. This ports TheekshanaA's branch oot-pool-table-notes (9579768, 9d486f8) to main's walk. Those commits sat on the table patching of seedds#1299 / seedds#1387, which main did not take, so the patching comes with them: - A registration records its generation and ID (g_reg_log), a storage change its serial (g_ser_log, beside the notes ser_sync takes). A table built at an earlier generation takes again only the IDs they name, by the walk's own rules (pool_entry), and is kept when no entry changes, else copied and patched. - A memory pool added or removed records REG_POOL and no ID: a table takes again only the IDs it found without storage (pool_snap.dead), and treg_gone notes each ID of a finalized pool. A note's generation is stored last (release) and read first (acquire), as pool changes come from threads that do not hold g_rmu. - A table keeps the IDs whose entries are views of retired registrations (pool_snap.trans and .sub); the next submission takes only those again. - A takeover records a note that names no texture (ser_note(0)). - Past POOL_IDS_MAX IDs on a list, a note the ring has overwritten, a bump without a note, a pool that grew or a sparse mapping, the table is built whole as before. The log line says why tables were built whole and how many were patched or kept; the power lines gain "[nvn] pool tables up to frame ...". What main's walk changes on the way: - A patch makes textures only within what the frame may still make (pool_room, taken out of pool_walk_limit), as a walk does. A texture left out counts in the table's left, and the walk goes on next frame as before. - Patched tables take and give their buffers through pool_table_take / pool_table_give, as walked ones do. - A retired registration's view that the read scan writes in place (pool_seen_scan) is listed as transient too, and a table holding such views from an earlier submission does not stand under POOL_SUB_MS. Before, nothing made a table take those IDs again after the submission whose end drops the views. - alias_sweep, and seedds#1387's exception that kept a retired view of a shared texture past its submission, are not needed: main drops every retired view at the submission's end, so the next submission takes those IDs again. - A sampler table follows registrations alone. Checked: t_pool_notes (new, 67 checks). A pool elsewhere keeps the table, the pool that brings an ID's storage patches it, a finalized pool's ID holds the dummy, a bump without a note builds it whole, a retired ID and a takeover are taken again at the next submission without a build, a registration past the budget is left out by a patch, and a read written in place is taken again; patched tables match tables built whole. Without the takeover note, the retake at a new submission or the in-place record, it fails (2, 4 and 2 checks). make -C dm/gfx/tests GAME=0400486024DEC000 for the 35 tests that reach registrations, pool tables, serial notes and the sharing table (t_pool_chunks, t_feedback, t_sparse_tiles, t_image_alias and t_nvn among them): all pass. The Mac host builds, and the two changed files compile for the iPhone SDK. clang-tidy (LLVM 22, not CI's 23) finds nothing new in them. Ocarina of Time, four scripted runs on an M4 with 24 GB, alternating main (88c7e03) and this change, from the autosave at Link's house (180 s in the house, Saria's greeting, 300 s on the balcony): the table was built whole 44,000 and 24,000 times per run on main (67 s and 72 s), about 100 times with this change (about 0.2 s), while about 86,000 tables were patched and 63,000 kept. The submit thread's CPU time per frame (p50) went from 12-15 ms to 3-5 ms; frame p50 is the GPU's and did not move, p99 in the house went from 34.6 and 41.0 ms to 24.2 and 25.6 ms, and frames over 50 ms walking from 23 and 48 to 3 and 2. Same picture, footprint and red lines. Co-authored-by: TheekshanaA <10327965+TheekshanaA@users.noreply.github.com>
…logue and first control at 30 fps with no red line (profile 0100276009872000, note, games table row, gallery)
…e title music for minutes: pso_manifest.tsv seeds the prepare step On a Mac that has no pso_user.tsv yet and a cold Metal shader cache, the first title frame built 11 pipelines and loaded 12 shader libraries inside one submission: 252.6 s on an M3 Pro (macOS 27.2) with MTLCompilerService at 100 %, while the window kept presenting the last frame, black, and the title music played (issue seedds#1477; the same symptom reported on an M4 Max). The profile's seed list was header-only, so the prepare step had nothing to build before the first frame. The seed list is this Mac's pso_user.tsv, recorded on v131072 (chain adec7d81d970) with no lists present, so everything was built on the spot: 9,620 lines (8,376 shaders, 1,112 render pipelines, 132 compute pipelines) over boot, the title and its movie, a new game and the opening, Link's house, Kokiri Forest and a save load. With those built behind the Preparing-shaders screen, or in the archive, the same start reaches the title in about 20 s. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ture: the comment says what holds that, and t_pool_notes checks a live sRGB / linear twin alias_take_over skips an entry a live serial holds (serial_holds on g_by_serial) and only unrefs one a finalized serial holds through g_ghost, so no serial resolves differently. The comment said only a retired ID could still hold a view of an evicted texture; a live sRGB / linear twin can too, as its serial holds a view of the shared texture rather than the texture. tex_mtl_ finds such a view stale once its parent has left the sharing table and drops it (serial_del), which notes that serial, so the patch takes the twin's ID again. t_pool_notes gains that case (81 checks, 0 failed): ID 8 is the sRGB twin of a texture whose ID was finalized; a texture of another description takes the storage over and evicts the shared texture; in that submission the twin's view is dropped and made again, no table is built whole, no ID goes down the slow path, and the patched table matches a table built whole.
…rina of Time no longer aborts when a save loads with DM_NVN_ENC_THREAD=1 (the iOS default) (PR seedds#1458) nvn_geometry.inc encodes an indirect task / mesh draw with drawMeshThreadgroupsWithIndirectBuffer:threadsPerObjectThreadgroup: threadsPerMeshThreadgroup:. With the encode thread on, the encoder is the recorder DmRecEnc, which had a stand-in only for the direct mesh draw, so forwardingTargetForSelector: stopped the host (SIGABRT under mtk_encode) at Ocarina of Time's first indirect one: on an iPad, and on the Mac with the encode thread on (issue seedds#1265). RO_R_MESH_IND records the argument buffer's GPU address and the threads per object and mesh threadgroup, as RO_R_MESH does for the direct draw, and replays the indirect call. t_rec_draws (CPU only, now in the CI list) checks that the recorder has a stand-in for each of the seven draws the NVN layer encodes; it fails on main and passes with this change. Co-authored-by: Adrian Smith <242086076+aksmith-git@users.noreply.github.com>
…s, and A loads where its LDSM read it: Ocarina of Time's title movie network takes 11.4 ms of compute a frame instead of 14.8 on an M6 (26.7 to 18.9 on an M4; chain 23a1ea1db092, PR seedds#1456) Each dmsh_hmma16816_f16 call took A, B and C through the SIMD-group's scratch block and D back through it, three barriers a call, although most of these HMMAs continue an accumulation nothing else reads, share A with the call before, and take A straight from an LDSM. TensorPass.Prepare now gives each HMMA flags (an accumulator slot for a chain, A kept, C = 0, A loaded where its LDSM read it), each a promise about values the translator's own body computes anyway; dmsh_build.py's hmma_msl keeps the slots and A's matrices in each calling function's registers and loads A from the caller's shared memory. An LDSM in a compute shader is a MatrixLoad a matrix until then. The divergence analysis that decides whether an LDSM's row address is the same in every lane plus a known per-lane part also treats a loop header's phis as divergent when the loop has several back edges and any branch in it depends on the lane: lanes that took different back edges bring the header different values. It changes none of Ocarina of Time's 71 HMMA shaders. Checked: test_dmsh_hmma.py (GPU against a CPU reference, and the translator's flags), the graphics tool tests; Ocarina of Time's 42,854 shaders build as on main (7 + 2 known failures), title A/B on an M6 (compute 14.78 to 11.41 ms, GPU span 15.63 to 11.87), title and opening movie clean on both hosts; Donkey Kong Bananza's 23,638 shaders build, its opening, cutscenes and first punch render cleanly with no red line (its NDSR upscaler takes the chain flags). Not run: gameplay A/Bs, the iPhone. Co-authored-by: yangyidong <63278156+sheepyd@users.noreply.github.com>
Keep bound color and depth descriptions in queue-owned storage before a submission can publish completion. Reusing completed command memory no longer overwrites the texture snapshots retained by queue state. Add NVN/Metal pixel regressions for inherited bindings, multiple queues, nested recordings, views, depth and the last color attachment. Document the Rhythm Heaven Groove kaeru crash and the verified fixed Mac app. AI-assisted.
…ed texture in one lookup instead of walking the serial table: Ocarina of Time's long frames in Kokiri Forest fall from 332 to 212 (PR seedds#1439) For each sharing-table entry a takeover could remove, alias_take_over called serial_holds(&g_by_serial, ...), a walk over the serial table's whole capacity. With thousands of serials and shared textures, every new shared texture walked the table once per overlapping entry, and in Ocarina of Time, where the texture pool's walks make hundreds of textures in a submission, that walk was the submit thread's hottest function while frames took up to a second. A count per object of the serial-table entries holding it (g_by_serial_held) is now kept by every writer of the serial table (serial_put, serial_del, tex_put and the upgrade's in-place replacement), and alias_take_over asks it in one lookup. An entry holds its object, so a counted address is not reused while counted. The reference entry notes that a change letting objects hold shared textures another way (PR seedds#1404's views and packaged members) must count those holds too. t_alias_takeover: live-held entries stay, unheld ones go after ALIAS_IDLE submissions, two serials keep an object held until both go, and a takeover past 1,000 held entries with 20,000 serials takes 0.03 ms instead of 4.3 ms. Ocarina of Time on an M4 Pro, loading a Kokiri Forest save through Saria's dialogue and a walk: a host asking both the count and the walk compared them 393,216 times with no difference; [longframe] lines 332 -> 212, frame p99 72.8 -> 38.8 ms (one run each, hosts on the build's chain at 88c7e03). Measured by the author on an M5 Max and by a commenter on an M3 Max (frames over 100 ms 85 -> 2). Regress: Breath of the Wild's build is in the packed layout regress.py does not read, so Ocarina ran instead. Co-authored-by: enjoyerofslop <281774948+enjoyerofslop@users.noreply.github.com>
…assification goes: a pool walk in between handed out an NvPkg as the texture, and Ocarina of Time stopped with "-[NvPkg buffer]: unrecognized selector" in tex_by_id (PR seedds#1420) nvnMemoryPoolFinalize (a game thread) ran mtl_alias_drop_pool, which dropped the pool's textures from the CPU-written set (cpu_tex_drop_pool) before it marked the pool's texture IDs gone under g_rmu (treg_drop_pool_views). A pool walk on the submit thread (pool_gpu -> pool_entry -> tex_by_id) holds g_rmu; an ID of that pool reached in between was not gone, the pool's magic was still set and the texture no longer CPU-written, so tex_mtl_ skipped the packaged path and returned the texture table's entry as it was: the NvPkg wrapper, not an MTLTexture. tex_by_id's base.buffer threw. Reported on PR seedds#1299 (M3 Max, 412 s into Kokiri Forest, right after a save) and on PR seedds#1420 (M4 Pro, Deku Tree, about 40 minutes). The IDs are now marked gone first: a walk holding g_rmu finishes while the classification is still there, or finds the ID gone and takes tex_made, which unwraps the NvPkg. PR seedds#1420 found where the NvPkg escaped and unwrapped it in tex_mtl_; that guard is not taken, as the state no longer arises. Checked: t_cpu_tex --metal 68 checks, 0 failed; its new finalize_during_walk (holds g_rmu while another thread finalizes the pool) fails 2 checks without the fix. t_cpu_storage 32, 0 failed; test_docs. Ocarina of Time, the save_ref village walk (about 230 s) with this host and with main's on the same chain and libraries: no crash on either, the same three red lines, p50 15-16 ms on both. The crash itself was not reproduced. Co-authored-by: Pedro Nascimento <91118+lunks@users.noreply.github.com>
…etween a raw shader's encode and its command buffer's commit is no longer missing from the table the GPU reads, where its loads read 0 (PR seedds#1427) An argument block held the address of the raw pool table that was current when it was encoded (gmem_map_gpu). A pool made after that, on another thread while the submission was encoded, was not in that table, though the GPU reads the block only after the commit, as NVN's GPU reads memory when it runs. Raw reads of that pool read 0: the failure that hung agl's task shaders in Ocarina of Time. The table words are noted as they are encoded (gmem_slot_note) and, at each commit of the command buffer, whole or segment (mtl_sub_commit, sub_cut), pointed at a table of the pools as they are then (gmem_commit_patch). A new table is built when the pools changed, not refilled in place, as earlier segments may be reading the old one. A block's write log follows its table (NvGmlLog), so gml_read decodes its bits against the table the shader searched. The memory report counts the words and those that were out of date (raw pool table words at commit). The idea of rewriting the words at commit is from seedds#1407 (@uncraftablexd). On the author's stack, loading Ocarina of Time's outdoor save, 5,729 of 154,107 words held an older table by frame 5,100 and 2,582 held the current one behind the pools. Whether this window is behind issue seedds#1326's page faults is open: an address the table misses is skipped, not faulted, and the pools near the M1 Max faults were made 8-41 s before them. Checked: t_gmem_words 16 checks, 0 failed; t_pool_chunks 102, 0 failed; test_docs, test_gfx_tests_makefile, docs_symptoms --check. Donkey Kong Bananza (raw stores and the write log), hosts at 6d928fd with and without this (the build's libraries are chain cfcc3a82c78b), fresh save into the mining tunnel, 210 s each on an M6: no GPU error on either, the same 4 VP9 red lines, frame p50 16.66 ms on both, 23 long-frame lines each; the patch rewrote 1 of 6,035 words. Co-authored-by: Pedro Nascimento <91118+lunks@users.noreply.github.com>
…level at 30 fps: the layer's frame history lists the presents, so Unreal's first full render flush no longer waits for ever after the logo movie (profile 0100A7C01B792000, note, games table row, gallery) Unreal Engine 5's Switch RHI learns which frames reached the screen only from nn::vi::ListFrameHistory: its Viewport Thread (main+0x142afb0) reads it after each vsync and takes the first shown entry as the latest flip, which completes the frame-end fences that wait for the swap chain (r.GTSyncType 2). The host answered with no entries, so the first FlushRenderingCommands (FFrameEndSync, main+0x15015f0) waited for ever right after the Xbox logo movie: the game thread polled its fence each millisecond and nothing was presented again. hle_vi.c now keeps the last 8 presents (recorded at each NVN present, numbered as GetLatestFrameNumber) and fills FrameInfo as the SDK does (sdk 20.5.15 +0x4510a0): status 2 once the present queue handed a frame on and the system did not drop it, its present tick from the drawable's presented time or the time it was first seen handed on; a null buffer gets the history's size, 8. Octopath Traveler 0 and OCTOPATH TRAVELER II call it from the same RHI code and got no entries before; no regression run covers them (skipped). nx_extract's --html read every base HtmlDocument NCA alone, and the base game's 16 KB one is sparse like its Program NCA: the build stopped as a damaged NSP although every NCA's hash matches. A sparse base HtmlDocument NCA is no longer read alone. Two particle vertex shaders pick a buffer texture at run time from c2's texture and sampler tables only (0x170-0x188, 0x568 / 0x578): tex_dyn_slots on (the title's campfire flame was black). Tested on an M4 Pro: logo movie, title, first-run screens, Play Offline, the opening cutscene and first control in Escape from Squid Coast, walking and attacking, 30 fps, no [RED] line, 2.6 GB resident. New host test t_vi_history; host, NSP, profile and docs suites pass. Regression check skipped at the user's request.
…inished shaders wait in cas.db and the image is written from there, so Octopath Traveler 0's build holds at most 328 files while it runs and about 65 when stopped, not 68,230 (chain c8849b34b7b0) dmsh_build.py wrote each shader's outputs into b-<chain>/ and packed them into the image only at the end, restored every cache hit into that folder before translating anything, and had the translator write every missed shader's working files into one temporary folder that went only on a normal exit, so a stopped build kept them and every later build made another beside it. Now a finished shader's files go into the compile cache under stage/<output folder>/ and leave the disk (commit); reads of another shader's outputs (vs_pad's paired pixel MSL, the mesh step) take them from there (output); at the end publish_library writes the image from the staged outputs (with a partial build's image under it, less the keys it built) through shader_lib.write, which now takes a function per file, and the folder and the stage go. When the image cannot be written the staged outputs go back into the folder. Cache hits are restored when their shader is finished. Shaders are translated and finished in batches of 4 x J, the next batch translated while this one is finished, and a shader's working files go as soon as it is done. Each working folder holds its build's process ID; a later build removes one whose process is gone (sweep_work). The NSP script's progress comes from a file the build rewrites (DMSH_PROGRESS) instead of counting its files; the NSP script, regress.py and bug_report.py read the image where they read the folder; shader_cache.Store gains delete_prefix. The outputs are unchanged: Minecraft Dungeons II's 61,655 library files and Octopath Traveler 0's 41,880 are byte for byte those of chain cfcc3a82c78b (reflection without its chain field), apart from _b_cas.txt. Measured on an M4 Pro, every shader translated again: Octopath Traveler 0, at most 328 files under shader/ and tmp/ (12 outside a build), the step 2:38; stopped with SIGKILL, 40 files in b-<chain>/ and 7 in tmp/, which the next run cleared before writing the same library in 59 s. A partial build for the Mac and the iPhone leaves one image each. Shader, NSP, regress and docs tests and the Python lint pass; no game run (the host is unchanged by this commit).
… uploads fit their 256 bytes of command memory again, so opening Adventure no longer jumps to 0 (PR seedds#1450) SetCopyRowStride / SetCopyImageStride were recorded as 24-byte OP_COPY_STRIDE commands. In NVN they are command buffer state that a copy reads when it is recorded (GetCopyRowStride reads them back) and take no command memory. Super Smash Bros. Ultimate 13.0.5 records each texture upload from a stack command buffer with 0x100 bytes of command memory and no out-of-memory handler (main+0x3664960, main+0x3794430): the two strides plus the copy (200 bytes and 16 kept for a JUMP) came to 264 bytes, the host called nn::gfx's memory callback (main+0x31be0), and it called the unset handler at +0xd0 of its CommandBuffer: pc=0, lr=main+0x31c14. - cmd_t keeps copy_row / copy_image; Set/GetCopy*Stride set and read them without a record. - A buffer <-> texture copy carries the strides after its texture snapshot (p_copy_strides, rec_copy) only when one is set, so copies with tight strides keep their size. - The replay reads them from the record (copy_strides); OP_COPY_STRIDE is still replayed from older captures. The capture layout gains a line: older replayers refuse new captures, this one reads old ones. - t_nvn 5c records Smash's sequence in 256 bytes with a callback that counts any call; t_capture checks the layout before the tail stays readable. Confirmed on the iPhone: Adventure opens and plays. Ocarina of Time A/B on the Mac (village walk, the only game with libraries for the current chain): the same copies with the same strides, no crash, no new red line. Co-authored-by: Ray McDonald <54215218+Raymon82777@users.noreply.github.com>
…s 259 nested brackets, and the BAHS split no longer reads a control section's words as a program entry; a decoder's busy answer is no red line (chain 844bf1a5ea8f, PRs seedds#1414, seedds#1357) The build left 9 of 42,854 shaders out. Six foliage compute shaders used ATOMG.CAS (0x3a9), which the translator refused (PR seedds#1414: AAtomgCas; also ATOMG / RED INC and DEC as a compare-and-swap loop, and desc[URb][Ra.64], where URb is a memory descriptor and not part of the address; no Ocarina instruction uses that form or INC / DEC). Two agl_technique_pfx compute shaders nest 259 brackets past Metal's 256 (PR seedds#1357: -fbracket-depth=1024 in every library compile, which changes no output within the old limit). The ninth, 68bbe98b3bfd9876 ("Ampere instruction 481 at 0x0"), was no shader. bahs_shaders scans the table area 4 bytes at a time, and an NVN2 control section's +0x38..+0x47 (instruction length, c1 start and end, code length: L, 0, L, L) reads as an entry whose control is the next program's and whose code is whatever lies at L: here a compute control with a vertex shader's code, whose SPH reached the translator as an instruction. An entry that lies inside a control section another entry names, measured by that section's own length, is dropped. Ocarina's map loses six such rows (agl and terrain files), none ever loaded, and tera_common gives 175 variation pairs once its lowest false entry no longer hides its binary array. Splatoon Raiders' map has the same three agl keys; it, Mario Kart World and Donkey Kong Bananza need their tables split again, which their notes say. nn::codec's refusal of a packet with 12 pictures waiting returns 3603, which NintendoSDK_movie expects: detail::SubmitDecoderInputBuffer turns exactly that result into its retry code 0x7a and VideoDecoder::DecodeAsync hands it to the player (Ocarina's subsdk0 +0x10fd20, +0x103840). It is logged plainly now (Ocarina AV1, Kirby Air Riders VP9). Checked: the 9 shaders rebuilt on chain 28ecc05ed4ac, before the rebase onto 9c2fb0c (8 ok, the ninth gone from the map); test_dmsh_ampere_alu.py, test_shader_pch.py, test_nsp_chain.py, the new test_split_bahs_entries.py (the false entry is taken on the previous splitter), test_docs.py; the re-split's tables against the profile's (only the six rows and the renumbering differ); the Mac host builds. Not run: the full shader build on this chain (stopped at 12,534 of 42,848), a game run, regress.py. Co-authored-by: Copak731 <224629733+Copak731@users.noreply.github.com> Co-authored-by: Pedro Nascimento <91118+lunks@users.noreply.github.com>
…stead of compiling them at the first dispatch A compute shader's pipeline was made with newComputePipelineStateWithFunction at its first dispatch, on the submit thread: no stand-in exists for a dispatch, as the hybrid pipelines are for draws, and the pipeline archives held render bases only, so the Metal shader cache was all that stood between a dispatch and a cold compile (20-40 ms warm, 100-1,200 ms cold, a title movie network kernel 58-83 s). Ocarina of Time's effects bring about 20 new compute shaders with each area: four frames of 2.2-4.4 s in a 25-minute session, and a cold-cache route to Kokiri Forest spent 195 s of its submit thread in pipelines, 155 s of it in the title's twelve kernels. cps_make takes a compute pipeline from the archives first (the bundled one, then this Mac's files; about 0.3 ms), else compiles it through the flush period's compiler so the flush writes it into the archive, else the device as before; the prewarm's C items, the dispatch and the prefetch go through it. pso_offline.py puts every compute shader of the shader map into the bundled archive (--compute, the profile's map with --game; metal-tt's compute_pipeline_descriptors), so a packed app never compiles a compute pipeline at play: 4,117 pipelines, 0.5 GB, 8 minutes on an M3 Pro. mtl_prog_prefetch also builds a compute pipeline when the game makes the program, through the same path, and a dispatch that comes first takes a queued build over instead of waiting behind the hundreds a scene load queues (DM_NVN_PREFETCH_CPS=0 turns that part off). Render pipeline events over 100 ms on the submit thread are named in the log.
Launch Mac metal and metallib by absolute path, share one pixel-shader body across the thin entry points, and cut the readable-tile library from that same compile. bf_fn looks up main0_f and keeps the per-shader specialization list. The bracket-depth flag stays on the metal command and out of the compile record. Chain 711dca74fb82. The posix_spawn capture files are read only after they exist, and fetch metadata is stored only when fold returns a dict, so the Python lint ratchet stays at its baseline. Cold own_compilers at J=8 on an Apple M2: Mario Kart 8 Deluxe 22:46 to 14:47, Super Mario Odyssey 30:02 to 22:23.
…two nil archives guarded in t_pso_stall, baseline 237 -> 220 - nvn_mtl.m: NOLINT with the reason on eight analyzer findings in landed code: a shift by __builtin_ctz of a nonzero word, indices bounded by the line before (addressed ranges, vertex streams, g_mrb, g_mrbuf), and two objects held by hm_put_id maps - t_depth16_clear, t_dlss_mfx, t_ndve_movie, t_pool_heap, t_upscaler_mfx: g_tex_live keeps the address a texture was initialized at; mtl_tex_release reads it through vm_read_overwrite and checks magic and serial, so a helper returning its tex_t by value is safe (NOLINT StackAddressEscape) - t_feedback: the bf_key offsets are int8_t on purpose (-1 = none) - t_pso_stall: an archive that does not open is left out instead of added as nil - lint_c_baseline.tsv: tidy --update (11 file/check pairs had fewer findings; t_view_swizzle back below main's) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g
…t (host warnings ratchet) mtl_tex_init passed its const tex_t * to nvn_hm_put's void * value, a new -Wincompatible-pointer-types-discards-qualifiers warning from the 0.12.0 upstream merge (upstream stores (void *)1 there). The map only reads the address back (mtl_tex_release, as a const tex_t *), so the cast is safe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g
… xinerqu, CHANGELOG disk-peak note Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qteK3qWJVi2jG3Gz4i1eY
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qteK3qWJVi2jG3Gz4i1eY
…ASan stack-use-after-return in the sanitizer job) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qteK3qWJVi2jG3Gz4i1eY
…mcmp of tex_t (clang-tidy bugprone-suspicious-memory-comparison) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qteK3qWJVi2jG3Gz4i1eY
…clang-tidy nullability) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qteK3qWJVi2jG3Gz4i1eY
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Tom · project thread
Summary
A performance release; full notes are in CHANGELOG.md under 0.13.0.
mainup to04df08d(Raw global memory lookups reuse the range they last chose: about 3 ms less GPU a frame in Ocarina of Time's Kokiri Forest seedds/nxnative#1464 gmem lookups, HMMA title-movie speed-up HMMA chains keep their accumulators and A in the SIMD-group's matrices, and A loads where its LDSM read it: Ocarina of Time's movie networks take 18.9 ms of compute a frame instead of 26.7 on an M4 seedds/nxnative#1456, A storage takeover asks whether a live texture object holds each shared texture in one lookup instead of walking the serial table seedds/nxnative#1439).metal/metallib, one compile per fragment body), perf(pipelines): take compute pipelines from the pipeline archives instead of compiling them at the first dispatch seedds/nxnative#1522 (compute pipelines in the archive), NSP script: the build folder's shader folder is named shader.noindex, so Spotlight leaves the files a shader build writes alone seedds/nxnative#1438 (shader.noindex), Shader split: decompress zstd files in-process, so a worker no longer deadlocks against the zstd command's pipes (Ocarina of Time's split hung at 88 %) seedds/nxnative#1463 (in-process zstd split).DM_NVN_PASS_SPLIT), Retain bound render targets across command submissions seedds/nxnative#1502.vk_shaders.pycloses children's stdin (from xinerqu/ns2ios-shell, MIT, credited in README).TensorPass.LowerLoad), the shared Metal cache,posix_spawn. NXForge's finish scheduler gives way to upstream's batch pipeline.29cd1eb1e851: every NVN game's shaders rebuild once. CHANGELOG notes the higher peak disk use of a first build (about 52 GB for Ocarina).Testing
test_host_mslandtest_uepak_iostore, which also fail on 0.12.0.t_codec_video(MKW fix) passed on the Mac.scripts/release_check.py lintandcheck 0.13.0pass.Generated by Claude Code