Skip to content

video_core: Implement shader precompilation - #814

Draft
failaip12 wants to merge 20 commits into
Eglantine-Community:nx2-experimentalfrom
cira0100:video_core/shader-precompile
Draft

failaip12 wants to merge 20 commits into
Eglantine-Community:nx2-experimentalfrom
cira0100:video_core/shader-precompile

Conversation

@failaip12

@failaip12 failaip12 commented Nov 9, 2026 •

Copy link
Copy Markdown

Summary

Adds a experimental toggle to precompile shaders.
It currently works in a weird way due to it needing a recorded pipeline cache. It needs the user to toggle the option first, run the game which will precompile the compute shaders, play at least one track, restart the game, and only then will the full recompilation process start.
This can be fixed by bundling the game specific render setups(recorded pipeline cache) which should be fine legally but is kinda close to bundling precompiled shaders so this is not done here.
This is a draft as windows and intel configurations are not tested and i am unsure of the stability of this right now.

Type of change

  • Bug fix
  • Game compatibility / game-support
  • Performance
  • New feature
  • Refactor / cleanup
  • CI / build
  • Documentation

Related issues

Works best with #812 (cache folder for Mario Kart World).
On RADV, precompiled pipelines are currently kept in Eglantine's vulkan_pipelines.bin; with #714 they would live in Mesa's shader cache instead.
May help with first-play shader stutter reported in #121.

Testing

  • Tested on Windows
  • Tested on Linux
  • Tested on macOS
  • Tested on NVIDIA
  • Tested on AMD
  • Tested on Intel

Games tested:

Tested on MKW 1.8.0 on Linux wiith nvidia and linux with amd gpu.
Nvidia shaders took much longer to build than AMD ones.

Checklist

  • I built it locally and it compiles without new warnings
  • I did not add any copyrighted game data, keys or firmware
  • Docs updated if needed
  • I added AI Disclosure if one was used

Screenshots / logs

Claude Opus 5.5 was used for the investigation and the implementation.

Assisted-by: Claude Opus 5.5 (investigation and implementation)

@github-actions
github-actions Bot changed the base branch from nx2 to nx2-experimental November 9, 2026 20:50
@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

Retargeted to nx2-experimental for pre-build testing. It will be promoted to nx2 after validation.

Switch 2 games ship their native Ampere programs verbatim in shader
containers (BNSH, agl BAHS, effect and layout archives). Each program
ends with a branch to itself, and a graphics program starts 0x80 bytes
into a 256-byte block that holds its program header. The scanner finds
the programs from those markers and checks each one with the same
capture the runtime uses, so it reads the code the runtime would read.

BNSH program tables also tell which vertex and pixel programs form a
shader program, and lead to the NVN shader control section that holds
a compute program's workgroup and shared memory sizes. Nothing uses
the scanner yet.

Assisted-by: Claude Opus 5.5 (investigation and implementation)
Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com>
Move the part of LowerNativeCode that builds the cached code (program
header, lowered instructions with scheduling words, native address
map) into MakeLoweredNativeCode, so programs read from the game's files
get the same code and shader hash as the ones captured at bind. No
functional change.

Assisted-by: Claude Opus 5.5 (investigation and implementation)
Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com>
ScannedEnvironment translates a native program before the game binds
it. Translation reads GPU state the program only sees once bound, so it
answers with what nearly all programs read: 2D textures with float
formats, mapped storage pointers and zero for constant buffer values.
Compute programs take their workgroup and shared memory sizes from the
caller.

Assisted-by: Claude Opus 5.5 (investigation and implementation)
Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com>
Add the per-game "Precompile shaders (Switch 2)" setting, off by
default. When it is on, booting a Switch 2 game scans its RomFS for
native programs on a background thread while the pipeline cache loads,
and hands the programs, their vertex and pixel pairs and the compute
dispatch sizes to the rasterizer. The list is cached beside the
pipeline cache as native_programs.bin, so later boots skip the scan.
No rasterizer uses it yet.

Assisted-by: Claude Opus 5.5 (investigation and implementation)
Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com>
… screen

Switch 2 games stutter for a long time the first time new tracks and
effects are drawn, because each new pipeline is compiled then. At the
end of the loading screen, build a pipeline for each vertex and pixel
pair of the game's files, for the render setups and vertex attribute
types the title's pipeline cache recorded, and drop it again. The
driver keeps what it compiled, so the game's own pipelines are ready
when first drawn. Built keys are listed in precompiled.bin, so later
boots only build what is new.

Assisted-by: Claude Opus 5.5 (investigation and implementation)
Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com>
Build the game's compute programs with the workgroup and shared memory
sizes from their control sections as well. They need no recorded state,
so this also works on a first boot. A program that uses shared memory
is only built with a dispatch that gives it some, as SPIR-V emission
aborts otherwise.

Assisted-by: Claude Opus 5.5 (investigation and implementation)
Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com>
@failaip12
failaip12 force-pushed the video_core/shader-precompile branch from c0c19b4 to c5be508 Compare November 9, 2026 23:21
…lt it

