Skip to content

Sync with upstream Lichii nx2 (d549411) - #17

Merged
NxtDoorActivity merged 103 commits into
nx2from
chata/sync-upstream-3
Nov 7, 2026
Merged

NxtDoorActivity merged 103 commits into
nx2from
chata/sync-upstream-3

Conversation

@NxtDoorActivity

Copy link
Copy Markdown
Owner

Merges 101 upstream commits, including the CPU-side performance work (zortos293#412, zortos293#413, zortos293#414, zortos293#178), the white-flash fix (zortos293#215), the stale reticle fix (zortos293#397) and the SparseLargeVector crash fix (zortos293#485).

Three conflicts, all branding or text: README.md, the desktop file (kept Chata name and keywords, took upstream's new MIME types), and the startup log line in main_window.cpp. No conflicts in the Vulkan or GPU code.

Updates UPSTREAM_BASE_SHA in the chata-linux workflow.


Generated by Claude Code

zortos293 and others added 30 commits November 6, 2026 19:42
…os293#287)

* frontend: fix Linux AppImage switch discovery and package defaults

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

* frontend: constrain adjacent switch files and reject relative config roots

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

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* qt: add consent-first desktop compatibility reporting

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

* tests: compare HTTP header names case-insensitively

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

* qt: preserve report state and prefill active GPU identity

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

* qt: prefill report title and enabled add-ons

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

* qt: report the loaded NUP package version

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

* qt: preserve literal characters in catalog searches

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

---------

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

* core: add the run walker the pending-write port uses

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit eb6a2de)

* buffer_cache: don't download GPU data over newer CPU writes

Ported from Duck912/Eglantine-ducky e27fbc6b03 (Duck912). CPU writes into
GPU-written memory are collected and applied later by the GPU thread;
until then two paths wrote the GPU's older copy over them: the fencing
thread's async download pops, and applying the collected writes one by
one (WriteMemory downloads the GPU bytes elsewhere on the pages it
touches). In Ocarina this made the Z-target reticle use values from two
frames before on about half the frames.

Every download write into guest memory now skips the collected,
unapplied CPU writes (GPUDirtyMemoryManager::ForEachPending, which here
also covers writes a Gather is handing out), and InvalidateGPUCache hands
the collected writes to the rasterizer as one batch that is taken out of
the GPU-written ranges before any is applied. This tree has more download
paths than the original (DownloadMemoryRanges, DownloadRangesMemory); all
eight writes go through the same check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 0c82282)

* tests: downloads keep CPU writes collected and not yet applied

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

---------

Co-authored-by: Frizti <FriztiMouche@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* shader: gather from a second descriptor buffer as TLD4.B

From MateusAugustoBrito/Lichii a04f756 (MateusAugustoBrito). A gather
whose handle comes from another descriptor buffer than the program's
loads it with MOV and emits Maxwell TLD4.B, as TEX already does, instead
of rejecting the program. Programs with one descriptor buffer lower as
before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 794233f)

* tests: gathers from a second descriptor buffer lower to TLD4.B

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

---------

Co-authored-by: Frizti <FriztiMouche@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…tos293#185)

* texture_cache: land deferred clears before non-pass image writes

A deferred clear is parked until its framebuffer's next render pass, but
texture cache copies bypass render passes: the clear then realizes after
the copy and wipes what the copy wrote. On hardware the clear executes
at its stream position, before the copy, so the copy wins.

That is the Ocarina of Time terrain layer (zortos293#101): the game clears the
occlusion depth, blits the half-res scene depth over it, then the layer
draw samples it. The parked clear landed after the blit from the second
frame on (GetFramebufferId only flushes when it creates a framebuffer),
the map read 1.0 everywhere, and the layer drew over the scene. It
matches the workaround that dropping the Z-only clears before the blit
removes the blotches.

BlitImage, CopyImage and PrepareDmaImage now realize a parked clear
before writing; draws keep the load-op fusion.

* texture_cache: only land deferred clears when the write proceeds

A rejected copy writes nothing and must not realize a parked clear ahead
of the next draw that consumes it as its load-op. Move the flush in
BlitImage after GetBlitImages (safe: framebuffer removal flushes first),
after CopyImage's early returns, and gate PrepareDmaImage's on the DMA
write actually happening (mark_as_modified).

Review note from RomainROCH on zortos293#185.
…SURED register shadowing (zortos293#149)

* vulkan: submit deferred work before waiting on pending fences

On the split GPU thread, Scheduler::DeferFlush holds fence submissions back to batch them; the
back submits them only when idle or asked between entries. WaitPendingFences(force) on the back
queued a fence and blocked on the fencing thread, which waited for that fence's submission:
Ocarina of Time froze on a black screen as gameplay began. Submit deferred work before waiting.

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

* present: interpolate between the previous and current frame in FSR3 frame gen

Process() copied the new frame into both the history and current images after the flow, so every
generated frame blended the newest frame with itself; the interpolation also applied the motion
vectors with the wrong sign and as full-resolution texels. Text over moving backgrounds was mixed
with its own drop shadow (black title menu options in Ocarina of Time).

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

* shader: shadow the data registers of RED and SURED

The register shadowing check reads bits 32-39 only for register-form
opcodes and a short exception list. zortos293#100 added ISBEWR (0x927) for
Ocarina's door NaN. RED (0x98e) and SURED (0x59f) take their data
register from bits 32-39 as well, and RegisterPressure already lists
operand 32 for both, but they were never asked: a data register at or
above the uniform bank read Lichii's UR mirror instead of the guest
value. Add them alongside ISBEWR.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…e rejecting (zortos293#241)

A native program whose own registers reach into the high uniform mirrors
(R(216 - n) and up) has those registers shadowed by PrepareRegisterPressure,
which works in aligned groups of four. n counts in pairs, so the base could
be misaligned (R202 for n = 14) and the program was rejected with "native
program uses lowering scratch registers" even though shadowing supports its
instructions. Request one more lowering pass with n rounded up to a multiple
of four instead; programs that lowered before are unchanged.

Also shadow the register form of SHF (0x219): its destination, low word,
shift amount and high word, like 0x210-0x212. Programs with it there were
rejected with "native register shadowing mode".

Shadow LOP3.LUT with a constant buffer or uniform B (0xa12, 0xc12) like its
immediate form (0x812) too: Rd, A and C are its only registers.

ampere_ported_uniform_vote expected the old rejection for a native R214 inside
the UR28 mirrors; it now checks the realigned result instead: UR28 read at
R212 and the native move written to its shadow, R210.

In Ocarina of Time both rejected pixel shaders of the Gerudo Valley rock
draws, so the rocks were skipped and the sky showed through.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…rray index against the innermost array (zortos293#203)

* fix(file_sys): apply LayeredFS mods to NUP titles

AppLoader_NUP reports the RomFS as not updatable (update already merged),
so RomFSFactory returned the raw file and user/load/<title>/<mod>/romfs
layers were never applied. Run PatchRomFS without an NCA: the update step
is skipped and LayeredFS still runs. No mods present = same file as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(config): assert nested array index against the innermost array

SetArrayIndex compared the index with the outermost array's size, so a
nested array (DisabledAddOns/<n>/disabled with more than one entry) hit
the assert on every entry past the outer size. Reading still worked; only
the log filled with Critical asserts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…arina white flashes) (zortos293#215)

* video_core: flush the query cache before macro DMA reads under the GPU split

Macro DMA reads of report regions flush the query cache so occlusion
reports and semaphore writes land before the read, but the flush was
gated on the split GPU thread being inactive. The default configuration
(EDEN_NX2_SPLIT=2) therefore served stale counts to macros whose own
comment warns that stale values make 'occlusion predicates read stale
values' -- the guest reads a zero visibility count and culls visible
objects, which matches Ocarina of Time's missing vegetation (zortos293#101) and
the count=0 draws traced there.

The native macro path now flushes under the same owns_caches condition
as its stale-read trace, and the split front path (FrontDmaRead) flushes
on the back next to its buffer cache flush. SemiFlushQueryDirty writes
out finalized values directly and waits out the rest through the fence
manager (WaitPendingFences, hardened in zortos293#149).

Build-checked and unit-tested on Linux; gameplay validation needs the
EDEN_NX2_SPLIT=0 vs 2 vegetation A/B described in zortos293#101.

* video_core: wait for pending query results before macro DMA reads

The query cache flush before macro DMA reads asks the fence manager to
wait out pending reports, but below High GPU accuracy a fence runs its
callback at once, so the wait returned before the report writes landed.
The macro still read the old occlusion counts. With the GPU split's
EDEN_NX2_QUERY_DMA_TRACE on, Ocarina of Time (0400486024DEC000) showed
976 such reads in 2.5 minutes, each reading a different value than the
one that landed later, and the scene flashed overexposed for a few frames
whenever the camera moved (cutscenes or the stick).

The macro paths now flush through FlushQueryResults: when the region
holds pending reports, Vulkan queues an operation behind them, signals a
fence, submits work the split's back deferred, and waits until the
operation has run. Other ReleaseFences callers are unchanged.

Build-checked on Linux (clang, Release). Gameplay tested on NVIDIA, Low
GPU accuracy, GPU split active: the flashes on camera movement are gone.
Not run: unit tests, AMD, High accuracy.

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

---------

Co-authored-by: Gavin Schiller <gavinschiller51@gmail.com>
Co-authored-by: WeslieDE <weslie@weslie.tk>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ortos293#197)

* shader_cache: skip the cached rejection path for compute dispatches

A rejected program fails the same way on every dispatch, but ComputeShader()
rethrows the recorded exception and then records it again: two C++ throws, a
mutex-guarded insert and a current_exception() each time the program is
dispatched. GetGraphicsEnvironments already skips the equivalent draws for
exactly this reason ("two C++ throws each time cost ~1-2% of the GPU thread"),
so give the compute path the same treatment.

Return a missing shader instead, which CurrentComputePipeline() in both
renderers already handles, and keep the first exception recorded for an
address rather than capturing a new one on every rejection.

Reported against build-75 (c9ebb5c): Ocarina of Time rejects compute programs
with "native texture gather mode" and "native per-tap gather control-flow
proof" and then dispatches them repeatedly, so the compute path paid this cost
per dispatch rather than once.

* renderer_opengl: report a null native compute pipeline as a failed dispatch

Native mode is enabled from the command stream (ChannelState::Init, gated by
g_nx2_ampere), not by the renderer, so the OpenGL rasterizer can reach the
cached-rejection path that now returns a missing ComputePipeline. Returning
silently there makes KeplerCompute::DispatchNativeCompute treat the dispatch
as successful, which skips its rejection bookkeeping (including
dlss_chain.rejected) and reports no QMD failure.

Match Vulkan and throw instead, as both backends already do for a native
compute pipeline that cannot be built.

Reported by CodeRabbit on zortos293#197.

---------

Co-authored-by: axsddlr <axsddlr@users.noreply.github.com>
…GPU back thread (zortos293#412)

* vulkan: look the draw's pipeline up once

UpdateDynamicStates looked the current graphics pipeline up again for each draw
(~2.5% of the split's back thread in Ocarina); PrepareDraw now passes the one it
found.

Ported from Frizti/Lichii 23f40d5 without its tools/nx2/sample_report.py
change (nx2 has no sampling profiler report).

(cherry picked from commit 23f40d5)

* common, query_cache: skip range subtractions and query erases that touch nothing

RangeSet::Subtract looks the range up first: the buffer cache clears several sets on every
write, and most hold nothing there. IterateCache only erases a page's queries when one was in
the range. About 3% of the split's back thread in Ocarina of Time (Lake Hylia).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit da215e8)

* video_core: skip resetting untouched buffer usage trackers

Port the tracked-state guard from sylvieistaken/selevet-eglantine d8c95fe. Keep page and recent-range clears after every Track while avoiding repeated clears for idle buffers. Preserve existing tracker ownership and reset semantics.

(cherry picked from commit b16ba4f)

* tests: cover RangeSet subtraction and skipped usage tracker resets

RangeSet::Subtract now returns early when nothing intersects the range, and
UsageTracker::Reset skips trackers nothing tracked since the last reset.
Check both against reference models: a per-address bitmap for the range set,
and the always-clearing tracker for resets, including back-to-back resets.

---------

Co-authored-by: Frizti <FriztiMouche@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…r-GPU-written pages (zortos293#413)

* vulkan: skip the buffer cache lock for dirty checks on pages the GPU never wrote

The split's front asks MustFlushRegion before each macro DMA read; it waited for the
buffer cache mutex the back holds through each draw (8% of the front in Ocarina).
Every GPU-modified range also marks its pages GPU-written (bits that never clear), so
pages without the bit are answered without the lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 6247fd5)

* video_core: answer the lock-free flush check from nx2.h and test it

Move the GPU-written page walk behind MustFlushRegion's lock-free answer into
Tegra::Nx2MayBeGpuWritten next to the bitmap it reads. A range past the bitmap,
or one that wraps, now answers true and takes the lock: Nx2MarkGpuWritten clamps
to the bitmap, so those pages were never marked. Test the page boundaries, the
bitmap end and clearing.

---------

Co-authored-by: Frizti <FriztiMouche@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…93#414)

From Hydro74000/Edlws 034668a14e (Hydro74000). The old buffer is kept
until the GPU is done with the current tick instead of finishing the
GPU, and the buffer grows by at least 1.5x.


(cherry picked from commit a20c9a9)

Co-authored-by: Frizti <FriztiMouche@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…(OoT near foliage) (zortos293#410)

* shader: bind loaded pointers that compete with a paired cbuf pointer

A native global access whose address is a select or phi of a constant
buffer pointer and a pointer loaded from that buffer was paired with the
constant buffer pointer alone, so the native candidate search never ran.
Accesses through the loaded pointer were then rebased on the constant
buffer's binding and read out of bounds as zeros.

Ocarina of Time reads its foliage unit placement records from c0[12] or
from *c0[12]. The near-leaf pass (539ffb3f650a34ac) read every record as
zero, normalised a zero vector and wrote NaN positions, so the near
ShootLeaf meshes (BancLeafMesh, FoliageLeafNear) were never built and
trees lost their leaves inside about 20 m.

Run the candidate search for paired accesses too. If a loaded pointer
competes with the pair, bind every candidate plus the paired pointer and
let the backend pick the buffer at run time; otherwise keep the pair.

Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com>
(cherry picked from commit a51a69f)

* tests: bind both a cbuf pointer and the pointer loaded from it

A native access whose address is a select or phi of c0[0x30] and a
pointer loaded through c0[0x30] binds both buffers and stays global, as
Ocarina of Time's near-leaf pass 539ffb3f650a34ac needs.

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

* shader: keep the pair when competing loaded pointers cannot be bound

Binding the loaded pointers that compete with a paired cbuf pointer
leaves the access global. The SPIR-V backend cannot route global subword
accesses or most global atomics, so those accesses keep their pair.

The loaded pointers are bound without the table budget, so a program
already at 32 storage buffers overflowed the descriptor vector. When
they leave too few bindings, collect again keeping the pairs.

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

* shader: keep the pair without 64-bit integers

The SPIR-V backend routes no global access among bound buffers without
shaderInt64, so a paired access left global would read zeros or fail to
translate.

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

---------

Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com>
Co-authored-by: Duck912 <350528965+Duck912@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
… CPUs on Windows (zortos293#232)

SetCurrentThreadToPerformanceCores(), SetCurrentThreadToEfficiencyCores() and the other
placement calls only did something on Android. On Windows a hybrid CPU (Intel P and E
cores) could run the CPU core threads, the GPU front and back threads, the Vulkan
worker, HostTiming and the fencing thread on an efficiency core, where a frame takes
noticeably longer, while the texture decoder and pipeline builders competed for the
fast cores.

Read the CPU sets with GetSystemCpuSetInformation, take the highest EfficiencyClass
as the performance group, and assign each thread its group with
SetThreadSelectedCpuSets (a soft affinity the power manager respects, unlike affinity
masks). Non-hybrid CPUs (one efficiency class) are unchanged; EDEN_HYBRID_CORES=0
turns it off. Both APIs are looked up at run time so older Windows builds still start.

On an i9-13900K (8 P + 16 E cores) the time-critical threads now show the 16 P-core
CPU sets selected and the texture decoder the 16 E-core sets (verified from outside
with GetThreadSelectedCpuSets). Ocarina of Time in Kokiri Forest measured within
noise of the previous placement (Windows already favoured the P-cores for those
threads in that scene), so this is parity with the Android placement and a guard
against the scheduler moving a hot thread, not a measured speed-up.
* video_core: fix GCC shadow error in pipeline cache

The deferred shader pool local reused the name of the CreateGraphicsPipeline parameter. GCC treats this as a shadow warning, and Linux builds with ENABLE_WERROR=ON fail because -Werror=shadow promotes it to an error. Rename the local while keeping pool release and reuse unchanged.

* tests: fix GCC shadow errors in Linux build

Recent config, audio, and video tests introduced local names that shadow inherited members or the global Result type. With ENABLE_WERROR=ON, GCC treats those warnings as errors and stops the full build. Rename the test locals without changing their behavior.
MSVC builds with warnings as errors stop on C4388/C4389 in
nx2_debug_server.cpp (std::optional<u64> == int) and
buffer_cache_ports.cpp (u32 == int).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…93#167)

swscale defines only the visible width of each output row: the bytes up
to the stride are spill from its vector stores and can differ between
two identical conversions. The scaler-path tests compared full pitch
rows, so two of them fail on FFmpeg 9.0 with 24 differing bytes that are
all in the chroma row padding past the output width (the visible pixels
are bit-identical). Compare the visible pixels instead. The hand-built
copy-path expectations stay byte-exact.

Build-checked and tested on Linux with FFmpeg 9.0.2: all 674 test cases
pass, including the two that failed before.
)

The comment said =2 paces submission two frames ahead. PresentClientBuffer
reads only whether the value starts with '1': 1 waits for the previous
frame's fence, anything else for this frame's. So =2 removes the overlap
the comment promised, and in Ocarina of Time it cost about a fifth of the
frame rate.


Claude-Session: https://claude.ai/code/session_01RqE9yWsiiDmjnKWMhij7HM

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
In NPad::OnUpdate the JoyconDual case built the attribute bit by bit:
is_connected, then is_left_connected and is_right_connected under an if
each. GCC 13 at -O3 (the Linux release build, Ubuntu 24.04) keeps only the
last of the two conditional assigns: with both Joy-Cons it writes 0x11
instead of 0x15, so the game sees the left Joy-Con missing.

Ocarina of Time (0400486024DEC000) then opens the controller applet
(raw id 0x100000c) about 40 times a second and never takes input, while
the controller settings show the pad working. Build 93's binary has the
two constant stores (5, then 0x11) and a lone g++ 13.3 -O3 compile of
npad.cpp reproduces them.

Build the value once and copy it to the libnx state, as Orchid did for
the same loop in Drag x Drive (MythosOfs/Orchid 3c0ca62).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…3#159)

OpenDataStorageWithProgramIndex only looks in the content provider, which has no
entry for a NUP container or a loose dump, so games using it abort at boot.
Return the RomFS the loader prepared when the lookup finds nothing.

Co-authored-by: Walnutoast830 <Walnutoast830@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…os293#279)

Ocarina of Time rejected two native pixel programs at the Kakariko
graveyard with "native register shadowing mode" on IMAD R229, R29, UR10,
RZ and IMAD R225, R49, UR10, RZ (opcode 0xc24), and their draws were
skipped. The destinations sit in the uniform bank mirror range, but
PrepareRegisterPressure had no operand list for 0xc24. Its register
operands match 0xa24 (Rd, Ra and Rc; bits 32 to 39 name a uniform
register), so it now shares that case.
…ortos293#329)

The required-subgroup-size create info was built for graphics stages but
never chained, so drivers chose the subgroup size themselves. Mesa's Intel
driver runs mesh shaders 16 wide (it reports 16 to 32, with task and mesh
in requiredSubgroupSizeStages), which splits the guest's 32-thread warp
and breaks the votes, ballots, shuffles and broadcasts the translated
programs use.

Ocarina of Time draws its vegetation with task/mesh programs: one pair for
the depth prepass and another for the colour pass, which tests depth EQUAL
against a 2D-engine copy of the prepass depth. With split warps the two
programs placed the leaves differently, so the colour pass left leaf-shaped
holes that showed the sky behind them, moving every frame with the wind
("white flashing leaves").

Chain the create info for every graphics stage that uses warp-level
operations when the device can force the guest warp size for that stage,
as compute pipelines already do. Quad shuffles and FSWZADD are left out:
they only span four lanes, and forcing SIMD32 on Intel can spill.

Kokiri Forest by the ladder, Arc B390 (Mesa), Low accuracy: leaf holes
went from 7-10% of the lower screen in every frame to 0 in 40 frames.

Assisted-by: Claude Opus 5.5 (diagnosis, fix)
zortos293#353)

* vulkan: don't write render target components the fragment shader doesn't output

A fragment shader writes the render target components its header lists.
The guest leaves the other components of a bound target untouched, but a
Vulkan pipeline writes undefined values to attachment components that
have no shader output, and AMD writes whatever its export registers hold.
In Ocarina of Time a depth-only visibility draw with RT0 still bound left
a band of NaN and saturated red/blue pixels above the horizon, which the
bloom spread into violet halos.

The pipeline's write mask, and the dynamic one with extended dynamic
state 3, now leave out the components the fragment shader doesn't write.
The dynamic mask takes the pipeline PrepareDraw binds, and is recorded
again when the bound pipeline outputs a different set of components.

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

* vulkan: invalidate the scheduler state after a texture blit to a framebuffer

BlitColor from a bare image view binds its own pipeline in a render pass
of its own but, unlike the other blits, left the scheduler's state as it
was. The next draw with the pipeline bound before it skipped binding that
pipeline again, and every draw skipped recording the dynamic state the
blit's pipeline replaced, such as the color write mask, until something
else marked it dirty. DrawTexture and the DLSS output use this blit. It
now invalidates the state as the other blits do.

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

---------

Co-authored-by: jpkto <255363096+jpkto@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
zortos293 and others added 29 commits November 7, 2026 15:20
…#520)

* ci: build and test with clang-cl on Windows in build-check

build-check had no Windows job, so MSVC and Windows-only breakage went unnoticed until a release run. The new job builds the production targets and tests with the release compiler (clang-cl from LLVM 22.1.8, without ThinLTO) and runs ctest, with sccache and the CPM cache. The pinned LLVM install moves to tools/windows/install-llvm.ps1 so both workflows use one version and checksum.

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

* ci: spell the build-check clang-cl flags with dashes

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

* build-check: make the Windows clang-cl tests pass and keep their cache

Bundled zlib registered its example tests, which cannot find an installed
package, so turn ZLIB_BUILD_TESTING off. The NUP metadata-edit test held
the archive open while the edit renamed over it, which Windows refuses.
Save the sccache cache even when tests fail.

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

* tests: keep the microphone capture stream off the stack

SinkStream holds several megabytes of inline queues, which overflowed the
1 MiB Windows main stack. Allocate it on the heap like the sysmix tests.

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

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…ts were null, bind up to 64 storage buffers (OoT Hyrule Field grass; imports zortos293#395, zortos293#421, zortos293#434) (zortos293#551)

* shader: charge pointer table entries only for bindings the first pass left free

Pointer table entries are bound in the storage buffer bindings that the
pass without them leaves free. The second pass charged a table entry's
holder to that budget whenever it was not in the set yet, but the holder
is usually a buffer the first pass bound, and so already left out of the
budget: charged twice, it shut out other tables. Ocarina of Time's
foliage culling compute 331e607152ce78d5 bound 28 of 32 storage buffers
and 4 of its 8 per-type instance tables, and a type whose table is not
bound is culled whole. Only buffers the first pass did not bind take
from the budget now: it binds 32 with all 8 tables (541ba22d44898196: 7
tables, was 6). Should the second pass still exceed the 32 bindings, the
program keeps the first pass's buffers rather than failing to build.

Two tests cover the charge and the fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 4ff805a)

* shader, buffer_cache: bind up to 64 storage buffers per stage

A native (Switch 2) program takes a storage buffer for every raw pointer
it reads, and 32 bindings are not enough for all of them. Ocarina of
Time's foliage culling computes need 33 in Hyrule Field and 37 with
Castle Town's leaf lists bound too. Past the cap the pass leaves pointer
table entries out, the per-type instance tables, and every instance of a
type whose table is unbound is culled.

Raise the cap to 64, which takes the buffer cache's storage binding
masks to 64 bits, and keep each program within the device's per-stage
storage buffer limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit f25c551)

* vulkan: rebuild native compute pipelines whose pointer slot was null at translation

Native programs only bind a constant buffer pointer as a storage buffer when
it pointed to mapped memory while the program was translated, and that
answer is stored in the pipeline cache. Ocarina of Time's foliage culling
compute was first translated while two list pointers (c4[0x90], c4[0x130])
were still null; its list writes were dropped for good and the near hedges
in Castle Town kept only their bare branches (Lichii zortos293#201).

Remember the slots that were null at translation and rebuild the pipeline
once such a slot holds a mapped pointer. Replaced pipelines stay alive for
work in flight. When the cache holds both translations, keep the later one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 30e43ca)

* vulkan: keep the cached compute translation that binds more pointers

Which of a native program's pointer slots are filled depends on the area
(Castle Town and Hyrule Field fill different list slots of Ocarina's
foliage culling compute), so the later cache entry is not always the
better one. Prefer the translation that bound more pointers and keep the
earlier entry on a tie, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 15ff6cb)

* vulkan: keep the old compute pipeline when its rebuild fails

A rebuild can fail where the stale translation worked, for example when
a narrow access now stays global across two mapped buffers. Keep the
working pipeline and stop retrying instead of losing the dispatches.

A cache reload loads entries while runtime entries exist; keep those and
only choose between duplicates read from the file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 1ba80f0)

* vulkan: choose between cached compute translations before building them

Replacing an already loaded entry could destroy a pipeline that a
dispatch was using, when loading overlaps guest work (cache reload, CLI
startup). Read all compute entries first and order the duplicates of a
key: more bound pointers first, the earlier entry on a tie. One worker
per key builds the first candidate that translates and only adds it, so
an existing entry is never replaced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 1e22e5c)

* vulkan: rebuild compute translations without dropping the pointers they bind

A rebuilt translation binds the pointer slots that are filled when it is
translated, so a rebuild in one area dropped the slots another area
fills: Ocarina of Time's foliage culling fills different list slots in
Hyrule Field and Castle Town. The load then preferred whichever cached
translation bound the most pointers, which need not bind those the first
translation did.

A rebuild now keeps binding every pointer the old translation binds (a
null pointer binds nothing), and the load tries first the cached
translations that bind every pointer the first one binds, those binding
the most first. With all of them bound, Castle Town's hedges keep their
near leaves and Hyrule Field's grass keeps its per-type tables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit dfc7211)

* tests: hold 64 storage bindings in the binding capacity test

The stages bind up to 64 storage buffers now, past what a 32-bit mask
holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 16551ff)

* shader: reject programs that read more pointers than a stage binds

Only the retries for competing loaded pointers and for pointer table
entries kept a program within the stage's storage buffer limit. A
program reading more direct pointers than that got a pipeline layout
past the device's limit, or, past 64, a bad_alloc from the descriptor
list, which the pipeline caches don't catch. Reject such a program with
a translation error instead: binding only some of its pointers would
drop the accesses through the others.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit a695c14)

* vulkan: free replaced compute pipelines once the GPU is done with them

A rebuilt compute pipeline's predecessor was kept until the cache was
destroyed. Retire it with the current tick and destroy it in TickFrame
once the GPU has passed that tick, as evicted graphics pipelines are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 4d62d81)

* shader: count NVN buffers and the descriptor set limit against the limit

The check that a program stays within the storage buffers a stage binds
ran before AddNVNStorageBuffers appends the NVN fallback descriptors,
and it took only the per-stage limit. StorageBufferLimit now also caps
by the device's descriptor set limit, and AddNVNStorageBuffers rejects a
program the same way when an NVN descriptor it adds would pass it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit a676f10)

