Skip to content

vulkan: link graphics pipeline libraries optimized on AMD's Windows driver - #1

Closed
justin-wq1337 wants to merge 16 commits into
PantACRO4life:perf/gpl-shader-warmup-v2from
justin-wq1337:gpl-amd-optimized-link
Closed

justin-wq1337 wants to merge 16 commits into
PantACRO4life:perf/gpl-shader-warmup-v2from
justin-wq1337:gpl-amd-optimized-link

Conversation

@justin-wq1337

Copy link
Copy Markdown

An AMD test of zortos293#579, and a fix for the one problem I found.

Problem

With "Shader libraries" on, Ocarina of Time draws dotted white fringes around alpha blended effects on AMD's Windows driver: the clouds, the particles in the air, Navi's glow and parts of a tree in Kakariko. A frame dump shows them already in the 1280x720 effects layer (pass 062, shaders 5bcce0f7d9cda422 / cfba60f48bde7a1c / e8193481decd0a8e), before it is composited.

The SPIR-V on the library route differs from the monolithic one only in the descriptor set decorations, so I compared four builds of the same scene:

Build Fringes
Shader libraries off no
Shader libraries on, fast link (current zortos293#579) yes
Shader libraries on, same per-stage sets and SPIR-V, built as a monolithic pipeline (FallBackToMonolithic forced) no
Shader libraries on, every link with VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT no

So Lichii's per-stage sets, bindings and SPIR-V are fine; only the fast (unoptimized) link draws wrong. That points at the driver's fast-link path, though I can't rule out something the optimized link happens to tolerate.

EDEN_NX2_GPL_OPTIMIZE=1 alone doesn't avoid it: pipelines loaded at boot have no build worker, so they keep their fast link, and runtime ones show the fast link until the optimized one is swapped in.

Change

  • Device::CanFastLinkGraphicsLibraries(): false on VK_DRIVER_ID_AMD_PROPRIETARY, true elsewhere. EDEN_NX2_GPL_FAST_LINK=0/1 overrides it on any driver (same pattern as CanUseUnbackedSparseImages).
  • Where fast links aren't usable, shader libraries keep link time optimization info and every link is optimized, including the pipelines built at boot. The later optimized swap of EDEN_NX2_GPL_OPTIMIZE is skipped there, since the first link is already optimized.
  • Nothing changes on other drivers.

Note for AMD users who already ran the warm-up: the libraries now carry the retain flag, so the earlier warm-up no longer matches. The warm-up marker doesn't include the library flags, so it should be rebuilt once ("Rebuild shader warm-up").

Cost

Optimized links are slower than fast links. On the first boot after the change, the ~6,900 pipelines from the disk cache took 320 ms per optimized link on average (max 5.9 s), since the driver had no optimized code cached yet. On the next boot, with the driver cache holding them, the same ~6,500 links took 74 ms on average (max 0.9 s). Links during play took 4 to 5 ms each, and draws held for unbuilt pipelines were 7 to 14 ms per 10 s window in Kakariko. That is still far below a full monolithic build of a new pipeline, but it isn't the near-free fast link.

Results on AMD

GPU RX 7800 XT, AMD driver 26.10.1 (Vulkan 2.0.416), Windows 11
Build nx2 d549411 + zortos293#243 + this PR on zortos293#579
Prepare shaders 3,996 compute, 15,357 vertex, 21,447 fragment programs, 38 skipped
Warm-up compute 1,899 built, 0 failed, 262 s; graphics 36,202 built, 602 failed, 1,183 s; about 24 min in total
Driver cache %LOCALAPPDATA%\AMD\VkCache grew from 1.1 to 2.6 GB
GPL the driver reports graphicsPipelineLibrary and graphicsPipelineLibraryFastLinking

Unrelated freeze during testing

Once, about 80 s into a session, the game froze for good: the same deadlock as zortos293#379 (GPUBack in the texture GC → BackSparseWrite → MapPageGroup waiting on the page table lock, a guest core waiting in HandleRasterizerDownload). It isn't caused by this change; I'm continuing with the higher EDEN_NX2_TC_*_MIB thresholds from that issue.

Screenshots

Fast link (current zortos293#579): dotted white fringes around the clouds, Navi and particles.

579-1-fringes-fast-link

The 1280x720 effects layer from the frame dump (passes 062 to 064), where the fringes already are:

579-2-effects-layer-pass062-064

With this change (optimized links), same spot:

579-3-fixed-optimized-link

AI disclosure: I tested in game; Claude Code (Opus 5.5) narrowed it down with the frame dump and the A/B builds, wrote the change and this description.

🤖 Generated with Claude Code

PantACRO4life and others added 16 commits November 7, 2026 14:42
…pute build logs

With EDEN_NX2_FAST_FIRST=1 a pipeline built while the game runs is first
created with VK_PIPELINE_CREATE_DISABLE_OPTIMIZATION_BIT and used as soon
as the driver returns it. The optimized build follows on a worker and
replaces it at a later draw or dispatch. On a demanding Switch 2 test
title the first-visit stutter is the driver compile (draws held up to
7 s of every 10 s with an empty driver cache); this trades it for a
slower shader during that time.

Compute pipelines built synchronously log their driver build time when
it is over 100 ms (Nx2Perf compute_build), since a dispatch waits for
the whole build.

Off by default; nothing changes without the variable.
EDEN_NX2_HEAVY_SKIP=<N> skips the draws of a native pipeline with at
least N converted instructions over all stages while the driver builds
it, instead of holding every draw behind it ("1" picks 4000). On a
demanding Switch 2 test title every driver build over 500 ms was above
4000 instructions, and those made about 90% of the compile time.

A heavy draw is still held when skipping it could lose data for good:
the pipeline writes storage or uses transform feedback, or its render
target was not cleared this frame, or is a new small target (a baked
texture rather than a scene-sized image redrawn every frame). Clears
are reported to the pipeline cache for this (NoteClear). Held draws
build their own pipeline instead of queueing behind skipped builds.

Environments report their converted instruction count (CodeWords) so
the pipeline knows its size. Counters are logged every 10 s, and held
draws over 100 ms log the pipeline size, the reason they were held and
the render target size (Nx2Perf held_draw).

Off by default; nothing changes without the variable.
EDEN_NX2_COMPUTE_WARM=<file> loads compute programs prepared offline
from the game's own shader archives (pipeline cache format, no recorded
reads) and builds them on 4 low priority threads after the disk cache
has loaded. The pipelines are thrown away; they only fill the driver's
shader cache, so a dispatch that later converts to the same SPIR-V finds
the driver build ready instead of waiting seconds for it. Programs
already in the disk cache are left out.

Reads a file record does not hold are answered with guesses
(FileEnvironment::SetGuessMissing / VideoCommon::GuessMissing) instead
of an error. With the defaults about 95% of the programs converted to
the same SPIR-V as the runtime records on the test title.

Tools:
- shader-sweep --guess-missing [--guess-cbuf0, --guess-cbuf-other,
  --guess-texture-type, --guess-pixel-format, --guess-storage-pointer,
  --guess-storage-pointer-other] translates records with missing reads
  the same way, and accepts cache version 53.
- shader-sweep --override-out FILE writes the lowering as a program
  override record and reports the program's native_key.
- tools/nx2/make_compute_warm.py builds the warm-up file from a static
  shader dump (extract_static_shaders.py output).

Off by default; nothing changes without the variable.
…GPL)