Graphics and compute pipelines catch a failed vkCreate*Pipelines call
and still return their object, marked built without a Vulkan pipeline.
Precompilation counted that object as success and saved it, so a failed
build was skipped on every later boot. Only count builds that created
the Vulkan pipeline.

Assisted-by: Claude Opus 5.5 (code review and implementation)
Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com>
precompiled.bin only checked the cache version, so a new driver, another
GPU or a deleted vulkan_pipelines.bin left every pair marked done while
the pipelines themselves were gone. Store a hash of the device's pipeline
cache UUID, driver id and driver version, and drop the records when it
changes, when vulkan_pipelines.bin held nothing at boot, or when neither
Eden nor the driver keeps a cache. Also bound the record count on read.

Assisted-by: Claude Opus 5.5 (code review and implementation)
Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com>
The loading screen waited for the RomFS scan to finish even after the
user cancelled loading, which on a first boot takes several seconds.
Forward the loading stop token to the scan thread while waiting for it.

Assisted-by: Claude Opus 5.5 (code review and implementation)
Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com>
The RomFS scan merged programs by their code alone, so a program that
reuses another's code with a different header (outputs, attributes)
was dropped, and its pairs pointed at the wrong header. Merge by header
and code, and bump the program list version so old lists are rescanned.

Assisted-by: Claude Opus 5.5 (code review and implementation)
Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com>
The scan read the loader's base RomFS, so an update or a LayeredFS mod
that replaces shader archives was never seen, and the program list was
reused as long as the base image kept its size. Open the RomFS through
the process's RomFS controller, as the game does, and key the list by
the names and sizes of the files in it.

Assisted-by: Claude Opus 5.5 (code review and implementation)
Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com>
@failaip12 failaip12 changed the title Video core/shader precompile video_core: Implement shader precompilation Nov 9, 2026
@failaip12
failaip12 force-pushed the video_core/shader-precompile branch from 8a5c8c4 to 6575f29 Compare November 9, 2026 23:48
A recompiler change can alter the SPIR-V a program gives while its
lowered code, and so its pipeline key, stays the same. precompiled.bin
then kept every key marked done and the new pipelines were never built,
though the setting says an emulator update redoes them. Add the build's
commit to the identity the records are tied to.

Assisted-by: Claude Opus 5.5 (code review and implementation)
Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com>
The scan read every file it did not skip by name whole, so a multi-GB
pack in another title meant one allocation that size, and an exception
there escaped the scan thread and ended the process. Skip files over
512 MB and catch scan failures, which then leave the list empty.

Assisted-by: Claude Opus 5.5 (code review and implementation)
Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com>
Pipelines are precompiled from LoadDiskResources, which the frontends
skip when the disk shader cache is off. The RomFS scan still ran then,
during play, and its list was never used.

Assisted-by: Claude Opus 5.5 (code review and implementation)
Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com>
Every precompiled pixel program assumed its vertex program writes all
the inputs it reads. That is only needed where a stand-in program fills
the other stage; for pairs from the game's program tables the vertex
program is the one drawn with, so give its outputs as at runtime. A
pixel program reading an input its vertex program skips then gets the
same SPIR-V as at runtime.

Assisted-by: Claude Opus 5.5 (code review and implementation)
Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com>
native_programs.bin was trusted as read: counts and sizes were not
bounded, and pair and dispatch indices not checked against the program
count, so a corrupt file could allocate without bound or index past the
programs. Reject a list outside those bounds; it is then scanned again.

Assisted-by: Claude Opus 5.5 (code review and implementation)
Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com>
The pair and dispatch scans walked every word from each container
header to the end of the file, so their cost grew with containers times
file size. Bounding each walk at the next container would lose entries:
Mario Kart World's Drive archive keeps the stage entries of its BNSH
containers past later containers (37 of its 375 dispatches). Find the
words that hold the 0xd8 tag once, and match each container only
against those. Results are unchanged; the table scans of Mario Kart
World's main containers take 0.06 s instead of 0.22 s.

Assisted-by: Claude Opus 5.5 (code review and implementation)
Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com>
Every program with a type 1 header was built as a vertex program, but
tessellation and geometry programs use that type too (Mario Kart World
has 13 tessellation programs), which gave wasted or failing builds.
Read the shader type from the header and leave the other stages out.

Assisted-by: Claude Opus 5.5 (code review and implementation)
Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com>
The program list (Mario Kart World: 11,261 programs), the recorded
render setups and attribute layouts and the done keys stayed in memory
for the whole session, though nothing reads them after precompiling.
Release them when RunPrecompile returns.

Assisted-by: Claude Opus 5.5 (code review and implementation)
Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com>
After loading was cancelled, the final line counted the jobs that never
ran as precompiled, and devices with broken compute logged an error for
each compute job. Count the builds that finished, and leave compute out
on those devices.

Assisted-by: Claude Opus 5.5 (code review and implementation)
Signed-off-by: failaip12 <86018517+failaip12@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant