Skip to content

Ocarina of Time on RX 9070 XT: #805 + #573 + #874 + #836 integration, 35 → 55 fps (draft) - #1069

Draft
lucastaonline wants to merge 114 commits into
zortos293:nx2from
lucastaonline:lucastaonline-oot-60fps-local-build
Draft

lucastaonline wants to merge 114 commits into
zortos293:nx2from
lucastaonline:lucastaonline-oot-60fps-local-build

Conversation

@lucastaonline

Copy link
Copy Markdown

Changes

Integration branch for Ocarina of Time (0400486024DEC000) on an AMD RX 9070 XT + Intel i9-14900K: several open PRs that had not been tested together, merged and measured with an automated benchmark. It is a draft to show the combined result and to help land the pieces; each piece belongs to its own PR and author.

Base: #805 at 34621dcd6b (@jpkto, decouple the split GPU threads). It is not merged into nx2, so this branch conflicts with current nx2 (mostly #965, #1028 and #949, which overlap #805).

On top of it:

Tried and left out (measured, no gain on this machine):

Testing

Machine: Intel i9-14900K, AMD Radeon RX 9070 XT (AMD proprietary 32.0.32015.7029 / Vulkan 2.0.416), 32 GB, Windows 11 25H2.

Build: clang-cl 22.1.8, ThinLTO, lichii-windows.profdata, the release job's flags plus -gline-tables-only (cmake + Ninja, --target yuzu). Unit tests were not built or run (BUILD_TESTING=OFF).

Settings: handheld, 1x, GPU accuracy Low, DMA Unsafe, fences Default, vsync FIFO, EDEN_NX2_SPLIT=2, EDEN_NX2_FENCE_BATCH=1.

Automated benchmark. Each run, scripted through the debug server:

  1. Launch lichii.exe -g <game>.
  2. From the title, load the same autosave. Link spawns on the Kokiri Forest path next to Saria, with the same camera every time.
  3. Wait 30 s.
  4. Take frame_times over 20 s, with EDEN_NX2_GPU_PROFILE=1.
  5. Capture a screenshot to check the spot, then close.

Two interleaved rounds per build:

Build fps p99 ms back thread µs/draw
build-125 release 35.3 32 8.9
nx2 a551b80888 34.3 35 9.2
+ #573 + #1028 37.5 31 8.8
+ #805 + #573 + #874 + #836 53.0 22 6.7
+ the clean cherry-picks above 54.1 23 6.5
this branch 54.8 24 6.4

That is about +55% at this spot. Most of it comes from #805 + #874 + #836. The later layers are within noise there; they bring crash, race and stutter fixes.

Verification. Two scripted sessions on this branch, walking and turning the camera:

Switch Checked Mismatches
EDEN_NX2_FAST_DRAW_VERIFY ~1.0M delta draws 0
EDEN_NX2_TEXTURE_MEMO_VERIFY ~2M memo draws 0
EDEN_NX2_DELTA_DESCRIPTORS_VERIFY ~780k entries 0
EDEN_NX2_UBO_KEEP_VERIFY ~2.5M bindings 0

Uniform keeps never trigger in this game (0 kept), so the #949 keep part is inert here.

Manual play. Several sessions in Kokiri Forest, no visual problems noticed. At the heaviest spot (the village water, ~3,800 draws a frame) fps went from ~35-42 to ~47-51 by hand measurement. That spot is still limited by the split's back thread at about 165-170k draws a second.

Not tested: other games, other GPUs and drivers (NVIDIA, RADV), Linux, Android, docked mode.

sanchodecuba11-del and others added 30 commits November 7, 2026 17:30
Measured on The Legend of Zelda: Ocarina of Time (0400486024DEC000),
Ryzen 7 5800X + Radeon RX 9070 XT 16 GB, AMD driver 32.0.32015.7029,
Windows 11. Each change was A/B tested in game behind a switch first.

- vk_query_cache: copy only the slots of the samples queries being
  synced and zero the gaps (the wide copy waited on slots no query
  ended, ~8 ms each on AMD); on AMD's driver, preset the synced slots to
  1024 samples and copy without WAIT_BIT, and read host results without
  waiting, so a late result counts as visible. 9 -> 22 FPS in Kokiri
  Forest, no vanishing scene.
- texture_cache: cap the collector at 3/5 of device memory when that is
  above 5 GiB. At 5 GiB a 16 GB card re-created ~10k images and
  re-uploaded 13-16 GB every 22 s; 14 -> 24 FPS at a busy spot.
- vk_pipeline_cache: skip native draws whose pipeline still builds by
  default on AMD's driver (EDEN_NX2_ASYNC_DISCARD=0/1 still decides):
  1-1.4 s freezes per new pipeline while running.
- pipeline_optimization: build compute e0a992208d87eeb8 unoptimized on
  AMD (1.3-4 s stalls).
- vk_rasterizer: submit every 512 draws on AMD's driver (128 elsewhere;
  EDEN_NX2_FLUSH_DRAWS still decides): 256 gave +10-12 FPS where it was
  already fast, 512 a further 35 -> 50 at one spot.
- decoders: split unswizzles of 1 MiB or more by lines over a quarter of
  the host threads (at most 4) plus the caller: +2-4 FPS running, +1-2
  standing; 6 threads or a 256 KiB threshold made busy spots drop harder.
- puller: take a semaphore acquire without draining the split back when
  the word in guest memory (on a page no GPU copy holds newer data for)
  already meets it: 73% of ~2800 acquires/s, 573k checked against the
  drained read without a contradiction; publishes and notes a sync
  point as the drain did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
- Drop the AMD default for native draw discard: a skipped native draw
  can write persistent resources; EDEN_NX2_ASYNC_DISCARD=1 stays opt-in
  (and evaluating the default per PipelineCache is moot).
- puller: the early acquire also requires that no release to the
  semaphore is still queued for the back. The front notes the address
  of each puller WriteLong and each 3D report release/report it sends
  (GpuSplit::NoteSemaphoreWrite / SemaphoreWritePending).
- texture_cache: derive the larger cap from the driver's memory budget;
  GetDeviceLocalMemory is capped at 7 GiB outside the aggressive VRAM
  mode, which left the cap at 5 GiB on 16 GB cards.
- decoders: request the helper threads' stop under the pool mutex and
  join them in the destructor, so a worker cannot miss the wakeup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review found queued writes the shortcut does not see (SEM_EXECUTE
releases, DMA engine clears, compute QMD releases). Covering them all
would leave it almost never taken for ~1 FPS, so the acquire drains as
upstream again; the pending-write tracking it needed goes too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On AMD's driver the samples queries were copied without WAIT_BIT over
slots preset to 1024, relying on the copy leaving unavailable slots
untouched, which the Vulkan spec leaves undefined. Copy them with
64_BIT | WITH_AVAILABILITY instead and resolve each slot on the GPU
(resolve_query_availability.comp, QueryAvailabilityResolvePass): its
count when it had landed, 1024 samples (visible) otherwise. Approach
from Savulska's test on zortos293#573.

RX 9070 XT, same MSVC build: 50 -> 56.7 FPS under the Deku Tree, 24 ->
25.0 at the slow Kokiri spot, the same in motion.
EDEN_NX2_QUERY_AVAILABILITY=0 keeps the preset method for A/B.

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

vk_query_cache: zortos293#679's wait on the newest query is applied to the synced runs
of each bank on the drivers that don't resolve availability (AMD open source);
AMD's proprietary driver keeps the copy without waits resolved from each
slot's availability. The texture cache cap is dropped: at 8 GB (5 GiB cap)
Ocarina of Time ran as fast as with ~9 GiB, and the earlier 14 -> 24 FPS
included the shader cache warming up.

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

The A/B switch against zortos293#679's copy is gone: on an RX 9070 XT (2.0.416, 8 GB
simulated) it gave 54 FPS under the Deku tree and a less steady run through
Kokiri Forest, this copy 56-58 FPS and a steadier run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The prefix scan of multi-slot queries sums every slot of the resolve buffer,
gaps between synced runs included; resolved as unavailable they read as 1024.
Mark them available with a zero count instead (CodeRabbit review on zortos293#573).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In Hyrule Field Ocarina of Time reads GPU-written memory about 80 times
a second (the "CPU read of GPU-written memory" count), and every such
read that GetFlushArea can't serve makes the reading thread wait for a
flush on the GPU thread. This measures how much of each frame those
waits take, to tell whether they hold the frame rate down.

One line, "GPU waits in 10.0 s:", always on:
- frames presented, and the time between presents (average, longest);
- reactive flushes: count, total and longest wait, per frame, by the
  cache that held the data (texture or buffer) and by guest core, split
  into queue (until the GPU thread starts the flush), flush (applying
  the collected CPU writes, then the downloads: the time spent in
  Scheduler::Finish waiting for the host GPU, how many flushes had
  nothing to download and how many bytes they copied back) and wake;
- the four areas that waited longest and, for buffer pages, the GPU
  writes that marked them in that window (what wrote them, the range,
  the largest one);
- the other sync operations guest threads wait for (CPU writes over
  GPU-written buffers, WaitForIdle);
- guest reads and writes that took the rasterizer path, by core, with
  the time of their cache lookups (GetFlushArea, OnCPUWrite), which take
  the buffer and texture cache locks the GPU thread holds while it draws;
- how busy the GPU thread and the split's back thread were, every
  Scheduler::Finish with its time, and every wait of FlushQueryResults
  for pending query results (macro DMA reads of report regions, zortos293#215).

Cost: a counter only its own core writes per rasterizer-path read or
write; two clock reads more per GetFlushArea or OnCPUWrite call; one
load per buffer write mark (four compares while pages are watched);
two clock reads and an atomic add per GPU thread idle period, per
Scheduler::Finish and per query result wait. The clock is the wall
clock's time stamp counter (steady_clock where it is not invariant),
the one EDEN_NX2_GPU_PROFILE uses.

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

A guest thread that reads GPU-written memory queues a sync operation and
waits for it. The operation ran at the next draw on the back, or when the
tick sent through the GPU thread's queue got there: behind every command
list the front still had, and then behind everything it had handed the
back. In Ocarina's Hyrule Field the front was busy 99.9% of the time and
these waits spent 1201.6 ms of 10 s in that queue, 1.17 ms a flush, before
the flush itself started (test38).

The guest synchronized with the GPU work it reads before it read, and with
the split every signal it can see (syncpoints, semaphore releases) runs on
the back after the work before it. So the back can run the flush at its
next entry boundary. GPU::OnCPURead asks it to (GpuSplit::RequestTick):
the back runs GPU::TickWork after the entry it is on, or wakes and runs it
at once when it waits for work. Without the split nothing changes, the
tick through the queue still runs it there. Both renderers also tick after
compute dispatches, as they do after draws, so compute work doesn't hold
a flush until the next draw.

The split test checks that a request wakes an idle back, runs between two
entries, and does nothing before the back runs.

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

Before the split's front fetches a command list on pages no GPU work has
written yet, it waited until its back had run every compute dispatch and
copy sent so far (DmaPusher::Step, dma_pusher.cpp:205; GpuSplit::
WaitForWriters), in case one of them writes the list first: the
GPU-written page bits (nx2.h:36) learn of a write only when the back
records it. In Ocarina of Time's Hyrule Field (EDEN_NX2_GPU_PROFILE, about
50 fps) these were about 7,500 waits a second at 19 us each, 142 ms of
every second of the front, which runs in near lockstep with the back. Nearly
all of them were for lists the CPU wrote: the lists Ocarina's culling
writes every frame are on pages already marked, which take the copy path
(ReadGpuWrittenCommands, dma_pusher.cpp:616) and still wait for their
writers there (line 642). Only about 220 lists a second take it.

The hardware gives a fetch no such ordering. The PBDMA fetches a GP entry's
commands ahead of the engines that run the work before it; only a
host-level wait in between (a GP entry's sync bit, a host WFI, a syncpoint
acquire) makes that work complete before the fetch. NoteSync marks those
(dma_pusher.cpp:138, puller.cpp:201 and 395), and semaphore acquires drain
the split anyway (puller.cpp:258). Lists on pages that draws write already
wait only for the work before the last of them
(BufferCache::Nx2ReadListCopy, buffer_cache.h:2208: NeedsSync, then
WaitForSync), and draws were never waited for here (IsMemoryWriter,
dma_pusher.cpp:37: compute and copy methods only).

So wait only for the compute and copy work sent before the last host-level
wait: GpuSplit::WaitForSyncedWriters waits for the earlier of the last
writer and the last sync. A list on fresh pages that work sent after the
last sync writes is read as the CPU submitted it, as the hardware could.
The split test checks that a list fetch waits for a writer before the last
sync, not for a writer after it, and not for other work before it.

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

Opt-in: with EDEN_NX2_DRAIN_CHECK unset, each site only tests a constant.
With it set, the GPU thread logs one line every 10 s:

  Drain checks in 10.0 s: command lists that did not wait for writers sent
  after the last sync N, GPU-written by those writers M | acquires at
  addresses the GPU never released A (met before and after the drain ...,
  only after ..., only before ..., neither ...), at ones it released B (...)
  | polls of suspended acquires at addresses never released C (...), at
  released ones D (...)

- Command lists on pages no GPU work had written that did not wait for the
  compute and copy work sent after the last host-level wait (the previous
  commit): at the list's place, the back checks whether that work made the
  list's pages GPU-written (DmaPusher::CheckUnwaitedWriters). M must stay
  0: a list it counts was read as the CPU submitted it although work before
  it rewrote it.
- Semaphore acquires and the polls of suspended ones (Puller): whether the
  value met the acquire before the front drained the split as well as
  after, by whether the GPU had run a release at that address before (any
  engine: the rasterizers' Query with a payload). Skipping the drain only at
  addresses the GPU never released, "only before" there is what an acquire
  would get wrong, "only after" what would wait longer, and "met before and
  after" the drains it would save.

The existing diagnostics are unchanged.

Rebased onto upstream: EDEN_NX2_DRAIN_CHECK is a plain flag read again
by VideoCore::CreateGPU, as upstream reads its diagnostic switches since
zortos293#714, so it works from nx2_switches.txt too, not only from the
environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lit when they can

Every Switch 2 semaphore acquire drained the split before it read the word
(Puller::ProcessSemaphoreTriggerMethod), and so did every poll of a
suspended one (CanResumeAcquire): the front waited until the back had run
everything queued, in case a release among it changes the word. In
Ocarina of Time's Hyrule Field (EDEN_NX2_GPU_PROFILE, about 50 fps) that
was about 7,100 drains a second at 34 us, 239 ms of every second of the
front. Counted without changing anything (EDEN_NX2_DRAIN_CHECK, per 10 s):
of about 48,000 acquires, 25,000 met their value before the drain as well
as after it and none before but not after; of 145,000 polls, the drain
changed the outcome of about 10.

Now a greater-or-equal acquire is checked first without the drain. The
words Switch 2 syncs on only advance (NVN2 even releases QMD completions
as maxima), so a value that meets one now still does once the back ran
the queued work. Only a release the front sent to the word that the back
may not have run yet can move it: the front now records those as it sends
them (GpuSplit::NoteSemaphoreWrite, from the puller's releases, the 3D and
compute engines' report semaphores and the copy engine's), and the last
one decides. A counter report or a reduction, whose value the front can't
tell, sends the acquire down the drain path as before.

- Met: the acquire passes without waiting for the back. When it is met by
  a release still queued, NoteSync makes what the front reads after it
  (GPU-written command lists) wait for the work sent before the release.
- Not met: an acquire drains as before. A suspended one polls again later
  without draining: until the back runs a release, nothing sent to the
  word meets it, and the back's release latches the waiter or reaches
  memory on its own. After 2 s a poll drains once to report the wait.

Words the buffer cache may hold newer than memory (Nx2MayBeGpuWritten),
equal and mask acquires, High GPU accuracy and delayed fences (where
releases reach memory later) and macros run on the back keep the drain.
The split test checks the record of queued writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With EDEN_NX2_DRAIN_CHECK set, every acquire or poll of a suspended one
that the previous commit decides without draining the split is checked
against the drain path anyway: the front drains, reads the word as before
(and, for a poll, asks whether a release latched the waiter). The 10 s
"Drain checks" line gains:

  without the drain: acquires passed N (by a release still queued Q),
  not met after the drain W; polls resumed N (by a release still queued
  Q), not met after the drain W, kept waiting K (met after the drain M)

W must stay 0: a pass the drained word does not meet. Such a decision is
corrected in this mode (it takes the drain path), so the run stays right
while it counts. M are polls that resume one poll later than a drain
would have resumed them. Unset, each site tests a constant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a macro on the split's front reads memory (MME_DMA_READ and
MME_DMA_READ_FIFOED, FrontDmaRead), the front drained the whole split
(maxwell_3d.cpp:1431 before this change), had the back download what the
buffer cache holds of the words (line 1449) and write out the reports
the query cache holds there (line 1456), then read them. In Ocarina of
Time's Hyrule Field (EDEN_NX2_GPU_PROFILE, about 50 fps) that was 25,500
reads a second, all by macro 0xe2c (24 bytes, a
ReportSemaphore::Compare: the render enable condition of two reports),
at 9.6 us each: 246 ms of every second of the front, which runs in near
lockstep with the back. The words were written long before almost every
time: occlusion reports a frame earlier, status reports some objects
back. EDEN_NX2_CMDPAGE_DIAG found 0.3% of the reads changed by the
drain.

What a drained read returns is guest memory once the back ran every
entry before it, plus what those two caches write out. Only entries that
write guest memory there, make it GPU-written or leave reports for the
query cache can change that. The 3D engine's front sends nearly all
entries and knows where its own writes go, so it notes them as it sends
them (FrontNoteWrites, maxwell_3d.cpp:1429):

- constant buffer data at the constant buffer address and offset, a
  report or semaphore release at the report address (CounterReport
  writes its value and timestamp, now or once the host result is in),
  and a pitch inline upload over its lines are placed at their
  addresses;
- a draw, or any entry the back runs while one may be pending in the
  draw manager (an instance loop's deferred draw is issued by the next
  register write, maxwell_3d.cpp:361; FrontState::draw_pending, line
  1045), a block linear upload, a render enable condition read from
  memory (ProcessQueryCondition reads it through every cache, line 864)
  and a syncpoint increment (macros read syncpoints through memory too,
  memory_manager.cpp:306) count as writes anywhere;
- the other executable methods change registers and host state only, and
  state writes the back sinks run nothing yet.

Every other entry, from the other engines, the puller or deferred calls,
counts as a write anywhere unless its sender says otherwise
(GpuSplit::Writes). GpuSplit::WaitForWrites (gpu_split.cpp:301) waits
for the last entry that may write anywhere and the last one placed in
the words read. It places writes by the device memory the GPU addresses
map to, so the same memory under another GPU address is found: GPU maps
drain the split before later commands (GPU::MapInOrder,
gpu_thread.cpp:128), and the unmaps it is sent are deferred calls.

Past those entries no queued work can make the words GPU-written or add
reports there, so the caches hold now what they would after a drain. The
front checks them where it can without the back: the buffer cache as
before (IsMemoryDirty), and the query cache under its lock for a report
whose value or timestamp reaches into the words (HoldsQueries,
query_cache_base.h:96). When either holds them, it drains and has them
written out as before; otherwise it reads the words at once. The fencing
thread writes only reports the cache holds, and those reads drain.

The split test checks that a macro's DMA read on the front goes ahead of
other queued work and of writes elsewhere, waits for a write of its
words under another GPU address and sees it, waits for work that does
not note its writes, and where writes are placed.

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

A macro DMA read on the split's front now waits only for the queued work
that can write its words and drains only when the caches hold them
(FrontDmaRead). EDEN_NX2_DMA_READ_VERIFY=1 checks that against what
every read did before: after reading the words without a drain, the
front drains anyway, has the back write out what the buffer and query
caches hold of them, reads them again and gives the macro those. It
counts the reads whose words differ (CheckDmaRead) and logs every 10 s
how many reads it made, how many without a drain, how many of those
waited for queued writers and how many differed, with the first 16
differing reads and their words. The differences must stay 0; a guest
CPU write that lands between the two reads would count too.

Off, it reads one constant flag per read.

Rebased onto upstream: EDEN_NX2_DMA_READ_VERIFY is a Maxwell3D member
read when the engine is made, as zortos293#714 made EDEN_NX2_3D_METHODS, so it
works from nx2_switches.txt too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A guest CPU read of GPU-written buffer memory (reactive flushing) had the GPU
thread download it with Scheduler::Finish. The GPU thread waited for the host
GPU with the buffer cache locked, and the reading thread waited for the GPU
thread. In Ocarina of Time that was about 1,500 flushes every 10 seconds and
80-90 ms of each second on the split's back thread. Meanwhile every guest core
that looked up a flush area spun on the buffer cache lock.

Now the GPU thread only records the copies and submits them
(RasterizerInterface::FlushRegionForRead, BufferCache::DownloadMemoryForRead).
The reading thread waits for that submission's tick itself. It then writes the
bytes to guest memory before it reads (FinishRead), and holds the lock only
while it copies them out. Until the bytes land:

- The ranges stay GPU-modified, so nothing treats guest memory there as
  current. Another read of them waits for the same copies instead of flushing
  again (RasterizerDownloadArea::in_flight).
- CPU writes keep their bytes. The copy-out leaves out the writes that are
  still collected, which now includes those InvalidateGPUCache has gathered
  but not applied yet. Applying a write takes its bytes out of the download
  (ClearDownload).
- A newer GPU write keeps its range GPU-modified. The older download still
  writes what a synchronous flush would have left there.
- Downloads are written oldest first, and before a later fence's downloads
  (PopAsyncBuffers). None lands over newer bytes.
- A write to guest memory from the GPU's side first writes the downloads
  under it. This covers semaphore releases, inline and constant buffer
  uploads (MemoryManager) and DMA clears (DMAClear). While no download is
  pending, the check costs one relaxed load.

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

EDEN_NX2_READ_CHECK=n checks 1 in n read downloads (1 checks them all). The
GPU thread copies a second time, in the same submission, what a synchronous
flush of the read would have copied. Once the download is written, guest
memory is compared with that copy, except where a newer value has replaced it
since (CPU writes, ClearDownload). Each byte a synchronous flush would copy
must also be in this download or an earlier one.

Every 10 seconds the log has a line "Read download check
(EDEN_NX2_READ_CHECK): ... bytes differ, ... GPU-modified bytes no download
brought (both must stay 0)". Each difference also gets an error line, up to
16 of them.

The "GPU waits" line now shows separately from the wake-up how long reading
threads spent waiting for their downloads and copying them to guest memory
("then copies on the reading thread"). It also shows how many reads only
waited for an earlier read's downloads. "downloaded" counts the read
downloads again.

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

Two writers went around the read downloads of 6f8c97dd86.

The query cache writes report values and semaphore payloads into guest
memory through device memory pointers, not through the memory manager:
the completion operation, the immediate payload path and
SemiFlushQueryDirty. A read's download still on its way over those bytes
landed the GPU's older copy over them later. FlushRegionForRead even
wrote them right after taking the download, where a synchronous flush
had written them after its copy. They now land the downloads first, as
MemoryManager::Write does.

SyncValues copies query results into the buffer cache's buffers on the
GPU without recording a write. A read download taken before the copy
kept its bytes as the newest, so a later read skipped them.
OutdateReadDownloads, which MarkWrittenBuffer now calls as well, takes
them out of the earlier downloads' clean ranges, and the later read
downloads them again.

Both ask ReadDownloadsMayCover first: whether a read download is
pending, and on Switch 2 whether the bytes' pages were ever GPU-written.
The memory manager's writes ask it too, so semaphore words and constant
buffer data on other pages no longer take the buffer cache's lock while
a read is pending.

SemiFlushQueryDirty runs with the query cache's cache_mutex held.
Nothing that holds the buffer cache's lock takes cache_mutex, so the new
lock order cannot deadlock.

Tests: the next read downloads a query copy made after an earlier read's
download again. On Switch 2, a write to a page no GPU write reached
doesn't wait for a pending download, and a write to a GPU-written page
lands it first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The check from a06257737b compared every GPU-modified byte of the read
with the GPU's copy at the read, guest memory included where an earlier
read's download brought the bytes. After that download lands, the GPU's
side can write newer bytes there before the later download lands:
constant buffer data or an inline-to-memory write through the memory
manager, which lands the earlier download first and tells the buffer
cache only at the next FlushCaching, or a query result. Synchronous
flushes keep those bytes too: the earlier read's flush had cleaned
them, so the later one doesn't copy them. The check reported them as
differences. In the harness, constant buffer data written that way
gives exactly the test47 line, "guest memory ...+0x30 differs from a
synchronous flush", with the newer bytes in guest memory.

Each check now notes the bytes earlier downloads bring (kept), and
what guest memory holds there right after each of them lands. At its
own landing:

- Bytes rewritten since are left out and counted ("got newer values
  after an earlier read's download brought them"). The first 16 are
  logged with their writer: the memory manager's writes and the query
  cache's results note themselves while read downloads are pending.
- Kept bytes are compared as they landed: a difference there means the
  GPU wrote them after the earlier download without taking them out of
  it, and is logged as such.
- This download's own bytes are compared as before.

The summary line adds the rewritten count. Tests: constant buffer data
over an earlier read's download is left out, and a GPU write that
skipped OutdateReadDownloads is found.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ocarina of Time reads back two compute results every frame, one page
each. Each read waited 2.6-2.8 ms for its download. The read-trace
(test47d) shows that the dispatch writing a page ended on the host GPU
6.8 ms before the read came. The download was recorded only at the read,
behind about 770 draws, 95 render passes and 10 submissions recorded
since the write. The host GPU had to run all of them before the copy.

The buffer cache now learns which pages the guest reads after each GPU
write. A page qualifies once two of its writes in a row were read. Four
writes in a row without a read stop it, and 64 pages are kept, the one
read longest ago making room. When such a page is written, its download
is recorded right behind the writing work (RecordEarlyDownloads):

- After a dispatch, at once: the scheduler is outside any render pass.
  Pages written by a draw wait for the next dispatch, a read's flush or
  the frame's end.
- It doesn't submit on its own. The copies go with the GPU thread's
  next submission, as a fence's do (QueueForHostRead). A thread that
  must wait for one before it is submitted asks the split's back to
  submit it. The back submits it itself.

An early download is an ordinary read download, recorded before any read
asks for it. The read finds it the way it finds an earlier read's
download. GetFlushArea now writes such downloads to guest memory itself
once the host GPU has run them. The read then waits for nothing. Finished
early downloads also land after each dispatch, every 32 draws and at the
frame's end, so most reads find the bytes already in guest memory.

Everything that keeps read downloads correct applies unchanged:

- A newer GPU write before the read leaves its bytes to a download of
  their own (OutdateReadDownloads). For a learned page, that is the
  next early download.
- CPU writes keep their values (ClearDownload, pending writes at
  landing).
- Writes to guest memory from the GPU's side land the downloads under
  them first.
- Downloads land oldest first, and before a later fence's downloads.

Early downloads are bounded to 1 MiB a frame and 32 not yet in guest
memory. Past either bound, reads flush on demand as before.
EDEN_NX2_EARLY_READ_DOWNLOADS=0 turns them off for comparisons.

Tests: a page read after each write gets its download behind the next
write. A read before the host GPU ran it waits for that copy alone;
after, it waits for nothing. A newer dispatch or draw write gets its own
download. CPU writes keep their values. A page no longer read stops
after four writes. A semaphore release over a pending early download
lands it first. Finished downloads land within 32 draws or at the next
dispatch. The 33rd download still on its way, and the frame's downloads
past 1 MiB, are left to their reads' flushes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The read trace (EDEN_NX2_READ_TRACE), carried onto early read downloads,
and the read download check (EDEN_NX2_READ_CHECK) extended to the reads
they serve.

The read trace is new in this series. It places a guest read of
GPU-written memory on the host GPU's timeline: Vulkan writes device
timestamps at the start and end of every submission, after each draw or
dispatch that writes pages the guest read back before, and around the
read's copies; the reading thread reads the device's clock while the GPU
thread runs the flush (calibrated timestamps, an extension the variable
enables). Once the bytes are in guest memory, the wait splits into:
until the write's work ended on the GPU, until the copy could start, the
copy, the wake-up, the buffer cache lock and the copy into guest memory.
Every 10 s the log has a "Read trace" line for all reads and one for
each of the four areas that waited longest. With the variable unset,
nothing changes but a render pass count.

Read trace: the copy marks now go around every read download's copies
(RecordReadDownload), early ones included. The work marks come from
NoteWorkRecorded. For a read that waits for an early download, "the
write's end to the copy's start" is the GPU time between the dispatch
and its copy. Each line also counts the reads that "waited for an early
download". Every 10 s a new line follows the "all" line, reported even
in frames where no read waits (TickFrame):

  Read trace early downloads: N recorded (K KiB) | reads they served:
  a found the bytes in guest memory, b found them done on the host GPU
  (landed at the read), c waited for them (in the lines above) | writes
  from the GPU's side that waited for one: d (t ms)

The reads counted under a and b don't wait at all, so the per-read lines
above leave them out.

Read check: before this commit the check never saw these reads. A landed
early download leaves the page clean, and one still on its way covers
the read, which then records nothing. Now an early download that lands
records which bytes it wrote to guest memory and what they were. In
check mode, a read that an early download serves flushes. Its check
copies the read's bytes from the GPU at the read, as a synchronous flush
would. The bytes still on their way are compared as kept bytes are. The
landed ones are compared the same way, against their record:

- If guest memory changed since the landing, the bytes are counted as
  rewritten and not compared.
- Otherwise they must equal the GPU's copy. A difference means the GPU
  changed them after the early download without anything downloading
  them again. That is an error, logged like other differences.

Newer downloaded bytes or guest values drop the record
(WriteDownloadedMemory, ClearDownload), and each read takes what it
compared. A check whose read gets all its bytes from other downloads
becomes a download of its own. The read waits for it, as does a write
from the GPU's side over those bytes.

With neither variable set, nothing changes.

Tests: early-downloaded bytes, landed or on their way, compare equal
(0 differ, 0 uncovered). A GPU change nobody recorded after the landing
is found. A guest write there is left out as rewritten.

Rebased onto upstream: EDEN_NX2_READ_TRACE is a plain flag read again by
VideoCore::CreateGPU, as upstream reads its diagnostic switches since
zortos293#714, so it works from nx2_switches.txt too, not only from the
environment. The test build also had the pipeline library work (zortos293#579)
here in vulkan_device.cpp; this commit adds only the calibrated
timestamps extension.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LsfgBindings holds u32 binding counts and LSFG_MIP_LEVELS is a size_t,
so the pair built from it narrows implicitly. MSVC reported that as
C4267, a warning, until the read trace header that vk_scheduler.h
includes in this test build turned the same instantiation into C4244,
which the build treats as an error (/we4244). Make the conversion
explicit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ocarina of Time's render enable macro (0xe2c, code at instruction 0x142)
reads the six words of a report pair with MME_DMA_READ_FIFOED and writes
the render enable mode, about 25,000 times a second in Hyrule Field. On
the split's front each read waits for the queued work that can write the
words (FrontDmaRead): 25,060 waits a second, 252.8 ms of every second of
a front that is 99.9% busy (test46b, No sharpening; the back is 77-81%
busy).

The back holds what the read needs at the call's point of the method
stream, so such macros now run there. A static pass over the macro code
(TuringMacroInterpreter::AnalyzeEffects, macro_turing.h:266) follows a
call from its start along every path, each branch either way and each
loop any number of times, with the register, carry and method values
the code fixes: it finds the methods the call can write (as long as each
is a constant of the code) and whether it uses the data RAM. A macro
whose code reads memory into its FIFO, uses no data RAM and writes only
registers the front can do without the value of (BackWritable,
maxwell_3d.cpp:1200) goes to the back (FrontRunOnBack, line 1562): its
call is one entry with its parameters (GpuSplit::DeferWords), and the
back runs it with a macro engine of its own, its code copied from the
front's, where ProcessMacro would. It reads the words as a drained front
did. Not back-writable: macro code and bindings, the shadow RAM mode,
draws, constant buffer data, reports, uploads and syncpoints (the front
tracks what they do), the registers the front reads to note their memory
writes, registers the back changes itself (the bind pointer,
shadow_scratch[0], logic_op.enable on AMD) and MME_DMA_READ, whose words
land in the front's data RAM.

The registers such a macro can write, and in a tracked shadow RAM mode
their entries, are then the back's until the front knows them again: a
front write to one is sent even if the front's copies hold the value, a
replay first takes the entry from the back (FrontTakeOver, line 1699),
and a front macro or DMA read that reads one waits for the back to run
the macro and takes the back's value (FrontRegister, line 1685). No
entry after that macro writes the register, so the value can be read
while the back runs on. The waits count as "back register", and the
macro code changing under queued back calls as "back code". The back
counts writes outside the predicted set; the front then runs that macro
itself from then on and takes the back's registers as SPLIT_VERIFY does.
Macros that don't qualify keep the narrower wait (MKW's read into data
RAM word 0x27). EDEN_NX2_BACK_MACROS=0 turns this off.

turing_macro_effects.cpp checks the analysis on hand-made code and
against the interpreter on random code. The split DMA read test now also
runs a macro on the back: the front goes on past queued work and the
back reads the word written behind it, a write of the value the front's
stale copy holds is sent, a front macro that reads the register waits
and gets the back's value, and a replay takes the shadow RAM entry from
the back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EDEN_NX2_BACK_MACRO_VERIFY=<n> (64 without a number): the first n calls
of each macro the back runs (FrontRunOnBack) also run the old way. The
front drains the split, runs the call against its copies of the
registers (the back's for those back macros may have written, exact
once drained), its DMA reads made once the caches wrote the words out,
and writes nothing (FrontRunTheOldWay). It then sends the call, whose
back run records its writes (MacroEngine::split_record), drains again
and compares the (method, value) lists. The first 16 differences are
logged with both lists; there must be none.

Every 10 s it logs the calls on the back, how many were checked and
differed, and the front's reads and replays of registers back macros
wrote, with how many of those waited for the back. Off, it costs a test
of a static flag where it counts. The waits themselves, and the back's
macro time, are in EDEN_NX2_GPU_PROFILE ("back register" and "back
code", the back thread's "macro" zone).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s GPU work wrote

In Ocarina of Time's Hyrule Field (EDEN_NX2_GPU_PROFILE, test46b and
test47) the split's front waits for the host GPU 169-204 times a second,
550-765 us each (93-156 ms of every second), for the copy of a GPU-written
command list page (BufferCache::Nx2ReadListCopy, the "list GPU wait"
zone). The back has nothing queued meanwhile: across two boots its idle
time moved one for one with these waits.

Only pages whose last noted write is GPU work take that copy: a compute
dispatch's storage write, or a copy engine copy of GPU-written bytes
(DMACopy notes only the source ranges that are GPU-modified). The CPU,
inline uploads and copies of bytes guest memory holds write guest memory
and retire the copy instead. So the words GPU work wrote exist only on the
host GPU, and those must wait. But the copy served the whole 4 KiB page:
a list elsewhere on the page waited for it as well, and read from the
copy the words the host had written, which the buffer it came from need
not have had uploaded yet.

- BufferCache::Nx2ListPage keeps the page's words GPU work wrote that guest
  memory lacks: set by every GPU write noted to the page (Nx2NoteListWrite,
  the whole range a dispatch or copy may write), cleared for the words the
  host writes whole (Nx2NoteListHostWrite) and for all of them when the
  front flushes the page (Nx2NoteListSynced, Nx2FlushListPages).
- Once the front has flushed the page (before, GPU work may have written
  it unnoted, before it became a command list page), a list on none of
  those words reads guest memory and waits for no copy. A list on some of
  them reads those words from the copy and the others from guest memory.
- The copy engine tells the command list pages what it writes to guest
  memory: DMACopy notes the copied bytes as a host write before it notes
  the GPU-written source ranges as GPU writes, and DMAClear notes the
  cleared words as an inline write does. Both left the page's copy
  current, so the next list there read the bytes from before. DMAClear
  also checked only a quarter of its range for registered buffers: it
  passed the word count as bytes.
- A list on a GPU-written page waited for every dispatch and copy sent so
  far (GpuSplit::WaitForWriters). As for lists on other pages (Step, since
  dab34bfd8c), the work that rewrites it is ordered before the fetch only
  by a host-level wait between them: wait for the work before the last one
  (WaitForSyncedWriters). The profiler counts this wait as its own site,
  "list copy writers".
- While the front waits for the GPU to finish a copy
  (BufferCacheRuntime::WaitSubmittedTick), the back runs only what the
  front published. When the front had not waited for the back before (the
  writers were done), the entries sent since its last publish, fewer than
  256 ring words, waited out the copy as well: it publishes them first.

Tests (buffer_cache_ports): lists beside the words a dispatch wrote read
guest memory, lists on them read the copy and spanning ones both; pages
the front has not flushed read every word from the copy; host writes give
the words they cover whole back to guest memory and retire the copy; a
flushed page reads guest memory until GPU work writes it again; copy
engine clears give their words back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_NX2_LIST_STATS, EDEN_NX2_LIST_VERIFY)

EDEN_NX2_LIST_STATS=1 logs every 10 s from the front, for the command
lists on GPU-written pages (DmaPusher::ReadGpuWrittenCommands):

- "list stats": the lists and their sizes; for each page read, how
  Nx2ReadListCopy served it (page synced, words clean, copy ready, copy
  waited, unknown); the copy waits' time and spread; the reads and waits
  by the page's last writer (dispatch, copy, draw); how many copies held
  the bytes guest memory already had (the GPU rewrote the same words); and
  at copy waits: whether the back had work and how much of it the front
  had not published yet, whether other channels had lists queued for the
  GPU thread (which could run while the channel waits for the copy), and
  how long before the wait the back had recorded the copy.
- "list stats writers": the dispatches (by shader hash) and copy engine
  copies (by source GPU address) that last wrote the pages read, with
  their reads, waits and writes to command list pages.
- "list stats pages": the pages read, the bytes the lists read on them,
  and the bytes and writer of their last write.

EDEN_NX2_LIST_VERIFY=1 reads each such list again as the commit before
this one did: after every dispatch and copy sent so far (WaitForWriters),
from the pages' whole copies (Nx2ListStats::t_reference), or drained and
flushed. When the two differ it also drains and flushes, which tells the
stale read. "list verify" counts lists and differences every 10 s, the
first 32 differences are logged with the list's pages, and the front
parses the words read last.

The writers are tagged in RasterizerVulkan::DispatchCompute and
AccelerateDMA::BufferCopy, the queued lists counted in ThreadManager.
Test (buffer_cache_ports): the read as before takes the whole copy.

Rebased onto upstream: EDEN_NX2_LIST_STATS and EDEN_NX2_LIST_VERIFY are
plain flags read again by VideoCore::CreateGPU, as upstream reads its
diagnostic switches since zortos293#714, so each works from nx2_switches.txt too,
not only from the environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Semaphore acquires and the polls of suspended ones no longer drain the
split. Fix 2 only checked greater-or-equal acquires without the drain,
and drained for the rest, for words shaders write, for releases it
couldn't value, and whenever the value wasn't met yet. In Ocarina of
Time that was about 4,800 drains a second at the acquire site (182 ms/s
on the GPU thread), most of them at words QMD releases advance.

Now the front decides each acquire (equal, greater-or-equal, mask) from
what it can see:

- The last release sent to the word, while the back hasn't run it: met
  passes (with NoteSync, as before), not met suspends.
- A release the front can't value (a 3D counter report, a reduction it
  can't compute): a positioned wait for that release only, then memory.
- Otherwise memory. Greater-or-equal holds once met, since the words
  only advance. Another operation holds only for the value the word has
  when the channel reaches it: compute and copy work sent before the
  acquire may release it unseen (QMD releases), so a met value waits for
  that work first (a positioned wait, not a drain).
- Words on GPU-written pages that the buffer cache holds GPU data for:
  memory still decides greater-or-equal acquires it meets. Otherwise the
  back flushes the word, the channel suspends, and the polls read memory
  once the flush ran. This keeps the MustFlushRegion check for semaphores
  shaders write.

An unmet acquire registers its waiter and suspends the channel. A
release the back runs latches it (Nx2SemaphoreWaiters::OnWrite). The
polls decide the same way without draining, so other releases resume it
from memory or the view. The fence work is a plain Defer. It is sent
once the back has run everything before the acquire and the acquire
still waits, which is where the drain path sent it. Acquires that the
queued work releases send none. SEM_EXECUTE acquires never blocked;
they drop their drain too, keep the NoteSync it implied, and are checked
for their warning only. High GPU accuracy, delayed fences, macros on the
back and unmapped semaphores keep the drain path.

Compute report reductions are valued on the front when the word's value
before them is known: the last release sent to it, or memory when no
compute or copy work is queued. That holds only for reports sent
directly, since a macro's own launches count as work only once the
macro ran. GpuSplit gains a position API: Position, Ran,
LastWriterPosition, WaitForPosition, and the position of a queued
semaphore write.

The GPU thread's poll loop counts a resumed channel as progress, as its
comment says, so it keeps polling fast while channels move.

Tests: a Catch2 case and the standalone split harness (real back thread)
cover the positioned wait for a queued release, which leaves the work
after it running.

Rebased: the back macros commit before this one added the same
GpuSplit::Position(), so this commit keeps that one instead of adding a
second (two definitions did not compile, here and in the next commit,
until the commit after them merged the position waits).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zortos293 and others added 23 commits November 9, 2026 09:33
… memory) (zortos293#878)

* vulkan: free compute IR before a synchronous driver compile

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

* vulkan: release the SPIR-V allocation too, not just its contents

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>
(cherry picked from commit fd4ce24)
… decoder choice (zortos293#879)

* nx2 video: decode AV1 with libdav1d on a capped thread count and log the decoder choice

FFmpeg's own av1 decoder only decodes through a GPU hwaccel, so AV1 asks
for libdav1d by name instead of relying on FFmpeg's registration order.
dav1d gets half the host threads (2 to 8) instead of one per host thread.

HardwareContext only creates the GPU devices the decoder has a hardware
config for. With NVDEC emulation on GPU (the first-run default), an AV1
movie no longer creates CUDA, D3D11VA, DXVA2, D3D12VA and Vulkan devices
before falling back to libdav1d.

Each nx2 decoder logs one line: GPU or CPU, the decoder, its threads, why
and how long it took to open. The vdec trace adds decode_us per input.

* tests: skip the AV1 decode test when FFmpeg has no libdav1d

(cherry picked from commit 64b962a)
…PIR-V emit (zortos293#902)

Assisted-by: Gemini (Section 6 clip_distance_written race and eviction comment)
(cherry picked from commit 0dfaf624bafc85122dde2a9eada17aad6834bbb2)

Co-authored-by: AManWithAPlan <amanwithaplan_dev@proton.me>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 3153d96)
…arina of Time) (zortos293#910)

ResolveMemoizedTextures prepared every memoized view, including
NULL_IMAGE_VIEW_ID, which VisitImageView never prepares. The null image
has an invalid format and the default CpuModified flag, so a memo hit on
a draw with a null texture tried to upload it and ended in UNREACHABLE()
in BitsPerBlock. Ocarina of Time crashed this way loading a save.

Fixes zortos293#904

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit c2e293e)
…tos293#918)

Log2Floor(0) wraps to 0xFFFFFFFF, so Log2Ceil(0) shifted by that much and
returned a huge index. A zero-size staging request (the null image's upload
in the crash zortos293#910 fixed) then read StagingBufferPool's 64-entry cache far out
of range. Log2Ceil now returns 0 for 0.

Refs zortos293#904, zortos293#910

(cherry picked from commit 281477e)
…zortos293#932)

size is std::size_t and address_space_size u64. They are the same type on Linux
and Windows but not on macOS (unsigned long and unsigned long long), where the
min from zortos293#858 did not compile. The result is at most size, so it fits the
std::size_t parameter it is passed as.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit cbb512e)
…ortos293#957)

MultiLevelPageTable::ReserveRange took (start + size) >> first_level_shift as
the last first-level index, inclusive. A range ending exactly at the top of
the address space (which MemoryManager::PageTableOp accepts) indexes one past
first_level_map: it reads past the vector and, when that word is null,
AllocateLevel writes past it. ASan reports it in the maxwell_fast_method test
that maps the last GPU page.

Take the index of the range's last byte instead; an empty range reserves
nothing.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit cee0c2d)
…iles (zortos293#947)

* vfs: return 0 from VectorVfsFile::Read for an offset past the end

VectorVfsFile::Read underflowed the remaining size for an offset past the
end and copied out of bounds. Ported from failaip12's Eglantine PR zortos293#462;
only the vfs_vector.cpp hunk is taken, the commit's audio, scratch buffer
and zbic hunks are unrelated.

(cherry picked from commit 6375dc49bf6a453192d5dd2948053557c831cc6f)

* tests: cover VectorVfsFile reads past the end

Check that VectorVfsFile::Read returns 0 for an offset at or past the
end. Ported from failaip12's Eglantine PR zortos293#462; only the vfs_vector test
is taken, the commit's usage tracker test belongs to an unrelated change.

(cherry picked from commit 9932bd647286591a7ebdc3e9e7366a42893493b0)

* common: let ASan builds link zbic's bundled zstd

zstd.c declares the ASan poisoning functions itself, which gives them C++
linkage inside zbic_compression.cpp and breaks ASan links (the tests
target fails with undefined __asan_poison_memory_region). Declaring them
first through the sanitizer header keeps C linkage. Ported from
failaip12's Eglantine PR zortos293#462; only the zbic_compression.cpp hunk is
taken.

(cherry picked from commit 6375dc49bf6a453192d5dd2948053557c831cc6f)

* vfs: size ReadBytes buffers by the bytes the file holds

ReadBytes allocated the requested size before reading, so a size taken
from a file header (a PFS0 or RomFS table) could ask for terabytes. Clamp
the buffer to what lies between the offset and the end of the file; the
returned bytes are the same as before.

* file_sys: bound PFS0 tables and entry names

Check that the entry and string tables fit in the file before reading
them, that each name offset lies inside the string table, and read the
name with strnlen up to the table end instead of strlen. Iterate entries
with a u32, since a u16 index never reached a count above 65535.

* file_sys: walk RomFS tables without recursion and reject loops

ExtractRomFS recursed once per directory level and followed sibling and
child links with no visited check, so a link back to an earlier entry
looped until memory ran out and a long chain overflowed the stack. Walk
the tree with an explicit stack, mark each entry offset when it is read,
cap the depth at what a 0x300-character Horizon path allows, and return
nullptr for a table that runs past the file, an entry past its table, a
revisited entry or a named root.

* fssystem: check NCA FS header ranges and layer parameters

NCA FS header offsets and sizes are signed and untrusted, but the driver
compared plain sums against the storage size, so a negative size or an
overflowing sum passed and later sized an allocation or an OffsetVfsFile.
Check every range with an overflow-safe helper, and check the hash
regions before they size the SHA-256/SHA3 buffer.

Also reject what Atmosphere asserts or never meets in signed content:
SHA-256 blocks smaller than a hash, integrity block orders a shift or a
u32 cannot hold, fewer than three integrity layers, layer counts outside
the hash target table, FS headers or NCA headers the file is too short
to hold, and misaligned patch tables. IntegrityVerificationStorage::Read
now returns 0 for an out-of-range read instead of asserting, and zeroes
only the requested bytes past the data rather than up to the block end,
which wrote past the caller's buffer on short reads.

CreateAesXtsStorage returned the storage it had just moved into the
alignment wrapper, so every AES-XTS section opened as a null storage.

* file_sys: reject XCI images without a secure partition or HFS0

An HFS0 offset past the end of the image underflowed the root partition
size, and a root partition without a secure entry handed NSP a null file.

* tests: cover malformed PFS0, RomFS, NCA and XCI inputs

Each case builds its input from scratch, with no game data: the shapes
fuzzing found (name offsets past the string table, tables larger than the
file, RomFS link loops and deep chains, SHA-256 and integrity layer
parameters, negative patch table sizes, XCI images without a secure
partition, short integrity reads past the data) and a valid input of the
same shape that must still parse.

* fuzz: add libFuzzer harnesses for the game-file parsers

LICHII_FUZZERS (off by default, clang only) instruments the build with
ASan and UBSan and builds three harnesses: raw PFS0/RomFS, a single NCA
(alone, as an update with a missing base, and wrapped in an NSP), and a
whole NSP or XCI. The NCA harnesses use made-up key area keys in a
scratch keys directory, so plaintext NCAs reach the FS header and storage
parsers without real keys. docs/Fuzzing.md covers building and running
them.

Based on the harnesses EGLA-84 used to fuzz the .nx loader (zortos293#866).

* fssystem: bound buffered NCA tables by the bytes the NCAs store

The indirect and AES-CTR-EX tables and the SHA-256/SHA3 hash layers are
read into memory whole. Their ranges were only checked against the
section storage, but sparse and indirect layers give that storage a
virtual size taken from header fields, so a 1.5 KiB NCA could ask for
a 372 GiB buffer. The patch tables are stored in the NCA itself and the
hashes in it or the NCA it patches, so reject anything larger than
those bodies.

The hash buffer also caches two hash blocks, and 2 * hash_block_size
overflowed s32 for a 1 GiB block. Compute it in size_t, require the
hash layer to fit in one block as HierarchicalSha256Storage assumes,
and reject blocks larger than the stored bytes rounded up to a power
of two.

* fssystem: drop the unused hash cache from SHA-256/SHA3 buffers

The hash storages only use the first hash_region.size bytes of the
buffer, so the two cache blocks were never read. Allocate the hash
layer alone. That removes the reason to bound the block size by the
file size, which rejected valid small NCAs whose hash block is larger
than the whole file.

(cherry picked from commit 59e3d22)
…3#431) (zortos293#961)

* common: trim all-zero guest pages from the working set on Windows (EDEN_NX2_TRIM_ZERO)

Windows can't punch holes in the pagefile-backed guest memory section, so pages that only
ever hold zeros stay resident. Ocarina of Time boots with ~7 GB of them, leaving a 16 GB PC
with 0.2-0.6 GB free. HostMemory::TrimZeroPages takes those pages out of the working set
(VirtualUnlock, like HintCold); an nx2:ZeroTrim thread runs it every EDEN_NX2_TRIM_ZERO
seconds for the first 2 minutes, then every EDEN_NX2_TRIM_ZERO_LATER (default 60). Off by
default.

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

* common: trim all-zero guest pages unconditionally and only read them through the backing

The zero-page trim runs for every title, without EDEN_NX2_TRIM_ZERO. Its thread moves from the
nx2:gpu session to DeviceMemory, which lives for the whole app, so a fixed boot window no longer
fits: a pass comes every 5 s while it trims 64 MiB or more and backs off to once a minute.

Pages are checked through the backing view only. It stays mapped and writable, while a guest
view page can be unmapped or protected during a pass; a view page's backing offset comes from the
placeholder map under its lock. This also drops __try, which MinGW can't build. A backing page the
check pulled into the working set leaves it again, and a run spanning two views, which
VirtualUnlock refuses whole, is trimmed page by page.

Trimming keeps the contents, so a guest or GPU write landing after the check costs a soft fault.
Discarding (DiscardVirtualMemory, MEM_RESET, punching holes) would lose it: neither OS can check
and discard a shared section page in one step.

Co-authored-by: Ghostwolf13 <353235520+Ghostwolf13@users.noreply.github.com>

---------

Co-authored-by: Ghostwolf13 <353235520+Ghostwolf13@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 2b30c2c)
… Time

The upload (a Splatoon Raiders fix for bank writes the memory tracker
missed) copies every native 64 KiB uniform bank in full each frame. In
Ocarina of Time a compare of the banks against shadow copies found no
page changed behind the tracker, and skipping the upload is ~1.5 fps
faster with the same screenshots. Every other title keeps the upload,
and EDEN_NX2_PERF_VERIFY=1 keeps it for Ocarina too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 913943a)
…s and buffers are

The draw macros mark the index buffer dirty on every draw, so every
indexed draw translated its address and looked its buffer up again, mostly
for the same buffer with another index count. The address and buffer found
are kept per range while the GPU mappings (MapSerial) and the buffers
(buffer_deletions) are, for sizes up to the one the buffer was found for.
EDEN_NX2_PERF_VERIFY resolves anyway and logs a mismatch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 12262ce)
(cherry picked from commit 6387f2c)
FindBuffer waits for a buffer's pending writes when GPU fence behaviour
is Accurate; a memo hit would skip that wait.

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

The flush could finish the whole GPU queue from the middle of a translation, stalling the GPU thread for seconds.

(cherry picked from commit 591ceee)
…down

RtlVirtualUnwind also reads through the frame register, which the walk's stack-pointer check
doesn't cover. Sampling CPUCore_* threads (EDEN_NX2_SAMPLE), a thread paused in a fiber switch
(KScheduler::EnableScheduling) with Rbp 0 made it read address 0, and the access violation
crashed Lichii. The walk now runs under SEH: a fault stops it and keeps the frames found so
far. The report counts samples without a stack and walks stopped by a fault per thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 7a755fb)
TextureCacheChannelInfo::image_views and samplers were node-based
std::unordered_maps. VisitImageView and FindSampler look them up
whenever a descriptor table entry changes, so each lookup chased a
bucket pointer and then a heap node. Every other hot texture cache map
is already Common::unordered_map (boost::unordered_flat_map): page
tables, framebuffers, sparse views, allocation and join tables. These
two now use it too. The API (try_emplace, find, erase by iterator) is
the same.