With EDEN_NX2_GPL=1 and a device that supports
VK_EXT_graphics_pipeline_library with fast linking, graphics pipelines
are linked from per-stage libraries instead of being built whole.
Without the variable the device is created exactly as before (the
extension stays out of the extension tables).

Each stage gets its own descriptor set (set index = stage index,
independent sets, plain descriptor sets, Bindings::descriptor_set in
the SPIR-V backend) with bindings from 0, and the fragment stage's
rescaling bits start at fixed indices, so a stage's SPIR-V and library
do not depend on the partner stage's resources.

Pre-rasterization and fragment shader libraries are cached in
PipelineCache, keyed by the emitted SPIR-V hash, the stage's set
layout, the state the part bakes in, the dynamic state list and the
render pass; a worker that needs a library another worker is building
waits for it. Vertex input and fragment output libraries are built per
pipeline, then fast linked. EDEN_NX2_GPL_OPTIMIZE=1 adds an optimized
link on a worker, swapped in like the fast first build. A failed
library or link falls back to a monolithic pipeline with the same sets.
Task/mesh, transform feedback, static rasterizer discard and stages
with too many textures or images for the split rescaling bits stay on
the monolithic route.

PipelineCache::WarmGraphicsLibraries builds a pipeline's shader
libraries with no draw; with a single stage and no partner it builds
only that stage's library. Nx2Perf gpl stats are logged every 10 s.

