Skip to content

shader_warm: follow-ups to #579 (warm-up on the loading screen, link mode in the marker, save the driver cache after a warm-up) - #741

Draft
PantACRO4life wants to merge 38 commits into
zortos293:nx2from
PantACRO4life:perf/gpl-warmup-at-boot
Draft

PantACRO4life wants to merge 38 commits into
zortos293:nx2from
PantACRO4life:perf/gpl-warmup-at-boot

Conversation

@PantACRO4life

@PantACRO4life PantACRO4life commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #579, built on top of it. Until #579 merges, this diff also shows its commits: only the last three are new here (1385900, cebb8d7 and 5cd5a66).

Changes

Prepare shaders on the loading screen (1385900), suggested by @jpkto on #579.

  • A pending shader warm-up can run on the loading screen, as a new "Preparing shaders x / y" stage after the cached pipelines, on every CPU thread at normal priority. The game starts once it is done. Before, it only ran in the background at low priority while the game played, with more stutter until it ended.
  • New setting "Prepare shaders before the game starts" (Ask, Always, Never) in Configure, Graphics, Advanced.
  • Closing the game during the stage cancels the warm-up. What was already built stays in the driver's pipeline cache, so the next launch mostly finishes the part that was left.
  • The new LoadCallbackStage::Warm is added last, so the other stages keep their values. Android shows it as shader building.

Link mode in the warm-up marker (cebb8d7), suggested by @justin-wq1337 on #579.

  • The "already warmed" marker now records the graphics link mode: whether fast links are usable on the driver, and whether optimized links are on (EDEN_NX2_GPL_FAST_LINK, EDEN_NX2_GPL_OPTIMIZE). Together they decide whether shader libraries keep link time optimization info, which changes what the driver caches. A change of link mode now reruns the warm-up instead of needing a manual rebuild.
  • The marker format changes, so an existing warm-up runs once more after updating. Already built pipelines come back from the driver cache, so this is fast (under a minute here).

Save the driver pipeline cache after a warm-up (5cd5a66), reported by @IorekByr on #579.

  • The warm-ups' builds go into the driver pipeline cache, but the timed flush counts only pipelines built during play, so a finished warm-up reached the file only at the next boot, game change or clean close. If the app ended any other way, the work was lost. On Linux/RADV the cache lives in that file. Each warm-up stage now queues one write of the file when it finishes.

Testing

PantACRO4life and others added 26 commits November 7, 2026 16:28
…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
The warm-up threads only stopped when the pipeline cache was destroyed, late in shutdown. Until
then they kept compiling (16 threads in one test), the emulation thread did not stop in time and
was terminated. They now also end, after their current build, when the loading stop token fires.
…river

Pipelines fast-linked from graphics pipeline libraries draw white fringes
around alpha blended effects on AMD's Windows driver (clouds, particles
and glows in a test title, 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>
…N_NX2_COMPUTE_NOINLINE)

The multi-second whole-process freezes in a test title (one dungeon, 1.4-5.6 s) are the
first dispatch of a new compute pipeline waiting for NVIDIA's driver compile. Configure
records RunBuild + a condvar wait on the scheduler thread, so the scheduler, the GPU thread
behind it and the guest behind its present fence all stop until vkCreateComputePipelines
returns. A native program's global memory functions (DefineGlobalMemoryFunctions: the
address tested against every storage buffer, ~300-870 instructions each) are called from
dozens of places, and NVIDIA inlines every call: single shaders took 2-13 s to build.

Mark functions of 128+ instructions called from 2+ places DontInline when the compute
module is built (the translation cache keeps the SPIR-V as emitted, so cached and fresh
stages build alike and the cache key is unchanged). EDEN_NX2_COMPUTE_NOINLINE=0 builds the
SPIR-V as emitted. Also: "Nx2Perf compute driver build" (>100 ms, with CPU time and thread)
and "Nx2Perf compute first dispatch ... waited" (>20 ms) log lines, and
EDEN_NX2_COMPUTE_SPIRV_DUMP=<dir> to dump compute SPIR-V for offline timing.