The old TODO said the code relies on iterator stability. The only
iterator held across other calls is VisitImageView's try_emplace result,
held across CreateImageView. CreateImageView can reach
RemoveImageViewReferences, which erases from image_views but never
inserts. A flat map does not rehash on erase, and the erased entries
never include the new one, whose id is still 0 at that point. Inserts
happen only in VisitImageView itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit a518616)
NativeImageHandles memoizes per program address but returned the pair of vectors by value, and
DispatchNativeCompute asks for every Ocarina of Time dispatch with constant buffer 0 (~4,700 a
second in the field): two vector copies, each a heap allocation and free, on the split back.
It returns a reference into its memo (an unordered_map: references stay valid as it grows).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5edee0c)
The buffer and texture caches ask for the device memory usage every frame, and with
VK_EXT_memory_budget each answer is a driver and OS query (~0.1-0.5 ms on Windows, PR author).
Answers younger than 50 ms are reused. The PR's EDEN_NX2_MEMSTAT_MS tuning knob is the
constant 50 ms (its default).

Ported from zortos293#717 by `stephenjayakar`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 73f82c8)
SignalFence queued the fence and then, when it had downloads to flush, called
FlushCommands. On Vulkan the fence's Queue already submits (or defers to the split's back
with Scheduler::DeferFlush) everything up to its tick, so the second call submitted again
whenever draws or native work had been counted since the last FlushCommands, at fences with
downloads or queries (thousands a second in Switch 2 titles). OpenGL's fence only inserts a
sync object: its QueueFence now flushes the commands before it itself.

