Repository navigation
perf(agc): share one image table per heap and write epoch through capture, compare, resolution and descriptors - #6454
Conversation
MarkWritten and StoreOwnBytes return 0 for a range that is not wholly watched, and they stamped none of its blocks. The watched blocks such a store wrote kept their old stamps, so UnchangedSince and StoredOver over them still read as unchanged after a GPU write that crossed into an unwatched import. A vouched shader table column or a cached range then kept bytes that had been replaced. Both now stamp each block whose part of the store starts or ends in watched memory, as the wholly watched path stamps every block. They still return 0 and note the store as unstamped. This costs one or two coverage lookups per block, and only on this path.
A driver store and an own store across a watched block and an unwatched one must stamp the watched block and no other. On Linux, a store across the halves of a split block must stamp it, and a store into its unwatched half alone must not.
… is unwritten Every capture of a shader with an image table read every record of each column and looked each record up among the snapshot's distinct descriptors. A 32,768-record column of distinct T#s cost 6.1 ms per capture here (2.3 ms with 16 distinct T#s), and Resident Evil Village's title captures such columns on nearly every draw. TableSnapshotter now keeps the last 16 column snapshots per thread (at most 4 Mi words in all, least recently used first out), keyed by heap base and size, record shape and key count: the runs it read, their words, each record's state and code, and the distinct T#s. A narrowed, selected or pointer-indirect column, or one with an unmapped record, is not kept. A capture of a kept column takes the codes and the distinct T#s from it with no per-record work. When they are the snapshot's first words they are taken as they are, and the words index is built only if a later column needs it, so the result is the snapshot a fresh walk would produce, codes and words order included. The runtime gets three optional callbacks: stampWrites gives a stamp for a range before any of it is read, unchangedSince tells whether nothing wrote the range since, and supplyWords hands words that equal guest memory to the capture as read, so they stay in the captured regions the draw cache compares. ShaderMemory implements them with the write tracker. The stamp is CollectWrites's generation, refused when this ShaderMemory already fetched a page of the range or recorded GPU work writes it. A range is unchanged while no block of it is stamped after the stamp (collected CPU stores, the driver's Write, WriteChanged and StoreOwnBytes, MarkWritten for GPU writes into imports), no recorded GPU work writes it and the guest allocation generation is the stamp's. When every run is vouched for, the column is not read at all; otherwise its runs are read and the snapshot is reused only when they are equal word for word. The 32,768-record column now costs 0.48 ms per capture with no word read when unwritten, and about 0.9 ms outside the write watch. APS5_NO_TABLE_SNAPSHOT_REUSE=1 turns the reuse off. The [image-table] profile line counts the reuses, and ResourceMaterializer::TableReuse returns them with the words the snapshotter read.
agc_shader_table_snapshot_reuse (agc_shader_memory_tests --table-snapshot-reuse) captures a 4,096-record image table in write-watched guest arena memory and checks each capture's snapshot and captured words against the table: - an unchanged table is reused without reading a word, and its snapshot equals the first one; - a CPU store, a driver store (GuestMemory::Write, as a CP write or a DMA copy or fill makes it), a GPU result stored by the driver (StoreOwnBytes) and a pending GPU write with a known value make it read again, and the new T# is in the snapshot; - a MarkWritten stamp (a GPU write into an import), a pending write the capture syncs on and a guest mapping change make it compared again; - after the reuses, the map selects each key's record; - a table outside the write watch is compared and reused, and a store into it is seen; - the memo keeps at most 16 columns. Without write watching, the captures of an unchanged table expect a compared reuse instead.
…table column Every read and store through GuestMemory calls the flush hook first. Recorder's FlushForAccess waits for recorded GPU work over the range, records a queued label that writes it, publishes the unit shadows over it and stores the storage image results kept on the GPU for it. A storage image result is stamped only when it is stored (its write-back's MarkWritten), so ShaderMemory's stamp and check, which never read the column, could vouch for words a read would have replaced: the column skipped its read and the snapshot kept the T#s from before the result. stampWrites and unchangedSince now call GuestMemory::FlushGpuWrites over the range after the pending write query and before the tracker, as a read does, and as the dispatch cache stores pending images before it validates an entry. The stamp then covers what the flush stored, so the capture after it skips the read again. agc_shader_table_snapshot_reuse installs a flush hook that stores a new T# into the table on its first call over it. Without the flush in unchangedSince the reused snapshot keeps the old T#; without the one in stampWrites the next capture compares the table instead of skipping its read.
…read A kept column the runtime cannot vouch for is read and compared with its kept words. When they differ, the fresh walk read the same runs again, so a capture that follows every change to a table read each word twice: 1,048,576 words per capture for a 65,536-record column, 12% slower than before the reuse with the write watch and 16% slower outside it. The compare now leaves its words with the snapshotter, and the fresh walk takes them instead of reading when its plan has the same runs: words the capture has already read read the same again. Such a capture reads each word once, as before the reuse, and the 65,536-record column costs 7% (12% outside the write watch) more than before the reuse; the rest is keeping the column. With 16 distinct T#s it costs 5 to 8% less. agc_shader_table_snapshot_reuse expects a changed table to be read exactly once per capture.
…ot that reuses it A capture that reused a memoized image table column still copied it: each record's state and code and the distinct T#s went from the memo into the snapshot, the snapshot rebuilt its codes from the states, and the column's per-record vectors were allocated before the memo was even looked up. Every Recompile then hashed the codes and the distinct T#s word by word for the result memo. On a 32,768-record column of distinct T#s that was about 155 us of copying per capture and 410 us of hashing per Recompile here, for a column nothing wrote. A column is now built once into immutable shared values (SharedValues: its codes, its distinct T#s and its read ranges) and the memo keeps that column. A capture that reuses it takes a reference, so two captures of an unchanged table share one column and copy nothing, and the per-record vectors are allocated only when the column is walked. The codes index the column's own distinct T#s (ImageTableColumnSnapshot::words) instead of a list shared by the shader's columns, so every column of a shader is shared whole, not only the first. A column read again after a write is a new one: the snapshots holding the old one keep it unchanged. The result memo hashes each shared value once and keeps the hash in it; equal contents hash equal whether they are shared or not. ImageTableReuseCounts gains the reuses that shared a column and the columns built.
… shader memory When the write tracker vouched for an unwritten image table column, the capture still copied each of its words into ShaderMemory's pages (supplyWords) so they would be in the captured regions, and Regions() then found them again bit by bit. On a 32,768-record column that is a 1 MiB copy per capture (about 120 us here) and a scan of 262,144 read bits per Regions() call (about 230 us), and the draw path calls TakeRecentRegions() and Regions() once each per stage. The memo now keeps a column's words in one immutable block (SrtGuestWords), and supplyWords hands that block over. ShaderMemory keeps a reference and reports its runs page by page, the regions a read of the same words reports, merged in address order with the pages and the initial regions; a second stage that supplies the same block only marks it recent again. The words reach pages only when they must: a later read in the same ShaderMemory that touches them first moves the whole block into its pages (spilled), so a word is never reported twice, and a block that overlaps pages already fetched or another block's runs is copied as before. A spilled block stays referenced until the ShaderMemory goes, so the regions already taken from it stay valid. ShaderMemory::Supplies() counts the words shared, copied and spilled.
…e on a result memo miss A result memo miss resolves every image table of the snapshot again: ResolveImageTables decodes each distinct T#, builds the element and poison indices and fills one map record per key, which is most of a miss for a large table. A miss whose table did not change is common: another buffer, flat SRT word or user data word changes the result key while the column is the one the column memo shares. ResolveImageTablesShared keeps the last resolutions and hands one out by reference when the variant (its id), the reducing state of the direct samplers paired with table images and every column match: base, size, records, keys, fault, outside, and the column's codes and distinct T#s by identity. The memo holds those shared values, so a block it compares cannot be freed and reused for other words; a column read again is a new block and resolves again even when its words are equal. It keeps at most 4 resolutions per variant and 256 in all within 8 Mi words, counting the columns their keys keep alive, least recently used first out, under one mutex. A resolution is immutable once made and is shared between threads read-only. APS5_NO_TABLE_RESOLUTION_REUSE=1 turns it off. ImageTableReuseCounts gains the resolutions made and the ones shared.
…e draw cache The draw cache sets each fresh stage capture against the variants it keeps (insertDrawEntry), and the dispatch cache sets a fresh capture against its front variant, by comparing every captured word. A 32,768-record table column is 262,144 of them, so each compare went through 1 MiB on each side even when both captures had copied the column from the same shared block. A variant now records the words it copied from a block the shader memory shares (SharedWords: where they sit in the capture and in the block, and a weak reference to the block; consecutive pages of one block join into one entry). SameCapturedWords takes the words both captures copied from the same place of the same block as equal, since a block never changes, and compares the words around them as before. Two blocks with equal words, words read into pages and a released block fall back to the word compare. The reference is weak, so a kept variant holds no block alive, and a released block's identity cannot pass to a new one: its control block stays until the last weak reference goes.
…e's regions cacheDrawStages and insertDispatch grew a new variant's words one region at a time. A table column read from pages, or shared and reported per page, is one region per 4 KiB page, so the words of a 32,768-record column grew through 256 resizes and about eight reallocations, each copying what was already there. Both now reserve the runs and the words once before copying. Built the same way in the image table microbenchmark, a 262,156-word capture's variant took a median of 191 us growing per region and 79 us reserved.
…rites before stamping A table column's stamp vouches that nothing wrote its memory after the stamp's generation and nothing was pending at the time. stampWrites asked for pending writes before it took the generation, and two GPU write paths stamped their blocks before they noted the write pending: a label or store kept behind completions, and a DMA copy. A capture on another queue worker that ran between those two steps saw no pending write and took a generation newer than the write's stamp, so once the batch was reaped the column vouched for the bytes from before the write. stampWrites now flushes, takes the layout and generation, and asks for pending writes last. The label path notes the completion before it stamps the store. The DMA copy still stamps first, so a newer copy's ring entry never meets an older one's generation, then notes the write pending and stamps again, and its ring entry carries the second generation. A store noted after the pending query is now always stamped after the generation the column keeps. The new two-thread test stamps a GPU write from another thread while the capture asks for pending writes; the next capture must compare the table instead of reusing it.
… before stamping it An unwritten column was vouched for run by run: ShaderMemory::unchangedSince, with its pending write query, flush hook, collect and tracker lock, once per contiguous run of the words the column read. A heap whose records are wider than the descriptor they hold, like Resident Evil Village's T#s of 8 dwords at a 48-byte stride, makes every record its own run: 32,768 calls per reuse there. Each capture also took a new stamp before it looked at the memo, so a hit paid the pending query, flush and collect twice. The memo's words now carry the range their runs span, and a reuse vouches for that range in one call. The range covers more than the runs (the bytes between records), so a store between two records refuses the vouch and the column is compared instead: still exact, never stale. A hit is tried before the capture stamps the column; the stamp is taken only when the memo cannot be reused unwritten. ShaderMemory::Vouches counts the vouch calls. The test reads a strided table of 4,096 records and checks that its reuse vouches in one call, that a store between records compares and keeps the column, and that a store into a record reads it again.
…ck's vouch A cache hit compared every captured region against guest memory, the table's words included: about 1 MiB and 32,768 regions for Village's heap, 77-81 us per compare. When a capture shared a column's words by reference (the memo's block), the variant records that block for those words, and an unwritten block now vouches for them instead: one ShaderMemory::Vouched call per block (the layout, no pending write, the flush hook, collect and UnchangedSince over the block's span), then validateCaptured skips the regions the block holds. Regions noted pending are always compared, and so is everything when the block is gone or refused. The block keeps its stamp in a shared cell instead of the memo, so a compare that finds the column unchanged after a store elsewhere in its span renews the stamp for every variant sharing the block, not just for the thread that compared it. The cell is a sequence lock: a reader that races a renewal gets no stamp and compares. APS5_NO_VOUCHED_VALIDATION=1 turns the vouch off. ShaderMemory::Vouches counts vouched and refused blocks and the words they covered. Tests: VouchSharedRegions over a strided table (only the table's regions, one block, one call), refused for a pending GPU write into a record or between records, a changed mapping, CPU, driver and GPU-result stores, a store between records until a compare renews the stamp, and a released block; and agc_driver_image_table_vouched_lookups, which dispatches over a write-watched heap (APS5_WRITE_WATCH_IMPORTS=watch, as on Windows) and checks that hits are vouched for and that a CPU store, a CP WRITE_DATA pending ahead of the dispatch and a driver store each refuse the vouch and capture the table again.
…without the registry mutex Every guest read through GuestMemory (and every table vouch) runs the recorder's flush hook, which asks the storage-texture registry for pending images over the range: under the registry's mutex, even when no pending image is anywhere near it. With queue workers recording and storing images, that mutex is contended: a Village-like bench (three workers, a pending storage image per submission) spent 72 us per hook call waiting for it, 200 us per table vouch. The registry now publishes the merged ranges of its pending images with the version they were built from, rebuilt under the mutex as before. overlappingPending answers a range that no published range overlaps without the mutex, when the published version is the registry's current one; any other case takes the mutex as before. A change bumps the current version with release order before the mutex is released, so a reader that could see the change through any other path cannot read the old ranges as current. In the bench the hook drops from 65-81 us to 4.5 us per call and a table vouch from about 200 us to 23 us. The existing storage-texture tests cover the pending images the hook must still find.
…ion at a time A table of strided records is one captured region per record: 32,768 regions for Village's heap. Validation by vouch still walked them all: VouchSharedRegions marked each one, and validateCaptured asked the pending view about each one and tested its mark, about 100 us and 150 us per hit in the Village-like bench. Each shared run now records the regions it was built from (AppendSharedWords takes the region's index and joins only consecutive regions), so VouchSharedRegions returns ranges of regions with their word count and the span of their block, one per shared run. validateCaptured asks the pending view once per range, over the block's span, and skips the range in one step when nothing is pending there; a range the view overlaps is checked region by region as before. The identity test checks that runs of regions that are not consecutive stay apart.
… walk its runs validateVariant asked about every run of the variant before it compared anything: the pending view for each run (syncPendingRuns), the storage-texture registry and the unit shadows for all runs at once, and every queued label against every run. With a strided table that is 32,768 runs per hit, about 3.2 ms per validation in the Village-like bench, all of it spent finding nothing. The shared blocks are now vouched for first, and a vouched range whose block span the pending view overlaps is dropped from the vouch. The other checks then run over the variant's other runs plus one range per vouched block, its span: the same questions over a superset of the bytes, so anything they would have found over the table's runs they still find. When the registry or the shadows report an image over those ranges, the vouch is dropped and the checks run per run as before. In the bench a hit's validation drops from about 3.2 ms to 25 us.
Every unwritten reuse handed the column's block to ShaderMemory::supplyWords, which checked the block's runs (each in order, aligned, not overlapping, adding up to its words) and asked GuestMemory::Accessible once per group of pages: one pass over 32,768 runs per capture for Village's heap, about 200 us in the Village-like bench. The column memo checks the runs once when it keeps them and marks the block ordered, so a supply takes the block's span and word count as they are. Accessibility is asked once for the span's pages; only when that fails (a page between records unmapped, which a stride wider than a page can leave) are the runs' pages asked group by group as before. The supply drops to about 3 us. The table vouch test checks that a strided table is kept as one ordered block over its records' span.
…ing each one again A reused table (FindReusedTable, TableCurrent and CachedTableTextures all hold) still bound every element through the same path as a fresh one: the filter-minmax, border-swizzle and seamless-cube requirements, and for each sampler the element is used with, a decode of the sampler's words for the border and anisotropy checks. For a table of 4,096 elements that was most of a hit, about 1.2 ms per table on lavapipe. Those checks depend only on the element (its words, resource and texture, which a reused table holds unchanged), the sampler mask the binding gives the element, and the stage's sampler sources. A table now records, for each element, whether its bind succeeded or the message that rejected it, with the binding's sampler masks and the stage's sampler words and flags. A reuse whose masks and sampler sources are the same replays that: a bound element takes its texture, layer flag and described range directly, a rejected one is rejected with the recorded message in the same order, so the fault codes are the ones a full bind assigns. Any other reuse binds every element with the checks and records the outcome for the next. APS5_NO_TABLE_REPLAY=1 turns the replay off. ImageTableReuses reports the replayed tables (also counted as reused). The image table reuse test checks that the three dispatches of one table replay twice; every case it samples runs both with and without the replay.
…ng a table it still holds An image table binding holds one descriptor per distinct T# of the table (4,096 for a heap of 32,768 strided records), more than the shared pool chain takes, so every resource build created a dedicated descriptor pool, allocated the set from it, wrote every table descriptor, and destroyed the pool when the resources were released: 8-11 ms per build on lavapipe for the pool and set alone. A released dedicated set is now kept (up to eight, APS5_KEPT_DESCRIPTOR_SETS, 0 turns it off) with a record of the table descriptors it holds: the written runs, each element's view and layout, and the textures behind them, which the record keeps alive so a view handle cannot be reused while the set refers to it. A build whose layout, pool sizes and variable count match a kept set takes it instead of creating a pool, and when the table descriptors it would write are exactly the ones the set holds (same runs, same views, same layouts) the table's writes are left out of vkUpdateDescriptorSets; every other binding is written as before, and the full write list is kept for the draw snapshot sets that replay it. A set is released only after the batches that used it completed (the recorder keeps the resources), so a kept set is idle when it is rewritten. What invalidates a kept table: any element whose texture was replaced (a new view), whose image layout changed, or a different set of elements; each makes the build write the whole table. ImageTableReuses reports kept and made table writes. The image table reuse test checks three dispatches of one table keep their writes, a texture cache drop makes one write and keeps the next, and two tables alternating make every write; it samples every case. The descriptor cache unit test now expects a freed oversized set to be kept and taken back by a set of the same layout and sizes, a set of other sizes to get its own pool, at most eight kept pools, and none left when the cache goes.
…t at every build Every build of an image table binding made a record of the table descriptors it wrote, so that a set kept after release could later skip rewriting them: a reference to each element's texture and a copy of each element's image info. For 4,096 elements that was 4,096 shared_ptr copies and a 96 KiB copy per build, about 70 us in the Village-like bench (table image infos 112 us per build against main's 40 us), paid by every build, including those whose set came from the pool chain and is never kept. A build now notes only where its table's image infos sit among the ones it wrote, and the runs it wrote, and only for a set from a dedicated pool. When it took a kept set, it compares that set's record with its own infos in place. The record is made when the resources are released and the set is kept: the runs, a copy of the infos, and the build's texture references moved over once and shared by both sets' records, so the textures stay alive while the set refers to them. A record is made only once the build's descriptor writes were issued; a build that threw before vkUpdateDescriptorSets leaves its set without a record, so a later build writes the whole table instead of trusting writes that never happened. Before, the record was set before the call. The kept and made table writes are counted as before for sets from a dedicated pool; a set from the pool chain is no longer counted. The image table reuse test's kept and made writes and the descriptor cache unit test's kept-set contract are unchanged and pass.
…witched off The shader disk cache keys every plan and variant with the recompiler's APS5_* switches that are set (ShaderDiskCache's switch key), except the ones listed as neutral. APS5_NO_TABLE_SNAPSHOT_REUSE and APS5_NO_TABLE_RESOLUTION_REUSE were not listed, so turning the table reuse off for a comparison run gave every shader a new key: a cache filled with the reuse on missed throughout the boot, and the other way round, and the comparison measured recompiles instead of the reuse. Both switches only choose between building a column or a resolution and taking the same contents by reference, so the output does not depend on them: they are now neutral switches. The cache format is unchanged and FormatVersion stays as it is; only a process that sets one of them now finds the entries stored without it. The plan loading test gains a process with both switches set, which must load every stored plan and variant without running the front end and produce the same output as the first process. Before this change it failed with every plan and variant missed (17 plan misses, 17 variant misses); now it loads all 17 with the same output.
Brings the series onto main 4e9a2a8 (boykopovar#1193, boykopovar#1201, boykopovar#1202, boykopovar#6060, boykopovar#6333 and the descriptor word tracking of 682b07e). Resolutions: - ShaderMemory: the column memo's supplied and spilled blocks now sit on boykopovar#1193's page vector, page pool and word masks. Pages are found by binary search, since a spilled block can add many pages, and a block placed into a lazily read page fills the rest of the 16-word blocks it touches, so a later lazy read never overwrites placed words. - ResourceMaterializer and SharedValues: the selected-record path and the words index carry availableDwordMask, as the snapshot hash does. A selected record's dwords past the end of its buffer count as read zeros, which is what a scalar buffer load returns there, so a key past the heap still selects a null image without reading the heap. - DescriptorCache: kept table sets live beside boykopovar#1201's recycled sets. - Tests and CMake: both sides' tests are kept.
A table vouch and a capture's stamp ask the pending-write query about a table's whole span, gaps between records included. With pipelined draws that query drains queue 0 when a queued draw writes anywhere in the range, so a vouch could wait for draws that main's per-record checks never wait for. Whole-span checks now go through Driver::QueryPendingSpan, which treats a queued draw writing into the span as a pending write and refuses instead of draining. The cached variant or the capture then falls back to the per-record checks, which drain exactly where they did before.
TableRequest keeps a span of the user data it is given, so the request must not be built from a temporary.
…to unwatched memory A driver store and a GPU result stored by the driver, each across the last record of a write-watched heap into unwatched memory after it, must refuse the table's vouch and make the next capture read the table again. Without the stamp of a partly watched store's watched blocks, the vouch still passes.
…er-image-table-reuse-v3-pr
…mage-table-reuse-v3-pr
|
This pull request is labeled |
…tamp The first vouch's pending-write query created the driver, and the driver's construction ends a guest-allocation mutation, which moves the layout generation the stamp holds. The second vouch then refused the table on a layout change the queued draws didn't make. The test now creates the driver first, so both vouches compare the stamp against the layout the draws run under.
Keeps this branch's tagVariantRuns declaration next to main's dumpRequest suffix parameter in Driver.hpp, and reads boykopovar#6232's pointer-chain test words from its table's snapshot, where this branch keeps them.
The data-hit verification keeps this branch's pending-span query on its ShaderMemory and takes boykopovar#5958's CaptureInvocation, which retries a capture with converted samples.
adriadam10
left a comment
There was a problem hiding this comment.
Thanks for the detailed numbers and for the Village/007 runs. Three things block this head:
- Merge with current
maindoesn't build/merge cleanly.Driver.hpp: keep bothdumpRequest(..., std::string_view suffix = {})andtagVariantRuns.Dispatch/DataHitVerification.cpp: the data-hit capture is nowCaptureInvocation(*verifyMemory, ...); build theShaderMemorywith&QueryPendingSpanas before.tests/ShaderMemory.cpp:3386(verifyChainedTableOffset, from #7527) still readstables.words; with per-column words it must readsnapshot.words.
agc_driver_image_table_vouch_queued_drawsfails on CI's lavapipe (Mesa 25.2.8 / LLVM 20.1.2) on the merged tree, atImageTableCacheBounds.cpp:319: "a queued draw writing outside the table: the table was not vouched for". Your own description reports the same failure once, passing afterwards. A test of this PR that fails intermittently will turn the Linux job red for everyone, so please find the cause (anelsewhererange atOutputOffsetsharing a write-tracked page/granule with the table, orQueryPendingSpanbeing coarser than the span, would explain a layout-dependent result) and make the expectation deterministic.- Please keep the
perflabel's requirement in mind: the bot asks for bothaps5-summary.jsonllines for each capture and the traces; the body has the lines for one pair, the Village A/B only has the figures.
Once these are fixed and CI is green on the new head we'll look at the rest again.
|
Current-main Windows integration update: 9f9a532 on main 876bd17 passes 90/90 selected native checks (32.37 s), including shared-table reuse, vouched lookups/queued draws, image-table cases, draw relocation, shutdown, and the new APR read tests. Public stack: https://github.com/Scramblrrrr/AnyPS5/tree/codex/first-light-oct18-resume-main . Title run is still cold-starting; no new FPS claim. Two test-only adaptations for newer main if useful:
AI-assisted: Codex; raw title data remains local. |
The pending storage list from main (boykopovar#6454) publishes its range snapshot through std::atomic<std::shared_ptr>, a C++20 library feature that Apple's libc++ does not provide, so the AGC driver stopped building on macOS. Where __cpp_lib_atomic_shared_ptr is not defined, the snapshot is a plain shared_ptr read and written with std::atomic_load_explicit and std::atomic_store_explicit, which give the same acquire and release ordering. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…otection agc_shader_table_snapshot_reuse, new in main with boykopovar#6454, protects one page of a write-watched table through GuestArenaSetProtection and expects the next table read to see it as unmapped. On macOS the write watch keeps its own record of each watched page's protection, and the AGC page query trusts that record over the host's, so the page still read as readable and the stale snapshot was reused. The guest's mprotect already reports protection changes to the watch; the arena's own protection call now does too. The watch ignores the report on other hosts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ted pool The image table reuse test (boykopovar#6454) expects three dispatches of one table to keep their table descriptor writes. Kept sets, by that PR's design, are only the sets from dedicated pools, which a table gets when its descriptors do not fit the shared pool chain; a set from the chain is not counted. On MoltenVK the sampler table is small (192 descriptors), the set fits the chain, and no write is counted, so the test failed although the driver did what it was designed to do. The test now reads the descriptor cache's dedicated pool count: with dedicated pools it checks the kept and made writes as before, and without them it checks that no write was counted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings combo19 (async lane, gpuind, chain-A, 1a f32 mix, boykopovar#7489, S1, bindless images, staging diff, HTILE work, epoch 5) up to upstream main of 2026-10-19. 118 conflicted files, resolved by area; notes in work/research/structural/merge19/notes-*.md. - Versions: FormatVersion 48, RuntimeAbi 18, request serialization 37 (upstream's 31..36 as upstream writes them, our single-workgroup flag is 37, gpuind's trailing block last; includes b75c4e6's fix). - Image tables: upstream's boykopovar#6454 reuse model, scratch indexes, selected records and pointer chains, with our bindless columns and resolve memo in front. - Pairs kept with ours as the default: boykopovar#6210 presenter epoch only with APS5_PRESENT_EPOCH=1; boykopovar#6402 table stores (our path); pending HTILE fills only with APS5_PENDING_HTILE_FILLS=1. boykopovar#6208 takes upstream's final code with DMA pipelined by default (APS5_NO_PIPELINE_DMA=1 off). AI-assisted: yes
What
A bindless heap like Resident Evil Village's (32,768 T# records at a 48-byte stride, 4,096 distinct textures) was read, compared, resolved and written again for every dispatch or draw that used it, even when nothing had written it. In Resident Evil Village's menu (PPSA01556, discussion #5169) on the PC's tree with draws not pipelined,
capture_resourcescosts 1,319.7 ms per frame, about 1.58 ms per draw over 835 draws per frame. In a Resident Evil Village-like bench with three queue workers onmain(lavapipe), a cached dispatch spent about 36 ms comparing the table, a resource build about 12 ms resolving it, and about 9 ms creating a descriptor pool and set for it.This keeps one immutable object per heap, record shape and write epoch, and has every stage hold it by reference:
vkUpdateDescriptorSetswhen the set already holds the same views and layouts.With pipelined draws, a vouch never waits for queued draws. A queued draw that writes anywhere in a block's span refuses the vouch, and the per-run checks then drain exactly where they did before.
Every check uses the same evidence as before, over a superset of the bytes:
WRITE_DATA, a compute or storage-image result, or an unwatched range falls back to reading and comparing exactly as before.Each case has a test.
APS5_NO_TABLE_SNAPSHOT_REUSE=1turns the reuse off.APS5_NO_VOUCHED_VALIDATION=1,APS5_NO_TABLE_REPLAY=1andAPS5_KEPT_DESCRIPTOR_SETS=0turn the later stages off one at a time.It also closes two stamp ordering races found on the way:
The flush hook's "is an image pending here" is answered from a published snapshot instead of the storage registry's mutex, which three workers contended at 65-81 µs per call. The two reuse switches no longer change the shader disk cache's keys, so turning them off doesn't rebuild the cache.
FormatVersionstays at #6243's 37, since no plan or table encoding changes, and the runtime ABI stays at 12.#6243, #6455 and #6537, whose heads this branch merged, are all in
main, so the diff holds only this PR's commits.Overlap
Seven open PRs change the same image table code. Merges that run this PR with #6440 and #6340 are on the fork:
test/v3-x-6440,test/v3-x-6340andtest/v3-x-6440-6340.ResourceMaterializer).ResourceMaterializer.cpp/.hppandtests/ShaderMemory.cpp.test/v3-x-6440passesctest -R '^agc'606/606 with both PRs' tests. Two things git doesn't flag: perf(shader): reuse an image table column's records while its words are unchanged #6440's nestedColumnMemo,ColumnKey()andColumnMemoEntrieswould shadow this PR's names (and its 512-entry limit would replace this PR's 16), so the merge renames perf(shader): reuse an image table column's records while its words are unchanged #6440's; and perf(shader): reuse an image table column's records while its words are unchanged #6440's memo-count test runs withAPS5_NO_TABLE_SNAPSHOT_REUSE=1, since this PR's compare otherwise serves every repeated compile first.ShaderResources::resolveImageTables).resolveImageTables; the composed resolution is ontest/v3-x-6340.mainkeeps that guard in the reuse path: a table with a written entry, or one whose set has no sampled table binding, takes the fresh path, as onmain. It still conflicts with perf(agc): reuse eligible image table entries independently #6340 inresolveImageTables.ControlFlowInfo.hpp,ResourcePlan.hpp,ResourceMaterializer.cpp,Recompiler.cppandtests/ShaderMemory.cppare unions of both sides, plus four changes git doesn't flag:WordsIndex(records, value)inLaneRecords, areturn;in thevoidReadColumn,fieldCount<TableColumn>() == 19, and the next freeFormatVersion.docs/dev/debt/libSceAgcDriver.md. Its checks cost 0.2–0.3 µs per entry under the load below, less than the 0.7–1.2 µs mapping check they replace.main,DescriptorCache::AllocateRewrittenhands out any recycled set of a layout, and fix(agc): reuse a recycled descriptor set only for a table of its own size #6567 keys recycled sets by layout and variable count. This PR's kept dedicated sets already match the variable count, and its rewritten tables take their recycled sets from that map, so fix(agc): reuse a recycled descriptor set only for a table of its own size #6567's fix covers them too, with no duplicated work. They conflict inShaderResources.hpp(SetAllocationand therecycledmap's key:variableCountis astd::optionalhere and a count there) and inShaderResources.cpp(the returns ofAllocate). Whichever merges second keeps this PR'sSetAllocationfields (layout,sizes, the optionalvariableCountandtable) and itskeptlist, keysrecycledby layout andvariableCount.value_or(0)in bothAllocateRewrittenandRecycle, and setsvariableCountin each ofAllocate's returns.main,PrepareDrawBindingsmatched recorded descriptor writes by binding number alone, so the fragment table's writes (binding 260 in set 1) went into the vertex table's snapshot set 0. SleglJan's runs of 007 First Light (PPSA11386) with this PR reached that pairing before the menu: all 12 runs ended on SIGSEGV, withVUID-VkWriteDescriptorSet-dstArrayElement-00321(339 table descriptors into a set allocated for 1). With fix(agc): keep each set's image table writes in its own draw snapshot… #6537 added, 3 of 3 reached the menu at 2.90 fps, against 0.68–0.74 fps with neither. This branch merges fix(agc): keep each set's image table writes in its own draw snapshot… #6537's head (AnthonyClemens's commit, unchanged), so the two can't land apart.verifyVolumeBiasTableRejectionintests/ShaderMemory.cppdidn't compile against this PR's snapshot layout, wherecodesandwordsare per-columnSharedValues(SleglJan's report on fix(shader): poison unsupported biased volume table entries #6881). Scramblrrrr's 913b936 adapts the fixture's two lines; it's cherry-picked here as 209f3ce, right after this branch's merge ofmain9caecfd (dad24e3).Other conflicts, in shared lines (an overlap check against
main668a7dd and the open PRs, 2026-10-16). Whichever merges second keeps both sides:DescriptorCache's constructor keeps this PR'sKeptSets()helper andkept.reservebody with fix(agc): free released lavapipe descriptor sets #6614'sfreeDescriptorSets(...)initializer. fix(agc): free released lavapipe descriptor sets #6614 frees sets on the chain-pool path, and this PR's kept sets are dedicated pools, so the two don't touch the same sets.DrawCapture.cppthat builds a draw variant's runs and words withtagVariantRuns, and added a user-data-use pointer to the SRT walker's options. This branch mergesmainwith it:tagVariantRunsalso records this PR's shared words (AppendSharedWords, from the capture's supplied blocks) for each run it appends, and the walker's options keep both sets of fields. The first merge ofmainrecorded the shared words by capture region, which names the wrong run once relocation splits a region; e448df1 records them by run, so a split variant's runs are validated as before (SplitRunsNameTheirSharedWords).tagVariantRuns: keep perf(agc): relocate cached draws across user words that only reach the shader as values #6539's forwarded-word block before the loop, and this branch's loop that records the shared words by run. Both add includes at the top oftests/execution/DrawCacheRelocation.cpp; keep both.DrawStageVariantand renames the variant merge inDrawCache.cpp. Move this PR's shared-words build intoDrawStageVariant, which then needs the capture's supplied blocks, keep this PR's comparison of a kept variant (push offset, mesh index descriptor, runs and captured words) under perf(agc): compile uncached draws ahead of queue 0 #6188's names, and keep both declarations inDriver.hpp.ShaderMemorywith this PR'sQueryPendingSpanand captures through feat(shader): sample converted unorm images in the shader #5958'sCaptureInvocation.Driver.hpp, kept both in this branch's merge ofmain.dumpRequest's declaration inDriver.hpp, next to this PR'stagVariantRuns. Keep both lines.EntryHeaderBytesnext toFormatVersioninShaderDiskCache.hpp. Keep it, and this PR'sFormatVersion.main.main3de84fa (db622e1) reworksTableSnapshotter::WordsIndex(records, value)on perf(recompiler): keep image-table indexes in per-thread scratch #7193'sHashScratchIndex, indexing the column's ownrecords.distinct, since this PR keeps a snapshot's words per table, with a newHashScratchIndex::Clear()called for each column.ResolveImageTablesuses perf(recompiler): keep image-table indexes in per-thread scratch #7193'sDenseScratchMap, sized by the largest column's word count and cleared per table, readingcolumn.words.at(code). perf(recompiler): keep image-table indexes in per-thread scratch #7193'sverifyImageTableScratchReusecounts the distinct records assnapshot.tables.at(0).words.size().More general than this PR, for a follow-up: #6340's per-entry reuse (this PR's replay is all or nothing per table; a follow-up would add per-entry reuse and keep the whole-table replay as the fast path when every entry is current), and from #6440 the layout reuse on changed words, narrowed columns and a process-wide memo. A follow-up could take them into this PR's memo. In the other direction, #6440 and #6340 have no unwritten-heap hit, identity share or kept descriptor sets.
Disk cache format:
mainhasFormatVersion37 since #6243, and this branch keeps it: its own disk-cache change only keeps its two reuse switches out of the cache keys. #6280, #6412 and #6488 also set 37, so each needs the next number; git won't flag it.Benches (one Linux VM, lavapipe, trees alternated).
A 19,000-record column, per
ShaderMemory::Capture, p50 / p90:¹ With #6440, 125 of the 200 captures hit and 75 miss, so the p50 varies by round. The mean per capture: 2.93 ms (main), 3.00 ms (#6440), 3.15 ms (this PR), 2.68 ms (this PR + #6440).
A Resident Evil Village-like load (a 32,768-record heap, three queues, 21 s runs):
resolveImageTables, p50 per buildWith an unwatched heap (the Linux default for imports) every tree measures 30–35 table dispatches/s.
Merge order: either works. #6440 and #6340 don't conflict with each other, and each conflicts with this PR in its own file. If they land first, this PR takes the resolutions on the three
test/v3-x-*branches; if this PR lands first, the same resolutions apply to them. #6481 and #6488 don't conflict with #6440, #6340 or #6402, except #6488 with #6440 intests/ShaderMemory.cpp.Tested
CI's Linux job, mirrored on a Linux cloud worker at fae2221 (Ubuntu 24.04, GCC 13.3, CI's configure flags, lavapipe): 1,279 tests, 1,213 pass and 61 skip. The 5 failures are network tests that need IPv6, which that machine lacks, and
check_skips.pyflags onlyguest_sce_net, skipped for the same reason.check_test_registration.pypasses.GitHub runners:
drawSnapshotTableSetTests(agc_driver_recorder_tests --draw-descriptors-only, registered for that check only) passes on Linux lavapipe (run). Both runs are marked failed only forguest_kqueueon Windows, and the first also forprecise_waitin the libc suite on Linux; this change touches neither.agc_draw_descriptors):agc_draw_descriptors,agc_driver_draw_cache_relocationandagc_driver_image_table_reusepass 3 of 3 on Linux, and skip on Windows without a Vulkan device (run).agc_shader_table_snapshot_reuse,agc_write_trackingand the relatedagctests pass 20 of 20 on Linux and Windows (run, run).^agcsweep at 5a31a9c, next to the same sweep ofmain4186f06 on the same runners (this branch, main): Linux 603 of 604 (41 skip), the one failure the known parallel-load flakeagc_driver_preparation_scope_settle, againstmain's 601 of 601. Windows 597 of 598 (465 skip without a Vulkan device) againstmain's 594 of 595, both failing onlyagc_driver_pm4_replay_tool(0xc0000139).main, d21b080) pass the snapshot reuse and related tests on both runners.guest_kqueuefails, which fails onmainon the same runners.Windows 11, NVIDIA GeForce RTX 3070 Laptop GPU (driver 617.42), on
main66e290f with this PR, perf(agc): settle Windows imports under 1 MiB without waiting #6678 and fix(agc): keep a compute data hit's live words with the regions that span them #6911 merged (the other two don't touch this path):agc_driver_image_table_reusepasses 5 of 5, with the Windows counts of its CPU-write cases (3 - CpuWriteResolutionsreuses withCpuWriteResolutionsresolutions for "a CPU write collected in a new epoch, then two dispatches").The Windows runner found that the snapshot reuse test's page-protection case never made the page inaccessible on Windows: the arena call it used changes only section views there, while the guest's
mprotectalso callsVirtualProtect. The test now protects the page the waymprotectdoes on each platform, and adds a table mapped again with new contents. With the vouch's allocation-generation check removed, the test fails on both runners (red: "a table was reused without a read after a guest mapping changed"); with it, it passes 20 of 20 on both (green).Ubuntu 24.04, GCC 13.3, lavapipe (llvmpipe, Mesa 25.2.8), the tree before the test fix above, merged with
main4e9a2a8 and with draws pipelined atmain's default:ctest -R '^agc'passes 599 of 600 (41 skip on lavapipe). The one failure is the known parallel-load flakeagc_driver_preparation_scope_settle, which passes 5 of 5 alone and fails the same way onmain4e9a2a8. Stage 1's head and the head of stages 2-3 also passed the suite on their own, before the merge withmain.New and extended tests:
agc_driver_image_table_vouched_lookups: dispatches over a write-watched heap are vouched for. A CPU store, a CPWRITE_DATApending ahead of the dispatch, and a driver store each refuse the vouch and capture again;agc_driver_image_table_vouch_queued_draws: with real queue-0 pipeline commits held, a vouch over a table is refused, without waiting, when a queued draw writes between two records or into a record. It is given when the draw writes elsewhere, or once the draws commit;Resident Evil Village-like bench: 3 queue workers, an active recorder, a populated storage registry, pending results, and a heap of 32,768 records at stride 48, write-watched as on Windows. Xeon 2.8 GHz VM, lavapipe, on
main4e9a2a8 against this PR's code, two interleaved rounds. µs per call:With 48 heaps (each worker cycles more tables than it keeps) or a heap that isn't write-watched (Linux's default), no vouch is possible or the memo thrashes, and both sides swing by 20-30% per round. Over five rounds each, the 48 heaps' compare is 30.9 ms against
main's 31.6 ms. The unwatched heap's compare and resolution average 2% and 3.4% abovemain's, insidemain's per-round range (compare 23.5-27.3 ms against 21.3-29.0 ms). Peak memory is lower in every case except 48 heaps, where the two ranges overlap (1,391-1,493 MiB against 1,261-1,543 MiB) and this PR's sits higher.The shared words by run (e448df1):
agc_driver_draw_cache_relocation'sSplitRunsNameTheirSharedWordsgives a variant two user-data bases into one region ahead of a table's region. At the merge it fails with "the shared words of a variant whose runs were split for relocation name run 1 of 5, not the table's run 4"; with the fix it passes 5 of 5, andctest -R '^agc'passes 612 of 612 (43 skipped) on Linux. The full Linux CI job at the merge ran 1,283 tests: 1,216 passed, and the failures were network tests that need IPv6, which that machine lacks, and theagc_driver_preparation_scope_settleload flake.precise_waiton Linux, a timing test, and libkernel'sguest_kqueueon Windows, which fails onmainthere too.The merge of
main72c5eed (with perf(agc): find cached draws across moved user-data pointers #4943), on GitHub's runners (run): the draw-cache relocation, image-table, table-reuse, draw-cache reuse and BDA tests and those tied to the changed files pass 3 runs in a row on Linux (96 tests) and Windows (97 of 98; the GPU tests skip there). The only failures are libc'sprecise_waiton Linux, a timing test, and libkernel'sguest_kqueueon Windows, which fails onmainthere too.The merge of
main3cc1fc8 (a53d23f, with feat(agc): store to images selected through image tables #6402, fix(agc): free released lavapipe descriptor sets #6614 and perf(recompiler): scan image-table key buffers in bulk #7101) and of perf(shader): select the record of a T# whose offset the CPU evaluates #6243's head (9dcf408, which expects a per-lane tableimage_storeto be supported now that feat(agc): store to images selected through image tables #6402 makes it so), at 6884e9c, on GitHub's runners (run): this PR's tests and those tied to the files it changes, 3 runs in a row, and the^agctests. Linux passes all of them. On Windows the only failures areagc_driver_pm4_replay_toolandagc_shader_replay_tool, which exit with 0xc0000139 when they start. They fail the same way on this runner for an unrelated branch today (run), and this PR doesn't touch either tool.The merges of perf(shader): select the record of a T# whose offset the CPU evaluates #6243's head 8c6765b (4978871, with
mainf1af6c5 and fix(recompiler): fold chained immediate addends of image-table record offsets #7325) and ofmainb39d90b (6720211, after perf(shader): select the record of a T# whose offset the CPU evaluates #6243 merged), on GitHub's runners. The first merge adapts fix(recompiler): fold chained immediate addends of image-table record offsets #7325'sverifyChainedTableOffsetto readsnapshot.words, since this PR keeps a snapshot's words per table. At 4978871 (run) and at 6720211 (run), this PR's tests pass 3 runs in a row on Linux, as do the^agctests (688 and 694, 0 failed). On Windows the only failures areagc_driver_pm4_replay_toolandagc_shader_replay_tool, as at 6884e9c. At 6720211 the Linux suite's one failure was libc'sprecise_waittiming test; this PR doesn't touch libc.The merges of
main3de84fa (db622e1, with perf(recompiler): keep image-table indexes in per-thread scratch #7193) andmainbb2624a (a508aa0, after fix(agc): stamp the watched blocks of a store into partly watched memory #6455 merged), on GitHub's runners: this PR's tests,agc_shader_bindlesswith perf(recompiler): keep image-table indexes in per-thread scratch #7193's scratch-reuse case among them, pass 3 runs in a row on Linux, as do the^agctests (710, 0 failed at both; run, run). On Windows the only failures are the same two replay tools. At a508aa0 the Linux suite's one failure was libc'sprecise_waittiming test.The merge of
main9caecfd (dad24e3, with fix(shader): poison unsupported biased volume table entries #6881) and Scramblrrrr's 913b936 (cherry-picked as 209f3ce), on GitHub's runners (run): the^agctests pass 3 runs in a row on Linux (711, 0 failed), and the suites' only failure is libc'sprecise_waittiming test. On Windows the only failures are the same two replay tools.The merge of
main0d078f7 (54f2cf3, no conflicts) and thedocs/dev/ENVIRONMENT.mdrows for this PR's five switches (790fe18, the head;mainlists every switch there since feat(tools): add the APS5_* and ANYPS5_* environment reference and check it #6078), on GitHub's runners (run): the^agctests pass on Linux (712, 0 failed), and the libc and libkernel suites pass. On Windows the only failures areagc_driver_pm4_replay_toolandagc_shader_replay_tool, as in the runs above.The merge of
main61d3d30 (d032505, the head), on GitHub's runners (run): fix(shader): keep a shader's selected image-table records within the image and pair limits #7527's new selected-record limit test read a record's descriptor words through the snapshot's sharedwordslist. This branch keeps a snapshot's words per column, so the merge has the test read them from its column. On Linux the^agctests pass 729 of 730. The one failure was this PR'sagc_driver_image_table_vouch_queued_draws("a queued draw writing outside the table: the table was not vouched for"); it then passed 3 runs in a row with the image-table and graphics tests (run). The suites pass. On Windows, where 546 of the 730 skip without Vulkan, the only failures are the two replay tools (exit code 0xc0000139), as in the runs above.Resident Evil Village (PPSA01556), the reuse switch's A/B asked for on this PR: one build of this PR's head 4978871 (before the perf(recompiler): keep image-table indexes in per-thread scratch #7193 re-port) on
mainf1af6c5, with fix(agc): leave a stage the title's pipeline ABI cannot prepare to its first use #6366, fix(agc): fall back to pixel (0,0)'s sample locations where the device has no 2x2 grid #6451, fix(recompiler): run a pixel shader the guest does not iterate once per pixel #6470, fix(agc): execute the DB's out-of-place depth expand #6146, fix(agc): publish unit-shadow results before an address-based build maps its heaps #6682 and fix(agc): plan a heap again when its mirror's range has left the write watch #6646, which the title needs to reach its menu and first scene. NVIDIA GeForce RTX 3070 Laptop GPU (driver 617.42), Windows 11, unprofiled, warm shader cache,APS5_LAZY_SHADER_PREPARE=1 APS5_TRACE_SUMMARY_EVERY=1 APS5_NO_GPL=1 APS5_HOST_IMPORT_MIB=6400, one run per arm. A is this PR as is, B addsAPS5_NO_TABLE_SNAPSHOT_REUSE=1. Frame times are in seconds (about 0.1 fps on this build):Resident Evil Village (PPSA01556), the same A/B on the re-port (a508aa0, before the merge of
main9caecfd), with the same six PRs, setup and arms, menu only (comment):Resident Evil Village (PPSA01556, 01.210.000) at the static main menu, on an NVIDIA GeForce RTX 3070 Laptop GPU (driver 617.42), Windows 11, unprofiled, with the game window in the foreground. The pair is on Resident Evil Village's test tree of
main0c4816f, with the fixes it needs to reach the menu. A′ adds perf(shader): select the record of a T# whose offset the CPU evaluates #6243 (090c0da). B is A′ plus this branch at ab82f50, which also carries fix(agc): stamp the watched blocks of a store into partly watched memory #6455, from before the branch merged fix(agc): keep each set's image table writes in its own draw snapshot… #6537. Both ran with the same environment (APS5_HOST_IMPORT_MIB=6400 APS5_NO_GPL=1 APS5_TRACE_SUMMARY_EVERY=60, and the test tree'sAPS5_LAZY_SHADER_PREPARE=1):Driver.Packetper frameDriver.Drawper frameAbout 10%: the two runs' 60-s intervals don't overlap, and the per-frame driver time is about 10% lower throughout. It is one run per arm.
No minute was cut for paging (page reads stayed under 700/s), and neither run logged a FATAL, a null image or an invalid runtime value.
B logged one non-fatal
[gpu] guest snapshot differs from registered memoryline during boot, for a 128-byte snapshot in a live import region, which serves the GPU whatever the snapshot holds. A′ logged none. The log doesn't say which capture it was. This PR vouches for a table only while its whole range is write-watched and unwritten (the tests cover an unwatched table, a store across the heap's edge into unwatched memory and a GPU write throughMarkWritten), andmain's check also fires when the guest writes captured words between the capture and the device lock.The current head also carries fix(agc): keep each set's image table writes in its own draw snapshot… #6537.
Summary lines (
APS5_TRACE_SUMMARY_EVERY=60): one 60-s interval of each run, near the middle of that window. A′ is 9a4775d4cf and B 2393232, captured on 2026-10-16, before this branch's later merges ofmain. GPU clocks weren't recorded for this pair. The traces are available on request.tools/trace_digest.pyon the two lines:2 intervals, 1638.6 to 1762.9 s, 18 presents
NVIDIA GeForce RTX 3070 Laptop GPU, NVIDIA 617.42, Windows 10.0.26300, 3840x2160, commit 9a4775d4cf90
commit, environment, GPU, driver or OS changed at interval 2
007 First Light (PPSA11386, v01.018.000) at the main menu, run by SleglJan on Linux 7.2.10 with an NVIDIA GeForce RTX 3080 10 GB (driver 615.78.08), Ryzen 9 7950X3D, GCC 16.2.1, Release (comment). The base is
mainbc3c0fc with four open shader PRs the title needs (fix(shader): apply cube output modifiers whenever IEEE mode is off #5799, fix(shader): leave out the all-zero position distance when guest distances fill all eight #6317, fix(shader): repeat phi folding until no phi folds #6332, fix(shader): poison unsupported biased volume table entries #6881). Each arm merges PR heads as they were, this PR at 1752c4a, which carries perf(shader): select the record of a T# whose offset the CPU evaluates #6243, fix(agc): stamp the watched blocks of a store into partly watched memory #6455 and fix(agc): keep each set's image table writes in its own draw snapshot… #6537. Every build has its own warm shader cache,APS5_PROFILE_DRAW=1and scripted pad input from a save; the window is 180 s in the menu. Frame p50:draw_enqueued72 → 524 ms), behind the committer's CPU copies, which perf(agc-driver): join a buffer bound from inside a range across the ranges it crosses #5372 removes; with both, the committer takes 162 µs per commit. That's SleglJan's reading of the counters; the parts weren't tested apart.007 First Light again, on a newer
main(1b0c7cf, after perf(shader): select the record of a T# whose offset the CPU evaluates #6243, fix(agc): keep each set's image table writes in its own draw snapshot… #6537 and fix(agc): stamp the watched blocks of a store into partly watched memory #6455 merged), run by SleglJan on the same machine with the same harness (comment). The base adds fix(shader): apply cube output modifiers whenever IEEE mode is off #5799, fix(shader): leave out the all-zero position distance when guest distances fill all eight #6317, fix(shader): repeat phi folding until no phi folds #6332 and fix(shader): poison unsupported biased volume table entries #6881 to thatmain; the second arm adds perf(agc-driver): join a buffer bound from inside a range across the ranges it crosses #5372 at e539061; the third adds this PR at a508aa0, whose code the current head 790fe18 keeps (since then, besides two merges ofmain, it adds only the two-line adaptation of fix(shader): poison unsupported biased volume table entries #6881's test fixture and thedocs/dev/ENVIRONMENT.mdrows). Every arm has its own warm shader cache andAPS5_PROFILE_DRAW=1; the window is 180 s in the menu. Frame p50:Shader.DrawStage.capture_resourcesfrom 300 to 97 and 156 ms per present (the two samples), andDriver.Packet.dispatchfrom 94 to 78 and 76 ms.Onimusha: Way of the Sword (PPSA27836) at the language menu with no input, run by simonbronk on Windows 11 with an NVIDIA GeForce RTX 5070 (driver 617.42) at 3840x2160, warm shader cache, the trace summary over 30 s from 60 s after the first present (comment). The base is
main84b0d2f with fix(agc): import native multisample depth whose HTILE is not ITERATE_256 #6557, fix(agc): retire an unwritten native multisample depth surface on guest access #6727 and fix(agc): replace with a stencil operation value the pixel shader exports #6762, 6 runs; this PR merged on top, 3 runs. Frame p50 with this PR: 79.1 and 80.0 ms, inside the base's 77.9 to 84.5 ms (mean 82.0), and 71.7 ms in a run whose p90 was 129.9 ms, which simonbronk counts as noise. No measurable change in that scene.Checklist
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file.mdfiles or images added to the repository (attach them to this PR)main)agc_driver_image_table_reuse; NVIDIA GeForce RTX 3080 10 GB (driver 615.78.08), Linux, for 007 First Light (SleglJan)