FC 27 stops on Windows with Failed to load module / error 126 because this PRX is absent. Export Initialize and Terminate as no-ops and keep RequestDevices throwing until the buffer layout is known.
Co-authored-by: Cursor <[email protected]>
WindowsLoadImage held one buffer spanning every PT_LOAD and BuildSections
copied gap pages into a section, so an image whose segments sit far apart
allocated the whole gap twice: a 32 KiB input with a 2 GiB gap needed 4 GiB
of memory and 30-40 s for --windows and --macos. Segment pages are now kept
in per-range blocks and an unmapped gap becomes a section with only a
virtual size. Output is byte-identical for the existing test inputs.
sceAudioOut2ContextGetQueueLevel left out the grains that
sceAudioOut2ContextAdvance completed and no push had consumed yet. While
pushes filled the 40 ms cushion the level stayed at 0, so a title that
renders as many grains as the level leaves room for and pushes them on
another thread rendered a full queue again after each push. PPSA23226's
Wwise sink does that and stopped once more than 16 completed grains
awaited a push.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ShaderMemory::readWords copied a run straight from its page only once every word of the page was
valid. A page mapped whole fills its 16-word blocks on first use, so a run over a fresh page went
through read() one word at a time: the initial-region lookup, the page lookup and a block test
per word. Image-table columns are read this way on every capture.
readWords now copies the run's missing blocks of such a page with one memcpy per run of blocks
and serves the words from the page, marking them read and recent as before. The bytes, the blocks
copied and the regions reported are those of the word-by-word path. Pages read word by word
(pending GPU writes, partly mapped) and the initial regions keep that path.
In 007 First Light's menu the capture walk takes 801 instead of 1,179 us per capture, and 132
instead of 495 us with the image-table column memo.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An address-based build asks for every registered GPU range whenever it
rebuilds its address space, and at a full budget it was refused each
one again: a title held at its default budget counted 304,394 refusals
over about 12,000 ranges at its title screen.
A request that may not evict (the build's walk, an import when a range
is mapped) now remembers a refused run with the live bytes and the
budget at the refusal. Until live imports drop below those bytes, the
budget changes or one of the run's ranges changes, a request for that
range is answered from memory, still counted as refused and also as
remembered, and a remembered block is skipped, so a block stepped down
for the budget keeps its import across registry changes. A request
that may evict always tries again, and a new import forgets the
refusals of the runs it covers. The shutdown report and the budget
line print how many refusals were remembered.
An import covered one registered range unless adjacent ranges filled a
whole aligned 8 MiB window. A title that maps its GPU heap in 64 KiB
pieces got one import per piece from every requester: Village held
about 27,000 of them at its first scene, enough to fill the budget.
The run of an address is now the largest aligned power-of-two block,
from twice the import alignment up to the window, that holds the
address and that continuing readable ranges fill exactly.
A block over the budget steps down to the largest block that fits what
is left of it, counting what the requester may evict, and nothing is
tried when not even the address's own range fits, so a full budget
costs one refusal per request as before. A block that would replace an
import a build holds, or that fails (a refused Windows alias,
vkAllocateMemory), steps down to the next smaller block. A failed block
is remembered until one of its ranges changes, instead of pinning the
span to single ranges for the session. An address-based build no
longer asks for the ranges inside the block it just imported.
A storage image over 64 adjacent pieces now finds its block import at
write-back instead of importing every piece as a segment. The split
write-back test keeps its two ranges apart with a 64 KiB window.
The storage write-back overhang test read both surfaces straight from
guest memory after FlushAllPending and WaitIdle. A write-back through a
host import is only recorded there; a reader of guest memory calls
FlushGpuWrites, which waits for the batch that holds it. The test passed
because the split surface took the CPU write-back, whose SubmitAndWait
also drained the first surface's recorded store. With both surfaces
imported, neither store had landed when the test read them.
A run without APS5_TRACE_HOST_IMPORT had no count of the imports it made
or refused. When the driver stops, one line now gives the live imports
and the imports made, refused and failed, in total and per requester:
[gpu] host imports at shutdown: N live (M MiB); made ... (... MiB),
refused ... (... MiB), failed ...; by requester (made/refused/failed):
map a/b/c, build ..., access ..., join ..., segment ..., expand ...
It prints only when an import was made, refused or failed.
agc_driver_graphics checks the line's text.
Every host import now names the path that asked for it:
- map: the map-time import of a GPU mapping;
- build: an address-based build's walk and its region imports;
- access: a lookup through HostImportFor;
- join: a join at bind;
- segment: a storage image write-back segment;
- expand: a DB expand's store.
The APS5_TRACE_HOST_IMPORT ok line, the import failure line and the
budget refusal report end with the requester, so a traced run splits its
imports by path. HostImportCounters returns the imports made, refused
and failed per requester, with the live count and bytes.
agc_driver_graphics checks that each requester's imports, refusals and
failures are counted under it.
Draws a perspective triangle into a 4x target without attachments and
records, for every pixel and sample slot, how often the pixel shader
ran and what it read: the ancillary's sample number, POS_X_FLOAT and
POS_Y_FLOAT, and the PERSP_SAMPLE, LINEAR_SAMPLE or PERSP_CENTER
barycentrics, which are compared with the values at the expected
position. A shader the guest does not iterate must run once per pixel
and read sample 0's values and the centre, also with
POS_FLOAT_LOCATION at the sample, POS_FLOAT_ULC and a partial quad
sample mask; an iterated shader must run once per covered sample and
read that sample's values; a single-sample draw is unchanged. 2x draws
run where the device supports them. Module checks cover 2x and 4x
without a device: no SampleId in a non-iterated shader, sample 0's
offset, the guest's location table in an iterated shader (standard and
custom per-pixel locations), and no offsets without sampleRateShading.
The ancillary input's sample number (bits 11:8) was the SampleId
builtin in every pixel shader. Vulkan enables sample shading for a
shader that uses SampleId, so a shader that reads the sample field or
the whole ancillary word ran once per sample on a multisampled draw
even when the guest runs it once per pixel (PS_ITER_SAMPLE clear), and
FragCoord, and with it POS_X_FLOAT and POS_Y_FLOAT, moved to the
sample. The sample number is now SampleId only when the request says
the guest iterates the shader, and otherwise 0, the iterated sample
number of a pixel the hardware does not iterate.
The other inputs that depend on the sample follow where the console
evaluates them:
- PERSP_SAMPLE and LINEAR_SAMPLE are interpolated with
InterpolateAtOffset at sample 0's offset in a shader the guest does
not iterate, and in an iterated one at the offset of the host's
SampleId, read from a table of the guest's locations, because Vulkan
leaves plain barycentrics under sample shading at an
implementation-defined position (lavapipe uses the pixel centre).
- PERSP_CENTER and LINEAR_CENTER are interpolated at offset 0 in an
iterated shader.
- POS_X_FLOAT and POS_Y_FLOAT follow SPI_BARYC_CNTL: the centre is
floor(FragCoord) + 0.5 under sample shading, a sample position in a
shader the guest does not iterate is the centre plus sample 0's
offset, and POS_FLOAT_ULC gives the upper-left corner.
The offsets need InterpolationFunction (sampleRateShading) and the
fragment-shader barycentric path; devices without them keep the host's
default positions. The shader disk cache format goes to 39.
A pixel shader's request now says whether the guest iterates it per
sample (PA_SC_MODE_CNTL_1.PS_ITER_SAMPLE on a multisampled draw), where
SPI_BARYC_CNTL puts POS_X_FLOAT and POS_Y_FLOAT (POS_FLOAT_LOCATION,
bits 17:16, and POS_FLOAT_ULC, bit 20), and the guest's sample
locations for each pixel of the 2x2 quad when the shader reads
PERSP_SAMPLE, LINEAR_SAMPLE or a position at the sample: sample 0's
once per pixel, every sample's when iterated. The fields join the
shader cache key, the draw cache key and the request serialization
(version 31). A missing SPI_BARYC_CNTL reads as the pixel centre and
the reserved location 3 throws.
Devices with sampleRateShading now advertise the InterpolationFunction
capability, which the pixel shader validator accepts.
On a page that recorded GPU work writes, a capture reads each dword on
its own. Where the driver's evidence says that work leaves the dword
unchanged (PendingWrite::Raw), the dword is copied straight from guest
memory, without the read guard the other word reads get through
GuestMemory::Read. The raw path (29b4601564) predates the guard
(d109f3966f), so a capture whose raw word lies in a live multisampled
target's memory has read stale bytes instead of throwing since then.
The raw read now runs GuardRead over the dword first. With no target
registered that is a few atomic loads, next to the pending-write query
each raw word already makes.
The test answers Raw for every page and word, so the capture's page is
read word by word and raw, and Sync for the word reads that were already
guarded.
Since #1193 (4651980eba) a capture copies a page in 64-byte blocks as it
reads them, instead of reading the whole page with GuestMemory::Read the
first time it touches it. The page now only gets FlushGpuWrites, which
runs the flush hook but not the read guard, so a capture over a
multisampled color target's memory, whose samples are only on the host,
read the stale guest bytes silently where it threw before.
ShaderMemory::page runs GuardRead over the page where the whole-page
read stood, before the flush, as Read does. The check stays once per
page a capture touches, as before #1193. The block copies need none: a
pooled page is reset and goes through page() again, and later reads of
a page were not checked again before #1193 either.
The test captures a shader whose SRT lies in a live 4x target's memory,
then one whose SRT lies just past it, then the first again on the
pooled page.
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.
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.
Brings the series onto main 4e9a2a871 (#1193, #1201, #1202, #6060,
#6333 and the descriptor word tracking of 682b07eb0).
Resolutions:
- ShaderMemory: the column memo's supplied and spilled blocks now sit
on #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 #1201's recycled sets.
- Tests and CMake: both sides' tests are kept.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Since #6060 a settled Windows host import of private write-watched
memory stays watched, and its bytes are vouched by the write tracker's
stamps instead of compared. A GPU write that runs from such an import
into a neighbouring import that left the watch reaches the tracker as
one MarkWritten over a partly watched range. The test imports two
blocks that stay watched next to one that leaves the watch, and checks
that a driver store and an own store across the boundary stamp the
watched block they wrote, are seen over exactly its stored bytes, and
leave the other watched block unchanged.
stampWatchedPart stamped a block when the store's part of it started or
ended in watched memory. That is exact on Windows, where the watch
excludes whole 64 KiB blocks and the arena's host regions are 64 KiB
holes. On Linux and macOS the watch unregisters page ranges, so a
block can be watched only between two unwatched ranges, and a store
across all three left its watched pages with their old stamps.
Off Windows the block is now stamped when the store's part of it
overlaps any watched page (GuestWriteWatchOverlaps). A store that
overlaps no watched page at all returns after that one lookup, where
it made two coverage lookups per block before.
The test watches only the middle pages of a block: a driver store and
an own store around them must stamp it, a store into an unwatched end
must not.
The entry now sits with the other sceKernelMprotect entries for
DirectMemory.cpp, before the Windows guest module image protection one,
instead of after the setcontext entry, where #5918 adds its own.