Repository navigation
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
Draft
lucastaonline wants to merge 114 commits into
lucastaonline wants to merge 114 commits into
Conversation
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>
… 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)
…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)
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
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. |
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
f837a6640a, which joins it with video_core: decouple the split GPU threads — Ocarina of Time holds 60 fps on AMD, plus fixes for three older races #805 as @jpkto described on query_cache: keep fence-flagged payload releases synchronized at low accuracy (#337 perf regression) #874: a synced release's operation doesn't callNoteFencedWrite, and the front notes those releases asLanding::AtRun(GpuSplit::FencedPayloadLanding).b2304fa55a:PrepareImageViewskips views of the null image. video_core: decouple the split GPU threads — Ocarina of Time holds 60 fps on AMD, plus fixes for three older races #805's memoized textures prepared it and aborted inBitsPerBlockabout 73 s after boot (the crash @danieljesus reported and diagnosed on video_core: decouple the split GPU threads — Ocarina of Time holds 60 fps on AMD, plus fixes for three older races #805). It's now part of the video_core: keep fast draw delta draws and memo hits to what a full resolve does #921 port below.a518616c1d,5edee0ccf8,73f82c80b6,5b6fd59ff0.6ec24332c1).a21232a5b8). Its acquire changes duplicate video_core: decouple the split GPU threads — Ocarina of Time holds 60 fps on AMD, plus fixes for three older races #805 and were left out.Tried and left out (measured, no gain on this machine):
A port of nx2: Ocarina of Time performance work (split GPU thread, caches, JIT, kernel), squashed on build 116 #717's front batching (@stephenjayakar, via Ocarina of Time on RDNA 4 and Zen 5: 224 fps VRR cap, AMD FSR 4 and frame generation, pacing, loads, HDR, 21:9 (draft, tracking) #831:
b212f2df16,bb4f73987b,defad42631). The back thread's method time went down, but the new delta/CB/run zones cost the same: 5.15 → 5.25 µs per draw.Two changes of my own:
FastDrawNeutral.Both raised delta draws (47k → 53k a second), but over 3 interleaved runs fps stayed within noise (C 49.4, D 49.0, F 49.5). They are kept on a separate branch.
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:
lichii.exe -g <game>.frame_timesover 20 s, withEDEN_NX2_GPU_PROFILE=1.Two interleaved rounds per build:
a551b80888That 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:
EDEN_NX2_FAST_DRAW_VERIFYEDEN_NX2_TEXTURE_MEMO_VERIFYEDEN_NX2_DELTA_DESCRIPTORS_VERIFYEDEN_NX2_UBO_KEEP_VERIFYUniform 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.