* vulkan: keep a replaced compute pipeline until its queued build has run

A runtime compute pipeline queues its build on a worker, and a dispatch
may build it first on the scheduler thread; the queued task still runs
later and reads the pipeline. Destroying a replaced pipeline once the
GPU passed its tick could free it before that task ran. The task now
marks the pipeline when it is done with it, and a retired pipeline is
destroyed only after both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit bec25b4)

* shader: keep a dual vertex program's storage buffers within the limit

MergeDualVertexPrograms joined Vertex B's storage buffers onto Vertex
A's without a limit, so the merged stage could pass the device's limit
or, past 64, overflow the descriptor list. JoinStorageInfo now takes the
limit and rejects the program as the storage buffer pass does; the
Vulkan and OpenGL pipeline caches pass their host's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 89f72df)

* shader_environment: keep the native shader hash comment on its function

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

* vulkan: keep a graphics pipeline's storage buffers within the descriptor set limit

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

* tests: give the table loop helper internal linkage (-Wmissing-declarations)

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

---------

Co-authored-by: jpkto <255363096+jpkto@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: win <atk3001@gmail.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…#221)

* file_sys: ignore IPSwitch lines that are not patch records

Text after a patch block, such as a mod's credits, was parsed as records
and written into the module.

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

* file_sys: require the IPSwitch value right after the offset