Ported from Citron `3a3297553` (by `hopesuffers`), with the OpenGL flush moved into its
backend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5b6fd59)
…c25fbd / zortos293#921)

Keep the zortos293#805 draw profiling scope and move the b2304fa null-image guard into PreparesImage, shared by full resolves, memo hits and sampled-image checks. Add the upstream sampled-view tests plus a valid-view/null-image regression.

Validation: git diff --cached --check passed. Build and runtime verification deferred to the caller; no CMake/Ninja build started.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… port of nx2 a4f6287 / zortos293#949)

Port 6b38851 (uniform keeps), ee98215 (descriptor replay), and 3e86407 (notified CPU-write invalidation). The ClearBuffer and buffer-copy test support from b2375cf already exists on zortos293#805.

Retain zortos293#805 scheduler/acquire/drain behavior, async readback bookkeeping, in-place GetSpan uniform comparisons and fine profiling. Keep all existing buffer-cache tests; add keep range/lifetime and write-overflow coverage. Skip empty image ranges during descriptor replay. Document the unnotified CPU-write assumption and separate verify passes.

Validation: git diff 58144d2 --check and static port invariants passed (excluded split/acquire code unchanged, all 45 existing buffer-port tests retained, test registration and conflict/hint checks). Compilation and executable tests deferred to the caller as requested; no CMake/Ninja build started.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
NVN writes the first vertex to vertex_id_base before each draw. No part of
a draw's resolve reads it, yet every change counted as a state change, so
draws after one took the full path. In Ocarina of Time that was the most
common fast draw break (21k to 36k per second during play).