RTX 3080 Ti, driver 615.78.08:
- Offline, all 618 compute shaders of a scene-tour translation cache, NVIDIA disk cache off:
  the 92 with marked functions 246 s -> 72.5 s of compile, shaders over 1 s 78 -> 20
  (the other 526 took 11.7 s and none over 1 s). VK_PIPELINE_CREATE_DISABLE_OPTIMIZATION_BIT
  does not shorten NVIDIA's compile; running spirv-opt first makes it longer.
- Harness, cold pipeline + NVIDIA caches, open field -> town -> dungeon, interleaved:
  first-dispatch waits off 51.3 / 51.6 s total (max 5.27 / 5.51 s, 18 / 19 over 1 s),
  on 22.1 / 24.1 s (max 3.14 / 4.04 s, 7 / 6 over 1 s); log silences over 1 s 47 / 48 -> 35 / 35.
- Second session (warm caches) has no compute waits in either mode: the freeze is paid once
  per new compute shader per driver cache.

Ported to this branch: the translation cache hit path and the "Nx2Perf compute driver
build" log are left out (this branch has no translation cache, and already logs compute
driver builds through MakeComputePipeline and the build trace). Every compute module,
including the fast-first and compute warm-up builds, goes through ComputeModuleCode.

(cherry picked from commit 8bd8342)
(cherry picked from commit 2fa4ecb5f048df579f58b98db095f0707875bcc3)
…X2_GRAPHICS_NOINLINE)

Graphics stages of native programs carry the same global memory functions as compute
(DefineGlobalMemoryFunctions), called from many places and inlined at each one by the driver.
Run the same DontInline pass on graphics SPIR-V in EmitShaderModule, where every graphics module
is made: monolithic pipelines, pipeline libraries, deferred worker emission and the graphics
warm-up. EDEN_NX2_GRAPHICS_NOINLINE=0 builds the SPIR-V as emitted.

The pipeline library key (spirv_hashes) is now the hash of the words handed to the driver, after
the pass, so a library built by the warm-up and one needed in game share a key only when their
modules are identical. The device SaveShader copy and the log dumps stay the emitted SPIR-V, as
for compute. Test: vertex and fragment entry points are marked like compute.

(cherry picked from commit e4e4fbea121b7e59584539e671a0ca57ee80a1c8)
The compute and graphics warm-ups each ran on a fixed 4 threads. EDEN_NX2_WARM_THREADS=N
(1 to 32) sets the count for both; anything else keeps 4, with a warning. The threads stay at
low priority, and the existing "on N low priority threads" log lines show the count used.

(cherry picked from commit cfb80df71b445613c36a0d60ccb708ada6828d40)
…starts its own

LoadDiskResources can run again in the same pipeline cache (another title or
program). The earlier load's warm-up threads were neither stopped nor joined,
so StartComputeWarm and StartGraphicsWarm returned early and never warmed the
new title, and clearing warm_cancel let old threads keep working on the
previous title's file. A later load now cancels and joins them, drops the old
stop callback and resets the compute-done flag first.
…ode, driver failures keep a graphics warm-up pending, heavy skip needs full clears, no warm-up deletion while preparing)

- The warm-up marker also records whether shared shader functions are
  built as calls (compute and graphics); after that setting changes, the
  driver compiled different SPIR-V, so the warm-up runs again. Markers
  without the field no longer match.
- Graphics warm-up entries now end as built, not buildable (a guess the
  translation rejects) or refused by the driver. A warm-up with driver
  failures writes no marker, so the next start tries again.
- A library-only build whose library the driver did not make reports a
  failed build instead of returning as if built.
- Heavy skip: a color clear counts only when it clears every channel, and
  a target counts as rebuilt only when each of its attachments was cleared.
- Deleting a game's warm-up while it is being prepared is refused.
…hole attachment and every depth/stencil aspect; the deletion guard applies to the title being prepared)