A quote or hex word later in free text still passed as a record.

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

* file_sys: require a closing quote at the end of IPSwitch strings

A line such as A "thank you was still written as a string record.

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

* file_sys: keep the opening quote active when stripping IPSwitch comments

An apostrophe inside a double-quoted value changed the quote state, so a
later comment was kept and a slash could cut the value.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
zortos293#160)

A ZBIC segment that failed to decompress only logged `assert r > 0`; the
loader then copied the previous segment's bytes into the code set and
booted a corrupt image that never presents a frame.

- DecompressDataZBIC logs zstd's error name instead of discarding it.
- LoadModule returns nullopt on failure, which the callers already report
  as ErrorLoadingNSO, and zero-fills a short segment instead of leaving
  stale bytes in it.
- zbic_compression.cpp is built with -fno-profile-instr-use: it runs once
  per module at load, and the aarch64 PGO build is the one that fails.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* dist: install the MIME package under the Lichii app id

The Linux install still shipped dist/dev.eden_emu.eden.xml to
share/mime/packages. That path collides with the file an installed Eden
package owns, so package managers refuse to install both. Each MIME type
also pointed at the dev.eden_emu.eden icon, which Lichii never installs,
so NRO, NSO, NSP and XCI files showed no icon.

Rename the package to io.github.zortos293.lichii.xml and point the icons
at io.github.zortos293.lichii, matching the installed desktop file,
metainfo and icon. The application/x-nx-* type names stay unchanged: the
desktop file's MimeType= line uses them, and shared-mime-info merges
duplicate definitions from several packages.