(cherry picked from commit d6fd421)
…g fast draws

A counter reset only touches the query cache, and the render enable
condition decides whether the next draws run, not what they bind. Both
ended draw reuse as executable methods. With vertex_id_base no longer
breaking it, they were the most common fast draw breaks in Ocarina of
Time (11k to 23k and 6k to 12k per second during play). A render pass
either one ends is still caught by the reusable draw's render pass check.

(cherry picked from commit b39d94b)
Ocarina of Time writes a report semaphore address and releases a report
around most draws, and points the macro engine's memory reads somewhere
new. Each ended draw reuse, and together they were behind most of the
remaining full resolves (17k to 29k draws per second during play).

The report's operands and the macro memory addresses are now draw
parameters. The release itself is recorded as constant data at the
address it writes: a delta draw whose texture handles live there, or a
write into the texture or sampler headers, still takes the full path.

(cherry picked from commit 7e843d3)
@coderabbitai

coderabbitai Bot commented Nov 9, 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

Copilot AI added 4 commits November 9, 2026 13:11
Merge origin/nx2 without replacing the complete zortos293#805 split with its partial
ports. Conflicting areas are resolved as follows:

- gpu_split.{h,cpp}, dma_pusher.{h,cpp}, gpu_thread.cpp, engines/maxwell_3d.cpp,
  engines/kepler_compute.{h,cpp}, engines/maxwell_dma.h and their fast-method
  tests retain zortos293#805's write classification, QMD/front view, position waits,
  tick/idle hooks, fast method path and sampled method timing. Do not import
  zortos293#949's reduced acquire table, back_quiet drain scheme or duplicate zortos293#1028
  write tracking. Keep the independent scheduler-owner check on CPU/service
  writes, the new RingFull drain site and opt-in live-read comparisons.