- A clear marks a target rebuilt only when it covers the attachment's own
  size and layers (scaled like the framebuffer), not just the framebuffer,
  which can be smaller than an atlas or array. Pitch-linear color targets
  never count.
- A combined depth/stencil target counts only when depth is cleared and
  stencil is cleared with every bit written.
- Deleting a game's warm-up is refused only while that same game's
  warm-up is being prepared.
…ame starts

A pending shader warm-up can now run on the loading screen, as a new
"Preparing shaders x / y" stage after the cached pipelines, on every CPU
thread at normal priority; the game starts once it is done. Before, it only
ran in the background at low priority while the game played, with more
stutter until it ended and a game that felt slow.

New setting "Prepare shaders before the game starts" (Ask, Always, Never;
Configure, Graphics, Advanced). With Ask the desktop app asks at launch when
the title has a warm-up that never finished, with "Don't ask again" keeping
the answer as the setting. Never keeps the background warm-up. Where nothing
asks (command line, Android) Ask acts as Never. Closing the game during the
stage cancels the warm-up; it runs again at the next launch.

The new LoadCallbackStage::Warm is last, so the other stages keep their
values; Android shows it as shader building.
…e of EDEN_NX2_GPL_FAST_LINK, EDEN_NX2_GPL_OPTIMIZE or the driver's fast link support runs the warm-up again
@coderabbitai

coderabbitai Bot commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Share

@justin-wq1337

Copy link
Copy Markdown
Contributor

Thanks for picking up the link-mode marker. Happy to test this on AMD (RX 7800 XT, Windows) once it leaves draft: the loading-screen stage, cancelling midway, and whether the marker reruns the warm-up when EDEN_NX2_GPL_FAST_LINK flips.