* dist: register NUP, NSZ and XCZ MIME types

The loader and the game list open .nup, .nsz and .xcz files, but the
MIME package only defined NRO, NSO, NSP and XCI. Desktops showed those
files without a type or icon and did not offer Lichii to open them.

Add application/x-nx-nup (matched by its "NUP\x01" header),
application/x-nx-nsz (a PFS0 container like NSP) and
application/x-nx-xcz, and list them in the desktop file's MimeType=.
* qt: don't resize a maximized window when a game boots

Resizing leaves the window marked as maximized at the new size, stuck in
the top-left corner until it is dragged.

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

* qt: keep a hidden render window hidden when resetting its size

showNormal() showed the render window of a stopped game again. Clear the
maximized state without changing visibility instead.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…s293#365)

* vulkan: disable installed Overwolf hooks before loading Vulkan

* docs: keep Overwolf startup notes limited to implementation and checks
A Switch 2 game paces itself on the vblank word in its present call: Lichii
holds each present until the next vblank of that word times the swap interval
(PaceToVblank), so the game cannot run faster than the rate it declares. Games
that declare 60 Hz therefore cap at 60 whatever the host display does, and
EDEN_NX2_VSYNC_HZ, which only drives the synthetic vblank event some titles
start frames on, does not change it (OoT ignores it: its word stays 0x1003c).