- engines/puller.{h,cpp}, nx2.h and query_cache.h combine zortos293#805's authoritative
  front semaphore view and source-tracking unbounded waiters with zortos293#979/zortos293#1014's
  unbounded region table and direct-release shadow. Mirror releases only when
  ViewApplies is false: unsplit, back macros, or delayed/high-accuracy releases.
  The full front view must not consume a second mirrored counter absent from
  its original releases. Note both destination writes on the fallback path.
  Compute max/reduction releases retain shadow combination and zortos293#805 checks.
  Keep zortos293#874's synced payload handling and FencedPayloadLanding: synced
  callbacks never advance fenced_landed.
- gpu.cpp, rasterizer_download_area.h, rasterizer_interface.h, memory_manager.cpp,
  buffer_cache.{h,base.h}, vk_buffer_cache.{h,cpp}, vk_rasterizer.{h,cpp} and
  buffer_cache_ports keep the full zortos293#805 read-back, upload ordering and tracing.
  Bring in zortos293#1028's review fixes: ClearDownload lands reads before DMA/discard
  writes, ClearReplacedDownload cancels only bytes reaching guest memory,
  and pending query results leave newer GPU writes dirty. Retain the new
  regression cases and the Qt-safe MayHoldGpuWrites boundary. Drop the unused
  reduced nx2_async_readback diagnostic implementation instead of duplicating
  zortos293#805's richer GPU-wait/read traces.