Based in part on suyu-main 3157ae0ce0 and f80bbc701a (library cache
built outside the lock, fast then optimized link, link statistics);
unlike there, shader libraries are shared across partner stages.
EDEN_NX2_GRAPHICS_WARM=<file> (with graphics pipeline libraries on)
builds the game's vertex and fragment programs as single-stage
libraries in the background after the compute warm-up. The file is
streamed (it can be over a gigabyte); worker threads take the next
entry under the reader's lock. Libraries built by the warm-up are
not kept (GraphicsPipelineLibraryCache::SetNoRetainOnThisThread), so
only the driver's disk cache keeps them and a later pipeline with those
stages costs a link.

tools/nx2/make_graphics_warm.py writes the file from a static shader
dump: one single-stage entry per unique vertex or fragment program,
with draw state taken from the most common state in pipeline caches
the game recorded (--runtime-cache). --validate compares the generated
entries' SPIR-V with the runtime records.

Off by default; nothing changes without the variable.
…tus bar progress

The warm-up files no longer need the offline scripts. A game list entry
"Prepare shaders (warm-up)" (with Rebuild and Delete) runs
ShaderWarm::BuildWarmFiles in a progress window with Cancel; games do
not start while it runs. The builder:
- reads the game's RomFS through the VFS (.nup base and update
  packages, other formats through the loader);
- extracts shader programs with C++ readers for the archive formats
  (zstd with the game's dictionaries, DCX, LZ4, SARC, BND4, BFSHA/BNSH,
  effect bundles, CGSB, SHN, BAHS v11), a port of the PR zortos293#214
  extractor with the same checks;
- lowers and converts them in process with the runtime code layout, on
  low priority threads in program order, and writes the per-title
  warm-compute.bin and warm-graphics.bin atomically, with the same
  records make_compute_warm.py and make_graphics_warm.py write. Draw
  state comes from the title's v53 pipeline caches when there are any,
  else from built-in defaults measured on one test title.

At boot the warm-ups load the title's warm files when
EDEN_NX2_COMPUTE_WARM / EDEN_NX2_GRAPHICS_WARM are unset. A warm-up
that ran to the end writes a .done marker (file size and write time,
entries, driver version, device ID, cache version); a matching marker
skips that warm-up at the next boot. Rebuild and Delete remove the
warm files, markers and temporary files.

Progress goes through ShaderNotify (SetWarmProgress / GetWarmProgress,
lock-free, 4 updates a second) to the status bar, next to the shader
build count: compute and graphics counts, with a percent when the
total is known.

A "Shader libraries" setting (use_graphics_pipeline_library) in the
advanced graphics tab turns on graphics pipeline libraries like
EDEN_NX2_GPL=1.

yuzu_cmd gets a hidden --build-warm <game> <title id> <output dir>
[--defaults | caches] that runs the builder without booting the game
(test hook).
…ent of fast-first compute, warm path encoding, builder exceptions, JSON summary, offline tool failures and defaults)

- vk_graphics_pipeline: the GPU thread checks a has_pipeline flag the build sets before it
  completes, instead of reading pipeline while the scheduler thread swaps in the optimized one.
- vk_compute_pipeline: a retired pipeline also waits for its pending optimized build.
- vk_pipeline_cache: open the warm files and their markers through one path in its native
  encoding (environment overrides stay native narrow).
- shader_warm_dialog: a builder exception is reported as a failed build instead of leaving the
  worker thread; deleting uses the shared DeleteWarmFiles (markers and temporary files too).
- yuzu-cmd: escape the error text in the JSON summary.
- make_compute_warm.py: a hung or unstartable sweep counts as a failed program.
- make_graphics_warm.py: clear error when a stage has no runtime records.
- shader-sweep: --guess-missing guesses no storage pointers in banks 1-17, as the emulator does.
…he right after a version bump

The builder read only the current version's runtime cache. Just after a version bump (53 to 54)
that cache does not exist yet, so it fell back to the built-in defaults; a first-visit walk on the
test machine after that warm-up still compiled many libraries in game (0.5 to 0.8 s each). Read
the newest cache of a version with the same record layout instead.
…, keep a warm file's marker until it is replaced
…river

Pipelines fast-linked from graphics pipeline libraries draw white fringes
around alpha blended effects on AMD's Windows driver (Ocarina of Time's
clouds, particles and Navi in Kakariko, RX 7800 XT, driver 26.10.1). The
same SPIR-V and per-stage descriptor sets built as a monolithic pipeline,
or linked with link time optimization, draw correctly. Libraries there keep
link time optimization info and every link is optimized;
EDEN_NX2_GPL_FAST_LINK=0/1 overrides the choice on any driver.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@PantACRO4life

Copy link
Copy Markdown
Owner

Thanks! Taken into zortos293#579 as c196b4f (your authorship kept, title names removed from the commit message and one comment). Closing this one since the commit is in the PR branch now.

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.

2 participants