One more AMD-specific idea for later, not for this PR: since every link there is an optimized one (20-190 ms each, and the driver cache doesn't shorten it; see my numbers on #579), the warm-up could also pre-link the pipelines of the used list on the warm-up threads, so the first draw in an area doesn't pay for it. The remaining cost for me is now exactly that: on entering Castle Town 173 links in 10 s with every library a cache hit. Whether the render pass is available at that point you know better.

@PantACRO4life

Copy link
Copy Markdown
Contributor Author

@justin-wq1337 no need to wait for it to leave draft. It's a draft only because #579 isn't merged yet and it hasn't been tested in game. You can build it now from my fork, branch perf/gpl-warmup-at-boot (https://github.com/PantACRO4life/Lichii/tree/perf/gpl-warmup-at-boot). It's #579 plus two commits, so merging it onto nx2 the way you did for #579 works. Your three checks are exactly what it needs.

Pre-linking on AMD: agreed, that's the remaining cost there. The catch is the render pass: the warm-up keys carry no render targets today (jarynclouatre's finding on #579). The pipelines in the play history do record theirs, so pre-linking those becomes possible once the warm-up reads that state. Noted for after this one.

cursor Bot pushed a commit to gamehhh7/Lichii that referenced this pull request Nov 8, 2026
Co-authored-by: gamehhh7 <gamehhh7@users.noreply.github.com>

# Conflicts:
#	src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
capy-ai Bot added a commit that referenced this pull request Nov 8, 2026
* vulkan: compute warm-up from the game files (EDEN_NX2_COMPUTE_WARM)

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.

* yuzu: in-app shader warm-up builder, Shader libraries setting and status 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 #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).

Ported for the shader precache option (#579) without the Shader libraries
setting, the graphics warm-up start and the pipeline library device setup.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* shader_warm: write and read warm files with the current pipeline cache version

* vulkan, shader_warm, tools: review fixes (pipeline swap race, retirement 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.

Ported for the shader precache option (#579) without the fast first build,
pipeline swap and graphics warm-up parts.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* tools/nx2: write warm files with cache version 54 by default

* docs: describe the functions added by the shader warm-up and pipeline library work

Ported for the shader precache option (#579): only the comments on the
shader warm-up code.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* shader_warm: take the draw state guesses from an older compatible cache 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.

* shader_warm: record the warm files' entry counts so the warm-up shows exact progress

Ported for the shader precache option (#579) without the graphics warm-up
part.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* vulkan, shader_warm: note only whole-target clears for the heavy skip, keep a warm file's marker until it is replaced

Ported for the shader precache option (#579): only the warm file marker
part.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* vulkan: stop the shader warm-ups as soon as emulation is asked to stop

The warm-up threads only stopped when the pipeline cache was destroyed, late in shutdown. Until
then they kept compiling (16 threads in one test), the emulation thread did not stop in time and
was terminated. They now also end, after their current build, when the loading stop token fires.

Ported for the shader precache option (#579) without the graphics warm-up
part.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* vulkan: stop the previous load's shader warm-ups before a later load starts its own

LoadDiskResources can run again in the same pipeline cache (another title or
program). The earlier load's warm-up threads were neither stopped nor joined,
so StartComputeWarm and StartGraphicsWarm returned early and never warmed the
new title, and clearing warm_cancel let old threads keep working on the
previous title's file. A later load now cancels and joins them, drops the old
stop callback and resets the compute-done flag first.

* vulkan, yuzu: review fixes (warm-up marker records the shader calls mode, driver failures keep a graphics warm-up pending, heavy skip needs full clears, no warm-up deletion while preparing)

- The warm-up marker also records whether shared shader functions are
  built as calls (compute and graphics); after that setting changes, the
  driver compiled different SPIR-V, so the warm-up runs again. Markers
  without the field no longer match.
- Graphics warm-up entries now end as built, not buildable (a guess the
  translation rejects) or refused by the driver. A warm-up with driver
  failures writes no marker, so the next start tries again.
- A library-only build whose library the driver did not make reports a
  failed build instead of returning as if built.
- Heavy skip: a color clear counts only when it clears every channel, and
  a target counts as rebuilt only when each of its attachments was cleared.
- Deleting a game's warm-up while it is being prepared is refused.

Ported for the shader precache option (#579): only the deletion guard.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* vulkan, yuzu: review fixes (heavy skip counts a clear only over the whole attachment and every depth/stencil aspect; the deletion guard applies to the title being prepared)

- A clear marks a target rebuilt only when it covers the attachment's own
  size and layers (scaled like the framebuffer), not just the framebuffer,
  which can be smaller than an atlas or array. Pitch-linear color targets
  never count.
- A combined depth/stencil target counts only when depth is cleared and
  stencil is cleared with every bit written.
- Deleting a game's warm-up is refused only while that same game's
  warm-up is being prepared.

Ported for the shader precache option (#579): only the deletion guard.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* shader_warm, yuzu: prepare shaders on the loading screen before the game starts

A pending shader warm-up can now run on the loading screen, as a new
"Preparing shaders x / y" stage after the cached pipelines, on every CPU
thread at normal priority; the game starts once it is done. Before, it only
ran in the background at low priority while the game played, with more
stutter until it ended and a game that felt slow.

New setting "Prepare shaders before the game starts" (Ask, Always, Never;
Configure, Graphics, Advanced). With Ask the desktop app asks at launch when
the title has a warm-up that never finished, with "Don't ask again" keeping
the answer as the setting. Never keeps the background warm-up. Where nothing
asks (command line, Android) Ask acts as Never. Closing the game during the
stage cancels the warm-up; it runs again at the next launch.

The new LoadCallbackStage::Warm is last, so the other stages keep their
values; Android shows it as shader building.

Ported for the shader precache option (#579/#741) without the graphics
warm-up, the shader calls and warm-up thread settings and
EDEN_NX2_WARM_THREADS: before the game starts the compute warm-up uses
every CPU thread, else 4.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* vulkan: save the driver pipeline cache when a warm-up stage finishes

The warm-ups' builds go into the driver pipeline cache, but the timed flush
counts only pipelines built during play, so a finished warm-up reached the
file only at the next boot, game change or clean close, and was lost when
the app ended otherwise (reported on Linux/RADV by IorekByr, where the
cache lives in that file).

Ported for the shader precache option (#741) without the graphics warm-up
part.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* shader precache: compute-only "Precache shaders before game start" option

Turns the ported warm-up into one setting, precache_shaders (Off, Ask, Always;
default Off; global or per game, Graphics > Advanced). With it on, the compute
programs found in the game files are built on every CPU thread on the loading
screen before the game starts, so the driver's shader cache already holds them
when the game first dispatches them. The loading screen shows
"Precaching shaders N / M" and has a Skip button; a finished precache is
remembered per file, GPU driver, device and cache version and is not run again.

Ask asks at launch while the game has no finished precache. Ask and Always
build the precache file from the game files first when it is missing (the
existing dialog); the game list keeps Build/Rebuild/Delete Shader Precache.

Compared with #579/#741 this drops the graphics warm-up and pipeline
libraries (GPL), the DontInline/shader-call setting, fast-first, heavy skip,
the background warm-up during play, the status bar warm-up label and every
EDEN_NX2_* switch: the setting is the only switch. Adds unit tests for the
setting, the launch requests, the marker and count files and the compute
entry format.

Co-authored-by: PantACRO4life <6643071+PantACRO4life@users.noreply.github.com>

* Precache shaders before game start (compute, from #579/#741)

* Remove a stray install log

* shader precache: review fixes

Skip and stop cancel every precache thread before joining them; a title
precached before goes back to the renderer's driver and file check (Ask
included); no precache without the disk shader cache, which is where it
runs; precache entries keep the texture bank the program names, like
InitializeNativeCode.

* shader precache: precache the pipeline cache's skipped pipelines; no error for games without readable shaders

Kirby Air Riders keeps its assets in its own packs (assets.cat and
assets_a000.dat) and Mario Tennis Fever in one archive, so the builder
found no shader archives and failed. It now writes an empty precache file
and the dialog says the game's shaders can't be read from its files.

The precache also builds the cached graphics pipelines the boot leaves for
later (not used by recent sessions, or past the cap without a used list),
then throws them away so only the driver's cache keeps them. The ones
built are listed per driver in precache-graphics.bin, so later launches
build only new ones. Their captured environments are held up to 1 GiB per
boot; the rest wait for a later launch.

---------

Co-authored-by: PantACRO4life <6643071+PantACRO4life@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
pavelgajdo pushed a commit to pavelgajdo/Lichii-Android that referenced this pull request Nov 8, 2026
…s293#741) (zortos293#781)

* vulkan: compute warm-up from the game files (EDEN_NX2_COMPUTE_WARM)

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.

* yuzu: in-app shader warm-up builder, Shader libraries setting and status 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).

Ported for the shader precache option (zortos293#579) without the Shader libraries
setting, the graphics warm-up start and the pipeline library device setup.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* shader_warm: write and read warm files with the current pipeline cache version

* vulkan, shader_warm, tools: review fixes (pipeline swap race, retirement 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.

Ported for the shader precache option (zortos293#579) without the fast first build,
pipeline swap and graphics warm-up parts.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* tools/nx2: write warm files with cache version 54 by default

* docs: describe the functions added by the shader warm-up and pipeline library work

Ported for the shader precache option (zortos293#579): only the comments on the
shader warm-up code.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* shader_warm: take the draw state guesses from an older compatible cache 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.

* shader_warm: record the warm files' entry counts so the warm-up shows exact progress

Ported for the shader precache option (zortos293#579) without the graphics warm-up
part.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* vulkan, shader_warm: note only whole-target clears for the heavy skip, keep a warm file's marker until it is replaced

Ported for the shader precache option (zortos293#579): only the warm file marker
part.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* vulkan: stop the shader warm-ups as soon as emulation is asked to stop

The warm-up threads only stopped when the pipeline cache was destroyed, late in shutdown. Until
then they kept compiling (16 threads in one test), the emulation thread did not stop in time and
was terminated. They now also end, after their current build, when the loading stop token fires.

Ported for the shader precache option (zortos293#579) without the graphics warm-up
part.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* vulkan: stop the previous load's shader warm-ups before a later load starts its own

LoadDiskResources can run again in the same pipeline cache (another title or
program). The earlier load's warm-up threads were neither stopped nor joined,
so StartComputeWarm and StartGraphicsWarm returned early and never warmed the
new title, and clearing warm_cancel let old threads keep working on the
previous title's file. A later load now cancels and joins them, drops the old
stop callback and resets the compute-done flag first.

* vulkan, yuzu: review fixes (warm-up marker records the shader calls mode, driver failures keep a graphics warm-up pending, heavy skip needs full clears, no warm-up deletion while preparing)

- The warm-up marker also records whether shared shader functions are
  built as calls (compute and graphics); after that setting changes, the
  driver compiled different SPIR-V, so the warm-up runs again. Markers
  without the field no longer match.
- Graphics warm-up entries now end as built, not buildable (a guess the
  translation rejects) or refused by the driver. A warm-up with driver
  failures writes no marker, so the next start tries again.
- A library-only build whose library the driver did not make reports a
  failed build instead of returning as if built.
- Heavy skip: a color clear counts only when it clears every channel, and
  a target counts as rebuilt only when each of its attachments was cleared.
- Deleting a game's warm-up while it is being prepared is refused.

Ported for the shader precache option (zortos293#579): only the deletion guard.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* vulkan, yuzu: review fixes (heavy skip counts a clear only over the whole attachment and every depth/stencil aspect; the deletion guard applies to the title being prepared)

- A clear marks a target rebuilt only when it covers the attachment's own
  size and layers (scaled like the framebuffer), not just the framebuffer,
  which can be smaller than an atlas or array. Pitch-linear color targets
  never count.
- A combined depth/stencil target counts only when depth is cleared and
  stencil is cleared with every bit written.
- Deleting a game's warm-up is refused only while that same game's
  warm-up is being prepared.

Ported for the shader precache option (zortos293#579): only the deletion guard.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* shader_warm, yuzu: prepare shaders on the loading screen before the game starts

A pending shader warm-up can now run on the loading screen, as a new
"Preparing shaders x / y" stage after the cached pipelines, on every CPU
thread at normal priority; the game starts once it is done. Before, it only
ran in the background at low priority while the game played, with more
stutter until it ended and a game that felt slow.

New setting "Prepare shaders before the game starts" (Ask, Always, Never;
Configure, Graphics, Advanced). With Ask the desktop app asks at launch when
the title has a warm-up that never finished, with "Don't ask again" keeping
the answer as the setting. Never keeps the background warm-up. Where nothing
asks (command line, Android) Ask acts as Never. Closing the game during the
stage cancels the warm-up; it runs again at the next launch.

The new LoadCallbackStage::Warm is last, so the other stages keep their
values; Android shows it as shader building.

Ported for the shader precache option (zortos293#579/zortos293#741) without the graphics
warm-up, the shader calls and warm-up thread settings and
EDEN_NX2_WARM_THREADS: before the game starts the compute warm-up uses
every CPU thread, else 4.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* vulkan: save the driver pipeline cache when a warm-up stage finishes

The warm-ups' builds go into the driver pipeline cache, but the timed flush
counts only pipelines built during play, so a finished warm-up reached the
file only at the next boot, game change or clean close, and was lost when
the app ended otherwise (reported on Linux/RADV by IorekByr, where the
cache lives in that file).

Ported for the shader precache option (zortos293#741) without the graphics warm-up
part.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* shader precache: compute-only "Precache shaders before game start" option

Turns the ported warm-up into one setting, precache_shaders (Off, Ask, Always;
default Off; global or per game, Graphics > Advanced). With it on, the compute
programs found in the game files are built on every CPU thread on the loading
screen before the game starts, so the driver's shader cache already holds them
when the game first dispatches them. The loading screen shows
"Precaching shaders N / M" and has a Skip button; a finished precache is
remembered per file, GPU driver, device and cache version and is not run again.

Ask asks at launch while the game has no finished precache. Ask and Always
build the precache file from the game files first when it is missing (the
existing dialog); the game list keeps Build/Rebuild/Delete Shader Precache.

Compared with zortos293#579/zortos293#741 this drops the graphics warm-up and pipeline
libraries (GPL), the DontInline/shader-call setting, fast-first, heavy skip,
the background warm-up during play, the status bar warm-up label and every
EDEN_NX2_* switch: the setting is the only switch. Adds unit tests for the
setting, the launch requests, the marker and count files and the compute
entry format.

Co-authored-by: PantACRO4life <6643071+PantACRO4life@users.noreply.github.com>

* Precache shaders before game start (compute, from zortos293#579/zortos293#741)

* Remove a stray install log

* shader precache: review fixes

Skip and stop cancel every precache thread before joining them; a title
precached before goes back to the renderer's driver and file check (Ask
included); no precache without the disk shader cache, which is where it
runs; precache entries keep the texture bank the program names, like
InitializeNativeCode.

* shader precache: precache the pipeline cache's skipped pipelines; no error for games without readable shaders

Kirby Air Riders keeps its assets in its own packs (assets.cat and
assets_a000.dat) and Mario Tennis Fever in one archive, so the builder
found no shader archives and failed. It now writes an empty precache file
and the dialog says the game's shaders can't be read from its files.

The precache also builds the cached graphics pipelines the boot leaves for
later (not used by recent sessions, or past the cap without a used list),
then throws them away so only the driver's cache keeps them. The ones
built are listed per driver in precache-graphics.bin, so later launches
build only new ones. Their captured environments are held up to 1 GiB per
boot; the rest wait for a later launch.

---------

Co-authored-by: PantACRO4life <6643071+PantACRO4life@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 44ac3b7)
nx2 now has the compute part of this work through zortos293#781 (shader precache on
the loading screen, Off by default). Resolved onto that:

- The graphics warm-up runs inside the precache stage, after the cached
  pipelines and the compute programs, on the same threads, with the same
  progress, Skip and stop handling (PrecacheGraphicsWarm). It needs Shader
  libraries and precache set to Ask (with yes) or Always.
- The separate background warm-up, its status bar label and the
  EDEN_NX2_COMPUTE_WARM / EDEN_NX2_GRAPHICS_WARM overrides are gone, as in
  nx2.
- "Shader warm-up threads" sets the precache thread count: 0 (default) uses
  every CPU thread as nx2 does; EDEN_NX2_WARM_THREADS still overrides it.
- nx2's markers, Ask and Skip requests, precache-graphics.bin and archive
  discovery (zortos293#812) are kept; the markers also record the shader calls mode,
  so a marker written by nx2 runs the precache once more.
- warm_records keeps the graphics records and the render target rule for
  the draw state template.
nx2's precache (zortos293#781) now covers the loading-screen warm-up and the driver
cache save; this keeps only the link mode in the graphics warm-up marker.
@MiguelRogelio

Copy link
Copy Markdown

Thanks @PantACRO4life, and everyone who worked on #579 and #741! I tested them on Windows with an AMD RX 7600 XT (RDNA3), an i7-8700, 16 GB of RAM and Adrenalin 26.10.1, using a local MSVC build of #717 + #741 merged together.

  • eden-cli --build-warm read 1,458 archives in 13 s and wrote 3,996 compute + 15,357 vertex + 21,447 fragment entries (38 skipped).
  • The loading-screen warm-up works: 843 compute programs took about 1 min on 12 threads, and the graphics entries took 20 min on 6 threads (36,202 built, 602 failed). After that, 21 minutes of play in Kokiri Forest averaged 40.5 fps, with about 5 frames per minute over 100 ms (worst 636 ms).
  • Something worth knowing for 16 GB machines: with a system-managed page file the commit limit was 32.7 GB. The boot warm-up on all 12 threads, plus the 14 GiB guest backing, ran the system out of memory, the same root cause as [Crash] OoT (0400486024DEC000): 8 compute shaders rejected on RX 6750 GRE, unoptimized table only covers 2 #462/vulkan: explain out-of-host-memory pipeline failures (Windows commit limit, #462) #807. Enlarging the page file (limit 44.9 GB) fixed it, and I also capped EDEN_NX2_WARM_THREADS=6. It might help if the boot warm-up lowered its thread count when free commit is low.
  • About 600 graphics entries failed translation with "Invalid cached native indirect branch address". They're skipped and do no harm; mentioning it only in case it's useful.

Also, thanks to Archi, Tekka and the TuxHub community for their help and for the idea of preloading the shaders.

Conflicts, both from the split back thread draw work (zortos293#858):
- vk_graphics_pipeline.h: kept this branch's members (source words, storage writes, held
  draw state) and nx2's fast draw members (TakesDeltaDraws, ConfigureDelta, CaptureNextDelta)
  side by side.
- vk_graphics_pipeline.cpp: took nx2's ConfigureDelta and kept this branch's ConfigureDraw
  comment. ConfigureDelta checks has_pipeline instead of pipeline, as ConfigureImpl does here,
  since the scheduler thread may swap pipeline (fast first build, optimized link).

Delta draws still end in ConfigureDraw, so the held and skipped draw handling of unbuilt
pipelines applies to them too. The precache compute memory release (zortos293#878) merged cleanly and
runs after this branch's compute module wrapper.
No conflicts; this branch still adds only the link mode to the graphics warm-up marker.
@PantACRO4life

Copy link
Copy Markdown
Contributor Author

@MiguelRogelio thanks for the thorough test, and good to see it combines with #717.

  • Memory on 16 GB: agreed. The thread count today doesn't look at memory (the default uses every CPU thread; EDEN_NX2_WARM_THREADS or the setting overrides it). Lowering the thread count when free commit is low is a good idea; I'll do it as a follow-up so it doesn't hold up this one. Until then, your EDEN_NX2_WARM_THREADS=6 is the right workaround.
  • The ~600 "Invalid cached native indirect branch address" failures: those are programs with an indirect jump whose target the warm-up can't resolve from the file alone. They're skipped and get built the normal way the first time the game draws them, so nothing is lost except the warm-up for those few.

Both #579 and this PR are now merged with the latest nx2 (3153d96). The only conflicts were with #858 in vk_graphics_pipeline; CI is running.

ThatGuyMcd added a commit to ThatGuyMcd/Lichii-TGM that referenced this pull request Nov 9, 2026
…e ownership

Finish the zortos293#656/zortos293#761/zortos293#579 shader stack without replacing zortos293#1021 recorded recipes.
Add shared-function SPIR-V controls on both prepared and translated paths, optional
fast-first builds, layout-correct motion bindings, cancellation/reload ownership,
and driver-persistent mesh warm markers. Preserve compute retirement and bounded
prebuild capture budgets. Keep new performance timing diagnostics opt-in.

Sources: zortos293#579 6724498;
zortos293#741/zortos293#579 7869334 and later graphics controls;
zortos293#761 ae537ce reload, warm persistence and conversion review fixes.

Validation: targeted Windows clang-cl syntax checks; standalone SPIR-V inline
control tests pass (5 cases, 25 assertions). Full linking and gameplay validation
remain the parent integration's responsibility. The frontend syntax check needs
the main build's generated ui_main.h, which is not present yet.

Co-authored-by: PantACRO4life <6643071+PantACRO4life@users.noreply.github.com>
Co-authored-by: DJ Lynch <daniel.lynch2016@gmail.com>
Co-authored-by: Ewan Betts <ewan.betts@southernforestsvet.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.

4 participants