- device_memory_manager.h/inc use zortos293#984's per-manager generation ranges with
  Map/Unmap increments and preserve DownloadLanding/write tracking.
- nx2_profile.h and video_core.cpp combine zortos293#805's fine counters, sampling,
  scope depth and runtime switch setup with zortos293#965/zortos293#1039's zone-parent tracking,
  GPU waits, contention, worker/present/submit zones and stutter sites.
- vk_pipeline_cache.cpp takes zortos293#1036's v2 cache, migration, jobs, parallel
  translation and emission. shader_environment.{h,cpp} keeps zortos293#970's unsafe TIC
  reads; also omit v2's new pool-wide pretranslation flush, since workers no
  longer flush descriptors. Keep the AMD e0a992208d87eeb8 exception in
  pipeline_optimization.h.
- vk_graphics_pipeline.cpp, uniform_stream_reuse.h, sampled_views.h and tests
  converge zortos293#921/zortos293#949 with the adapted branch ports. PrepareImageView uses the
  single PreparesImage predicate, including valid views of NULL_IMAGE_ID.
  Keep uniform/descriptor delta replay, zortos293#974 fast-draw checks, zortos293#805 profiling
  and the empty-descriptor-range guard; no parallel duplicate paths.
- vulkan_device.cpp and vulkan_memory_allocator.cpp combine cache-v2 device
  support and zortos293#880's backable-image failure policy with zortos293#836's synchronized
  recycler and zortos293#831's 50 ms memory-usage cache. Preserve all remaining branch
  AMD, query, shader, texture and fast-draw changes in the automatic merges.
- nx2_video_decoder.cpp takes nx2's final dav1d/default-thread state. CMake test
  lists combine both sides; keep zortos293#805 fast-method tests rather than tests of
  rejected reduced split internals, and adapt waiter tests to >64 owners.
  Other matching cherry-picks converge to nx2; NX archives, HostTiming,
  memory/stutter/cache tooling and their sources are retained.

Validation before commit: git diff --cached --check passed; 30 cache_metrics
and cache_bench Python unit tests passed using workspace-local test data.
The requested clang-cl ThinLTO/PGO build is in progress. BUILD_TESTING is OFF;
C++ tests and in-game semaphore/read-back/cache/fast-draw validation remain
separate. No emulator launched or stopped; no push or other branch changes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The zortos293#1028 pending-report list retires callbacks in fence order. zortos293#874's synced payload callback runs immediately, possibly ahead of an older report at the same address. Do not register that callback as deferred or let it retire the older report, whose rewritten bytes must remain GPU-modified until its own landing. Preserve the existing is_synced/fenced_landed contract. Add regression coverage alongside the imported read-back tests and document the interaction.

Validation: git diff --cached --check passed. C++ tests are not built (BUILD_TESTING=OFF); the requested integration build is still running and will be rerun for this commit.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
origin/nx2 advanced to zortos293#1056 during the first merge's successful build.
Merge that tip too, keeping the complete zortos293#805 implementation over its port.

- maxwell_3d.{h,cpp}, macro.{h,cpp}, macro_turing.h: keep zortos293#805's full effect
  analysis, front/back register and shadow ownership, macro safety checks,
  verification and profiling. The new port is a reduced version of code this
  branch already has, not a second implementation to run alongside it.
- gpu_split.{h,cpp}: retain zortos293#805's richer APIs, write classifications, drain
  sites and back macro execution. Take zortos293#1056's review fix: NoteSync records
  the actual last writer, WaitForSyncedWriters waits for that position (keeping
  this branch's site parameter), and Stop resets it. Do not duplicate Position,
  WaitUntil or WaitForSyncedWriters.
- dma_pusher.cpp: retain zortos293#805's command-entry/copy-writer distinction and
  diagnostics. Take the other review fix: recheck the GPU-written page after
  the decision even when a writer completed without blocking the front.
- maxwell_fast_method.cpp: keep zortos293#805's broader macro/DMA tests; add the port's
  synced-writer regression, including unrelated work between the last writer
  and a sync, and verify the new position resets on restart. The turing effect
  tests and CMake list already agree.
- nx2_async_readback.h stays deleted: the reduced port's counters are not a
  replacement for zortos293#805's full macro/read tracing. No new experimental switches.

Validation: git diff --cached --check and duplicate-test-name checks passed.
The preceding committed integration built with the requested clang-cl command
(exit 0); rerun the same incremental build after this merge. BUILD_TESTING is
OFF, and the emulator has not been started or stopped. No push.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The zortos293#1056 test name already existed in the full zortos293#805 suite. Fold the new unrelated-work scenario into that test instead of registering it twice. Keep the restart assertion. Correct the preceding merge's validation note: its duplicate-name assertion failed before commit; this commit removes that duplication and the repeated check now passes. git diff --cached --check also passes. BUILD_TESTING remains OFF.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.