An image_store with DIM 2D_ARRAY through a 1D T# had no runtime image
mode, so the dispatch stopped with "image descriptor is incompatible
with the static runtime image interface". #2348 added a 1D mode for
DIM 2D stores only.
Write-only 2D-array storage images now get the same 1D mode as the 2D
case. On RDNA2 (gfx1036, hardware oracle) such a store writes texel x
and ignores y when the slice is 0, and writes nothing for any other
slice, with DEPTH 0 or 3, so the store is skipped unless the slice is 0.
New execution test agc_driver_image_store_line_through_plane_array.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A buffer V#'s presence came from the dispatch's V# snapshot as the
specialization constant BufferBase + 2, which buildSpecializedModule
turns into a literal. One guest program then compiled to two SPIR-V
modules, one with the buffer bound and one with it still null, each a
separate pipeline. descriptor() now reads presence from the V# words at
run time, as registration modules already do, and the formatted-buffer
helper takes it from its valid parameter.
AI-assisted. Tested by me on my own machine.
A local program that keeps the relative patch index only on inactive
lanes, or in a vector-window slot nothing reloads, was refused as a
data use. Follow the index through the values that carry it, and
refuse only when a side effect still observes it.
A resource block behind v_cmpx_gt_u32 exec, s, v0 and s_cbranch_execz keeps
its condition only if the compare can decide it, but UGreaterThan32 and
ULessThan32 needed both operands and v0 is a lane value. When the uniform
operand is 0 (or 0xffffffff on the other side) no lane can pass the compare,
so the walk can skip the block without the lane value.
UGreaterThan32 now yields false when its first operand is 0 or its second is
0xffffffff, and ULessThan32 when its first is 0xffffffff or its second is 0,
in the SRT interpreter and in the SRT tape alike; MayDecide keeps such a
condition when either operand is evaluable. Fully evaluable compares are
unchanged.
Destruction AllStars (PPSA02630) dispatches compute shader 0x200a220000 with
a thread count of 0 and leaves the V# in the skipped block stale (one
dispatch holds 00000000 00000000 00000000 ffffffff), which stopped it at
about 40 s with "buffer descriptor uses an unsupported type".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
image_get_resinfo returns 0 in its third component when the instruction's
DIM has no layers (1D, 2D, 2D MSAA) and the T# is not 3D, as measured on
RDNA 2 in #7550. Keep 1D-array T#s on their first layer and MSAA T#s
refused, and record the unmeasured forms in the shader debt file.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
image_get_resinfo with DIM 1D on a cube, 3D or 2D-array T# was refused
with "image address has too few coordinate components": the tracker made
a 1D image that only the query uses, and no 1D mode matches such a T#.
The returned size, layer count and levels come from the T#; DIM only sets
the number of address registers. A 1D image used only by dimension
queries now decodes the T#'s own dimension and accepts 2D, 1D array,
2D array, 3D and cube views.
The flag is stored in the shader disk cache, so FormatVersion moves to 38.
PPSA01317 queries a 64x64 cube with DIM 1D in a pixel shader.
AI-assisted: yes
Table T#s keyed at runtime feeding image_atomic64_* now lower to
OpAtomic* on the table Atomic64View instead of faulting, mirroring
the 32-bit path: gate, dedup identity, binding flags and view
selection. Packed formats and BY element counts still fault; hosts
need VK_FORMAT_FEATURE_STORAGE_IMAGE_ATOMIC_BIT for the view format.
A plan copies a branch condition only as far as the evaluator reads it,
and an operation the evaluator does not read is kept without its
arguments. Two questions the walk asks still read below such a leaf.
ResolveInvariantPhi compares a phi's incoming values with
EquivalentValue, which follows their arguments, so two leaves with the
same operation looked equal in the copy where their arguments differ
in the program: a phi whose incoming values differ only below a vector
load resolved in the plan and not in the program, and the plan decided
a condition the program leaves open. LoadedFromMemory looks for a read
from memory anywhere below a raw read's handle, so a handle that
reaches memory only below a leaf lost it in the copy: a read from a
null base counted as a null root, and its condition stayed open where
the program decides it.
ownPlanValues now checks both after it copies. Each phi it copied in
part must resolve in the copy to the copy of what it resolves to in
the program, and each raw read's handle must be loaded from memory in
the copy when it is in the program. A phi that differs is copied whole;
a handle that differs keeps the arguments along its shortest path to a
read from memory (its whole closure if that path is already kept), and
the copy is made again. The checks only add values, so the loop ends,
at worst with the whole copy. IsMemoryRead names the operations that
LoadedFromMemory and the path both stop at.
Tested: agc_shader_memory's verifyPlanCopyAnswers walks a phi whose
incoming values differ only below a vector load, and a read from a null
base whose handle reaches memory only through a select arm the active
mask never takes, through the plan's tape, the plan's interpreter and
the program's own walk, and all three agree. Without the checks the
plan decides the first condition and leaves the second open.
A resource plan owns copies of the IR values its run-time evaluation
reads: the descriptor sources, the SRT reads, the uniform fills, and
the branch conditions that tell which images a dispatch can reach.
Below a scalar load from memory, the copy took every value the load
depends on, whether the evaluator can compute it or not. A condition
that reads memory after vector work (a load predicated by a vector
compare, an address computed from vector data) brought the program's
whole vector graph into the plan: in a title's programs 2.4 million of
2.6 million retained values, about 500 MiB, belonged to twelve or so
conditions per program.
Below a condition, the copy now follows only what the evaluator reads:
the arguments of values it can compute, and for a load from memory the
words of its descriptor and its offset; a value the evaluator cannot
compute is kept without its arguments, which is what evaluating it
needs (it does not evaluate). Descriptor sources keep their whole
closure, which the device image pre-pass walks.
Tested with a new test (agc_driver_resource_plan_retention): the plan
of a program whose branch condition reads a scalar load at an address
taken from vector data now keeps the same number of values for vector
chains of 1, 16 and 64 steps before the load (84, 114 and 210 before);
the device table, written image, buffer descriptor, memory and SRT
range and shader disk cache tests pass; in a title's corpus the plans
of 314 programs hold 40 thousand values instead of 837 thousand; the
corpus recompile with SPIR-V validation has no invalid module.
Resolved onto main: the retention test runs on DeviceTableFixture.hpp,
which main does not have, so the test and its CMake line are left out
here; the next commits test this on main.
ExtractPlan copied the whole argument closure of every control-flow
condition into the plan. The draw-time walk (SrtDescriptorEvaluation)
uses a condition only when the evaluator computes it, and an operation
outside the evaluator's set ends the evaluation without reading its
arguments. For conditions on vector loads, image results or values
carried around loops the copy reached most of the program, and none of
it was ever read.
Detail::Evaluator::Reads names what the evaluator reads of a value:
nothing (an operation it does not evaluate, an immediate), its
arguments (the operations it evaluates, identities, phis, and the pairs
CompositeExtract reads through), or its whole closure (a raw read, whose
handle LoadedFromMemory walks). EvaluateInst returns early for a value
it reads nothing of, so the two cannot drift apart. ownPlanValues still
copies descriptor sources, SRT reads and fill values whole, and copies
a condition only as far as Reads goes: an operation it does not read is
copied without its arguments. The pure and guarded flat slots are
computed before the copy, from the same values as before.
Evidence: a title that registers about 5,700 shaders at load held
24.1 M copied IR values in its plans (about 5.4 GiB by value size), of
which 2.1 M were reachable from descriptor sources, SRT reads and fills
(counted by a local report over the recompiler's source map).
Tested: agc_shader_memory gains verifyPlanConditionPruning: a condition
on a load with a 64-addition address keeps the load as a leaf and none
of the additions, and conditions read through an extracted pair and
through a raw read still evaluate. It fails without the change. Full
ctest passes.
Resolved onto main: Reads is a static member of BasicEvaluator next to
MayEvaluate, and its operation list is main's evaluator's
(evaluatedArguments adds BitCount32, FindILsb32 and FindUMsb32; the
undefined values, which main never evaluates, read nothing). The
EvaluateInst guard reads the node's value, so it covers the compact
plans too. ownPlanValues now runs after the flat slots and before the
compact plan, and gives a value it keeps without arguments an empty
operand list, so the leaf drops the operand capacity main's IrValue
constructor reserves. Main's MayEvaluate already drops the condition on
the vector load, so the test checks that it is gone.
Table T#s keyed at runtime no longer fault on image_atomic_*32: the
table operation stays supported, elements carry the atomic flag through
dedup into the descriptor binding, and the table path binds AtomicView
like the direct path. 64-bit atomics stay unsupported, packed and BY
formats stay unsupported, and the debt entry records the relaxed
Device-scoped emission and the remaining gaps.
Since the synthetic FMASK mode was removed, a direct FMASK load reads
the stored word and only Fmask32_S8_F8 is accepted. A load from any
other FMASK format of up to 8 samples now reads the 8-, 16- or 32-bit
surface and expands it in the shader to 4 bits per sample, an index
past the fragment count reading 8 (unknown). One runtime image mode
handles every such format, reading the format from the descriptor at
run time or from a specialization constant.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three allocations ran on every stage materialization:
- DescriptorBindingBuilder::Populate pushed one shaderDataUserWords
entry per layout.userDataRegisters element without reserving, so the
vector regrew through 1, 2, 4, ... Reserve the exact count first.
- GuestSamplersDescriptor appended each sampler's dwords without a
reserve. Samplers are four dwords (the table path forces 4, and
Populate indexes the result as element * 4), matching the
resources.size() * 4 reserve GuestBuffersDescriptor already uses.
- ResolveImageTablesShared made a fresh shared ResolvedImageTables for
every snapshot without image tables. With no tables,
ResolveImageTables either throws (the shader expects tables) or
returns a default-constructed result, so return one shared, immutable
empty instance instead, still calling ResolveImageTables first so the
mismatch keeps throwing. Its only caller reads the result through a
const reference and never compares the pointer.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A DIM 2D image_load or image_sample of a 2D MSAA T# returns sample 0 on
RDNA2 (RX 6700 XT, measured through HIP; bilinear blends sample-0 texels).
The recompiler now reads such a direct sampled access as a 2D image, and
the driver binds a one-sample copy of the target's sample 0, refreshed by
a fragment pass before each binding. Peace Walker (PPSA18216) stops on
this read at the end of its opening movie.
Co-Authored-By: Claude <[email protected]>