Add "FPS Limit" to the renderer settings (0 = off, up to 480): the paced
interval becomes the longer of the game's vblank interval and 1/fps_limit, so
the game itself runs at the requested rate instead of only the display.
EDEN_NX2_FPS_LIMIT=<n> overrides it for tests without touching a config.

The interval maths moves into PacedInterval so it is testable; the pacer tests
cover limits above, at and below the vblank rate.
The experimental pin_gpu_threads setting (zortos293#418) only applied Windows CPU sets
and was skipped elsewhere. On Linux, read each allowed logical processor's
package and core id (and cpu_capacity on hybrid CPUs) from sysfs, plan the
same way (the front and back on a physical core each, the guest cores on the
other performance cores) and apply it with pthread_setaffinity_np.
…tos293#495)

* vulkan: grow descriptor payload storage on demand after draining its worker

* docs: clarify bounded descriptor growth and format its tests
…ffer (zortos293#427)

Draws with no colour and no depth target take their framebuffer size from
the surface clip, which Ocarina of Time leaves at 32768x32768 after the
intro video. Apple GPUs allow 16384 (maxFramebufferWidth/Height), and
Metal loses the device on the larger attachment-less render pass
(kIOGPUCommandBufferCallbackErrorPageFault, or Internal Error 0000010d on
M1). Clamp the framebuffer width, height and layers to the device limits.

Constant buffers are bound from the stream buffer and shaders declare each
at 64 KiB, so a binding near the end of the buffer reads past it; the Metal
debug layer reports "argument c3[0] ... has space for 26624 bytes, but
argument has a length(65536)". Create the stream buffer 64 KiB larger than
the part that is handed out.

Based on the patch balderos posted in zortos293#237, which was tested on an M1 and
an M6. Adapted to current nx2: the layer clamp now applies to the layer
count the framebuffer is created with, and the clamp is a small header so
it can be unit tested.

Refs zortos293#237
…owing (zortos293#440)

* vulkan, texture_cache: fail an image that does not fit instead of throwing

MemoryAllocator::CreateImage threw vk::Exception once every fallback was out
of memory. On the texture cache path nothing catches it (the cached image is
built inside SlotVector::insert, which is noexcept), so an 8 GB RX 9060 XT
running Ocarina of Time died when the 1024x1024 RGBA8 decode image of an
ASTC to BC3 transcode did not fit (zortos293#342).

TryCreateImage returns a null image on VK_ERROR_OUT_OF_DEVICE_MEMORY or
VK_ERROR_OUT_OF_HOST_MEMORY instead; CreateImage still throws for every other
caller. On the texture cache path:

- A cached image without memory is left out of the cache: JoinImages erases
  it and returns no image, so lookups get a null view (blits fall back) and
  make it again later.
- TranscodeAstcToBcn frees its idle decode images and retries once; if that
  fails too it leaves the image to be uploaded again on its next use.
- The allocator counts images it could not make, and the collector runs at
  the critical threshold the frame after one, whatever the usage counts say.

Refs zortos293#342

* texture_cache: retry a texture whose image did not fit on its next read

VisitImageView kept the null view of an image that had no memory in the
channel's descriptor map, so the texture never came back once memory was
freed. Leave it out of the map and invalidate the table so the descriptor is
read and the image made again.

Refs zortos293#342

* texture_cache: retry only images that did not fit, after the collector ran

6a53fbd dropped every null view from the descriptor map and invalidated the
table on each visit. Drop it only when the allocator's failed image count
moved during CreateImageView, and leave the table alone: the collector's
deletions invalidate it, so the image is made again once memory was freed
rather than on every draw.

Refs zortos293#342

* texture_cache: re-read texture tables the frame after an image did not fit

A texture left out for lack of memory was only retried once some image was
deleted, which the out-of-memory collection does not guarantee. Invalidate
the channels' image tables after that collection, so such textures are made
again once a frame while images keep failing.

Refs zortos293#342
… (Ocarina ~11% faster in Kokiri village) (zortos293#446)

* gpu_thread: poll suspended channels every 50 us instead of every 1 ms

A channel suspended on a semaphore its CPU releases is polled with yields for 500 us, then with
1 ms sleeps. Ocarina of Time suspends ~330 times a second and its present waits for the GPU work
behind those acquires, so a release seen up to 1 ms late delayed the frame: in Kokiri village
its present waited ~17 ms a frame on the GPU fence. Sleep 50 us instead, with the GPU thread's
timer slack lowered on Linux so the sleep stays short.

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

* gpu_thread: back to 1 ms polls for a channel still waiting after 2 s

Long waits (menus, loading) don't need 50 us polls. The waits behind the frame time measured
in Kokiri village last ~1 ms.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…base (zortos293#245)

Handle programs record storage buffer offsets relative to the buffer address
aligned down to the host storage buffer offset alignment (StorageOffset), and
the buffer cache binds the buffer at that same aligned base. The CPU-side
evaluator read from the unaligned address instead, so when a storage buffer
address is not a multiple of the host alignment (16 on NVIDIA, up to 256
elsewhere) a storage-sourced handle is read from the wrong word.

Pass the device alignment through from both the Vulkan and OpenGL pipelines
and mask the buffer address the same way the shader does. For indirect
buffers the table entry is still read at its exact address and only the
address it holds is masked, as in the SPIR-V base_address. Adds a unit test
that reads the same program at alignments 16, 4 and 0, directly and through
an unaligned table.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Lets the shuffles of a group overlap; unrolling all of K16 raised register pressure and slowed large dispatches. Ocarina's DLSS menu frame went from ~18 to ~12.6 ms of GPU time on a 7900 XT.
…ose pages (zortos293#250)

* video_core: skip GPU-write invalidations of caches with nothing on those pages

The GPU's own cached guest writes (WriteBlockCached: constant buffer data, every draw) are
invalidated in the texture, buffer, query and pipeline caches at the next draw
(FlushCaching -> InnerInvalidation). The texture, query and shader caches only visit the objects
registered on those pages, and constant buffer memory holds none: each call still took the
cache's lock and walked its page tables, about 88k times a second in Ocarina of Time.

Each of the three now keeps a PageFilter: one bit per 4 KiB device page it ever registered an
object on (images, cached queries, shaders), never cleared. A write whose pages have no bit set
can't concern that cache, so InnerInvalidation skips it. The buffer cache is still always told.

Ocarina of Time, Kokiri Forest village, GPU accuracy Low, RTX 4090 + i9-13900K:
nx2 52.0-52.3 fps -> 53.6-54.2 fps.

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

* shader_cache: mark rejected native programs in the page filter

A program rejected before Register is never marked, so InnerInvalidation
skipped the shader cache for GPU writes over it and its rejection was never
cleared. Mark its start address, which is all the erase predicate matches.

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

* video_core: leave ranges past the page filter's space untracked

Marking one (a stand-in address of an image without CPU backing) set an
overflow flag that made the filter report every range as possibly holding
objects. Any already reports ranges past its space that way, so Mark now
ignores them and the flag goes.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The buffer cache makes about 30,000 RangeSet lookups and 18,000 subtractions
a frame in Ocarina of Time, 97% of the subtractions missing every range,
against 3,400 additions. The sets stay small: 35 ranges on average at an
add, 198 at a subtract that hits, 763 at most. A boost interval_set with
pooled tree nodes behind a mutex spent 5.0% of the GPU back thread on that.

Sorted, joined ranges in one array answer the same questions with a binary
search over contiguous memory and no allocation per range. Add and Subtract
keep the interval set's semantics, including the joining of touching ranges.
OverlapRangeSet needs counts per segment and stays as it is.
…3#333)

* nvdrv: size Switch 2 GPU page tables to the VA NVN is given

nx2:gpu reports [0x8000000, 0xC000000000) to NVN, but AllocAsEx built the
GMMU from NvRm's 128 GiB va_range_end. Nintendo GameCube - Nintendo
Classics mapped 0x2109ec0000 and PageTableOp indexed off the end of
entries[]. Size the tables to the advertised end on Switch 2, and refuse
a map that is still outside the address space.

* nvdrv: refuse a sparse destroy that runs past the GPU address space

PageTableOp leaves a range that crosses the address-space end mapped.
DestroySparseRegion still released that range's backing, so the pages
could be freed while the GMMU pointed at them. Reject the destroy
before the release. A map or unmap of the last valid page that crosses
the end leaves that page's translation alone.
…3#465)

Scenario: Switch 2 (native Turing) draws whose graphics pipeline is still
building are never skipped, so the GPU waits for the driver build. On an
RX 6700 XT (Zelda OoT Switch 2) the log showed up to 14656 ms of draws held
in a 10 s window and single builds of 16.7 s.

Add native_async_draws (Wait, Bounded, Skip; default Wait keeps the current
behavior) and native_pipeline_wait_ms (per-frame budget, default 8 ms), both
in the Qt advanced graphics settings. Bounded does a timed condition-variable
wait on the pipeline build up to the per-frame budget, reset in
PipelineCache::TickFrame, then follows Skip. EDEN_NX2_ASYNC_DISCARD=1 still
forces Skip. The decision is a pure function covered by a Catch2 test.

Measured with Bounded at 8 ms on the same machine: at most 229 ms of draws
held per 10 s window, 10 to 614 ms of bounded wait per window, and a
one-frame flash when the budget runs out.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…293#550)

* vulkan: don't evict disk-cache pipelines that were never used

A pipeline prebuilt from the disk cache at boot keeps last_use_frame 0 until
a draw uses it, so the first eviction pass after 9000 frames destroyed nearly
all of them and the game translated them again on the GPU thread when it
reached that content.

* vulkan: track first use separately from the frame number

frame_count starts at 0, so a pipeline used only on the first frame also had
LastUse() 0 and was never evicted. Mark first use with a flag instead.

* vulkan: cap the never-used prebuilt pipelines kept from eviction

The used list spans 16 sessions, so a large cache could keep thousands of
never-used pipelines resident for the whole session. Keep at most 2000
(~0.9 GB of driver memory); past that they are evicted like the rest.
…ter sizes (imports zortos293#478 by @mstampfli) (zortos293#580)

Imports zortos293#478 by @mstampfli.\n\nCo-authored-by: mstampfli <marc.kurt.stampfli@icloud.com>
… queued (imports zortos293#170 by @janvitos) (zortos293#585)

Imports zortos293#170 by @janvitos.\n\nCo-authored-by: janvitos <10037804+janvitos@users.noreply.github.com>
…OMG (imports zortos293#189 by @vouion) (zortos293#588)

Imports zortos293#189 by @vouion.

Co-authored-by: Gavin Schiller <gavinschiller51@gmail.com>
…12% standing, +24% walking) (imports zortos293#475 by @mstampfli) (zortos293#582)

Imports zortos293#475 by @mstampfli.

Co-authored-by: mstampfli <marc.kurt.stampfli@icloud.com>
…eneration loads (imports zortos293#230 by @PantACRO4life) (zortos293#587)

Imports zortos293#230 by @PantACRO4life.

Co-authored-by: PantACRO4life <6643071+PantACRO4life@users.noreply.github.com>
…a of Time 41 → 60 fps) (imports zortos293#521 by @estevanbs) (zortos293#583)

Imports zortos293#521 by @estevanbs.

Co-authored-by: estevanbs <estevanbartmann@gmail.com>
Brings in 101 upstream commits (including zortos293#412/zortos293#413/zortos293#414, zortos293#178, zortos293#215, zortos293#397, zortos293#485).
Conflicts were branding and text only: README, desktop file, main_window log line.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmvPX2tqfoEsDjDfWTAmSf
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmvPX2tqfoEsDjDfWTAmSf
@NxtDoorActivity
NxtDoorActivity merged commit d679d7f into nx2 Nov 7, 2026
2 checks passed
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.