Repository navigation
[Proof of concept] Shader precompile from game files: per-stage translation cache checked on every use (see #899) - #900
the-cashman wants to merge 15 commits into
Conversation
… handle sources, per-stage descriptor set, mesh state, relative LEPC) Hooks for the per-stage translation cache and graphics pipeline libraries (docs/shader-precompile.md, added with the video_core part). All default to the old behaviour: - Environment::AdoptAnswers / NoteTextureHandleSource: no-ops unless an environment overrides them (the precompile records which constant buffer words a texture handle came from). - RuntimeInfo::descriptor_set: 0 (the old hard-coded set) unless a library stage asks for 1. - Maxwell::ApplyMeshState: TranslateProgram's mesh-state code as a function (same result), so a cached task/mesh translation can take the live mesh state. - nx2_relative_lepc.h: EDEN_NX2_RELATIVE_LEPC=1 lowers a dead geometry-prologue LEPC address program-relative; off by default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…acy vertex attributes, only with EDEN_NX2_GPL=1 Without EDEN_NX2_GPL=1 none of VK_EXT_graphics_pipeline_library, VK_KHR_pipeline_library or dynamic rendering is enabled (as before). With it, all three when the device has fast linking; VK_EXT_legacy_vertex_attributes in addition on RADV with dynamic vertex input and null descriptors (EDEN_NX2_GPL_ATTRS=0 turns that off). vkCmdBeginRendering/EndRendering in the wrapper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rom game files, graphics pipeline libraries (docs/shader-precompile.md)
New files hold the code; existing files get short hooks marked "nx2 shaderwarm":
- nx2_ir_cache.{h,cpp}: <title>/vulkan_ir.bin, translated stages (IR + the environment answers
to check), tagged with the build and driver. Used whenever the file exists
(EDEN_NX2_IR_CACHE=0: ignored); graphics VS/PS, compute (EDEN_NX2_IR_CACHE_COMPUTE=0 off; a
record with bounded storage write extents only serves its own work group), task/mesh only with
EDEN_NX2_IR_CACHE_TASKMESH=1.
- nx2_precompile_env.{h,cpp}, vk_shaderwarm.{h,cpp}: EDEN_NX2_PRECOMPILE=<pack> translates every
stage of a pack (tools/s2warm) into the cache on low-priority threads, builds each compute
pipeline with a known launch once (driver cache; EDEN_NX2_PRECOMPILE_COMPUTE_BUILD=0 off) and,
with EDEN_NX2_GPL=1, each stage's library. EDEN_NX2_PRECOMPILE_ONLY=1: the boot builds no
pipelines of its own.
- vk_gpl.{h,cpp}, vk_gpl_pipeline.cpp, vk_scheduler_rendering.cpp: EDEN_NX2_GPL=1 links native
VS+PS pipelines from per-stage libraries (made without a render pass, so 3D draws use dynamic
rendering), with an optimized link in the background (EDEN_NX2_GPL_LTO=0 off).
- nx2_pipeline_stats.h: EDEN_NX2_PIPELINE_STATS=1|2 timings and counters; EDEN_NX2_PIPELINE_LOG=1
one line per runtime pipeline; EDEN_NX2_ASYNC_SHADERS=0|1 overrides the setting.
Off (no vulkan_ir.bin, no variables): the old paths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ronment variables, precompile and play) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… inputs and SPIR-V hash (precompile and run time) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er that rejects a cached stage Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ters The check route's EDEN_NX2_IR_MISMATCH_LOG showed most rejected cached stages (34 of 57, including 16 compute programs whose live builds then missed the driver cache for 100-700 ms) failing on 'storage pointer unmapped live, mapped cached': games first use programs while a constant-buffer pointer is still null, and the runtime rebuilds once it is mapped. The precompile now also translates every stage that asked with the pointers unmapped and stores that record too (the lookup takes the record whose answers match). EDEN_NX2_PRECOMPILE_POINTER_VARIANTS=0 turns it off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…OMPILE_POINTER_VARIANTS=1) Measured on the OoT check route: 32,988 extra records, the rejected stages unchanged (their live pointers are a mix of mapped and null, which an all-unmapped record doesn't match). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ant records; lazy record decompression - The precompile makes, for each stage that asked whether constant-buffer storage pointers are mapped, a record per combination (all 2^n up to EDEN_NX2_PRECOMPILE_POINTER_MAX=6 pointers, else each one null and all null). Live, games first use many programs with some pointers still null; the lookup takes the record whose answers match. EDEN_NX2_PRECOMPILE_POINTER_VARIANTS=0 turns it off. - StageCache::Find keeps programs compressed; LoadProgram decompresses only the record used, so a lookup among many variants stays cheap. - A lookup counts and logs one mismatch, and only when no record matched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_LEARN=1) A native vertex, pixel or compute stage that the precompile missed, or whose records don't fit the live environment (a storage pointer null, another texture type), was translated again in every session. With EDEN_NX2_IR_LEARN=1 the live translation is appended to vulkan_ir.bin with the same checks a precompiled record carries: the storage pointer answers, the texture queries with their handle's constant buffer words (logged by GenericEnvironment while armed, plus kind 2: exact pixel format for direct format reads), the viewport transform when read, the texture bound buffer and the local memory size. The next session finds it like any other record. Task/mesh stages aren't stored (their records hold the program before the live mesh state is applied). Records are deduplicated per session by their checks; compression and the write run on the serialization thread. The NX2STATS lines count them (learned N). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…SKMESH=1) Stored under the program's own hash like the precompiled task/mesh records; the live mesh state is put in when the record is used (ApplyMeshState). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ronment) The build has no RTTI, so dynamic_cast doesn't compile. Shader::Environment gets IsLiveEnvironment() (true only for GraphicsEnvironment and ComputeEnvironment); FileEnvironment and the precompile's environment are never stored as learned records. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…only for the one used StageCache::Find read every record of a stage in full, compressed program included, on the GPU thread. With the pointer variants a stage has dozens of records (~10 KB of compressed program each, the checks and answers ~140 bytes), so loading cached stages took 3.7 ms on average and up to 17 ms in play (the remaining 40-75 ms frames when new geometry came into view). Find now reads 2 KB per record (the whole record only when its checks are longer) and keeps where the program is; StageCache::LoadProgram reads and decompresses it for the record whose answers match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ation cache in memory A stage that several pipelines use (27-37 % of the stage loads logged in OoT play and the check route) read its records from vulkan_ir.bin and decompressed its program again for every pipeline, on the GPU thread. StageCache now keeps Find's parsed records (checks and answers, no program) per unique hash for the session (Append drops the hash's entry) and up to 256 MiB of recently decompressed programs, least recently used dropped first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cache is per user; no off-tree references) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Tip 🎯 Customize your risk assessments
✅ Automatic approvals, with your rules
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
What it does
Switch 2 titles stutter the first time a new shader is drawn. The emulator translates the stage on the GPU thread, then the driver compiles the pipeline. This removes both ahead of time, from the user's own copy of the game, with no recorded play:
<shader dir>/<title id>/vulkan_ir.bin,nx2_ir_cache.*). A stage's translation depends only on its own program and its environment answers, not on the pipeline key. So the IR afterTranslateProgramis stored per stage, together with every answer it used.EDEN_NX2_PRECOMPILE=<pack>,RunPrecompileinvk_shaderwarm.cpp). During boot, on low-priority threads, every stage program of a pack is lowered and translated with default answers (nx2_precompile_env.*): 2D float textures, mapped storage pointers, the program's own bank-1 constants. Each stage is then checked: the stored form must give the same SPIR-V. The pack lists program blobs found by a generic scan of the game's files (outside the tree; see "Not included").TryCachedTranslation). A record applies only if every recorded answer is the same live: storage pointers (direct and pointer-table), texture queries asked again through the live handle's constant-buffer words, texture bound buffer, viewport transform, local memory size. Otherwise the stage is translated as today.EDEN_NX2_IR_CACHE_CHECK=1translates every hit live as well and compares the SPIR-V. It logsNX2PC check <hash> (vs|ps|cs|task|mesh): cached translation same as liveorDIFFERS from live, and a difference uses the live result.EDEN_NX2_IR_MISMATCH_LOG=1names the answer that rejected a record.EDEN_NX2_IR_LEARN=1): a stage translated live is appended with the same checks, so it costs one translation per build, not one per session.EDEN_NX2_PRECOMPILE_POINTER_VARIANTS,_POINTER_MAX).EDEN_NX2_GPL=1,vk_gpl*,vk_scheduler_rendering.cpp): graphics pipeline libraries with per-stage descriptor sets and dynamic rendering. The precompile creates each stage's library once so the driver's disk cache holds it, and builds each compute pipeline with a known launch once. This is a separate GPL implementation from vulkan: pipeline libraries + shader warm-up from the game files (first-visit shader stutter) #579's and is here only because the measurements used it. nx2 should keep one (see Shader precompile: a per-stage translation cache filled from the game files, on top of #781's precache (which form do you want?) #899).Everything is off unless an environment variable or an existing
vulkan_ir.binturns it on. Hooks in existing files are markednx2 shaderwarm;docs/shader-precompile.mdlists every switch.Each user generates their own cache from their own game. Nothing derived from a game is part of this PR or meant to be shared.
vulkan_ir.binholds translated game code, and its tag (format, git revision, Info/Inst sizes, driver, GPU) makes it apply only to the build and driver that wrote it.Measurements (from our branch, not from this one)
AMD Radeon RX 7900 XT 20 GB (RADV, Mesa 26.2.6), Ryzen 7 5700X3D, Linux (Fedora 45 base, kernel 7.2.15), local clang build. Ocarina of Time, scripted headless route; user settings with async shaders on.
Our branch: Lichii nx2 merged up to
ad477ef1(first-session run, 2026-11-08; the next-session run was one build earlier, 2026-11-07), plus this stack and our other, unrelated fixes. Each run started from a precompiledvulkan_ir.bin+ Mesa cache and an emptyvulkan.bin, with GPL and learning on:vulkan_ir.bin6.9 GB), 225,468 stage libraries and 34,308 compute pipelines. The Mesa cache already held most libraries from earlier runs (0.13 ms each), so a cold run takes longer. Translation alone, without variants, took 14-16 s for 42,781 stages (older build); that file is ~0.55-0.65 GB.0857e8b4+ this work at the time, before the Lichii port), 2026-11-06, async off, empty caches vs. precompiled, two runs each, interleaved: the driver-compile time frames waited on fell from 17.2 to 2.8 s, and frame waits from 5.0 to 1.2 s. GPU-thread build time halved. Gameplay frames over 100 ms went from 18-22 to 9-11 (the rest are not shader builds).Not included / limits
Related work
#781 (@zortos293, from #579/#741 by @PantACRO4life), #895 (@zmckenney), and daniel-lynch's per-stage translated-shader cache on his fork. #899 compares them and asks which direction to take.
🤖 Generated with Claude Code