MoltenVK has no VK_EXT_image_view_min_lod, so since #130 the first T#
with a MIN_LOD clamp above its base level threw. ASTRO BOT (PPSA21564
01.007.000) hits it on the title screen, about four minutes after boot.
MoltenVK does offer shaderResourceMinLod, which maps the SPIR-V MinLod
operand to Metal's min_lod_clamp.
On a device with shaderResourceMinLod, without the extension and
without graphics pipeline libraries, the new target flag
shaderImageMinLod moves the clamp into the shader:
- DescriptorBindingBuilder writes each direct image's clamp, relative
to the view's base level and bounded by its last level and MAX_MIP,
as a float specialization constant at MinLodBase + image.
- Samples of direct images take it as the MinLod operand, or as an FMax
on an explicit LOD when the clamp is above 0, combined with an
instruction's _cl clamp.
- The image view no longer chains the clamp there; image table entries
with a clamp are still rejected, and devices with the extension are
unaffected. APS5_NO_IMAGE_VIEW_MIN_LOD forces the shader lane.
- It throws when the clamp sits above a paired S#'s MAX_LOD or has no
mip filter, follows an explicit LOD or gradient with a nonzero S# LOD
bias, pairs with a run-time S# or different min and mag filters, or
meets image_get_lod, gathers at a clamp of a level or more, or the
emulated depth-compare or 11:11:10 sample.
- ValidateShaders accepts CapabilityMinLod when the device enables
shaderResourceMinLod.
- The flag is written after hostInterpolationFirst as request
serialization version 53; older versions read it as false.
The gaps, including llvmpipe applying the S# bias after the clamp, are
recorded in docs/dev/debt/shader.md.
Tested on an M-series Mac (MoltenVK 1.4.2, x86_64 under Rosetta):
image_sample_lod_clamp samples a T# with fractional and level clamps
over BASE_LEVEL 0 and 1 in both lanes, and fails without the shader
clamp or with an absolute one; graphics, shader disk cache, shader
memory and request serializer tests pass. PPSA21564 renders the title
and reaches the warp sequence after it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SpirvEmitter.cpp: main now calls Finalize() before filling info and checks
spanned-wave barriers on the words; keep that and run this branch's
VerifyScratchPoolReturns(state, words) on the same words before returning.
Keeps the specialization constants #6959 records next to this branch's
compute workgroup memory exclusion.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
RADV gives the second triangle of a full-screen quad PerVertexKHR values
in an order that does not match gl_BaryCoordKHR, so passes whose pixel
shader uses plain v_interp_p1/p2 show a diagonal seam (Silksong final
pass on a Radeon 780M). On RADV, pixel shaders are now first translated
with host interpolation; only shaders the host interpolation checker
rejects keep native VK_KHR_fragment_shader_barycentric.
APS5_NO_HOST_INTERPOLATION_FIRST=1 restores the old behaviour.
AI-assisted. Tested by me on my own machine.
NVIDIA's front end spends tens of seconds per large compute program on the
dynamically indexed Function arrays that held the per-lane scratch. A compute
program that uses scratch (scratch_* or a FLAT access that the aperture split
routes to the private aperture) now keeps it in a storage buffer instead, so
no Function array is declared or indexed. FLAT routing is unchanged.
The driver allocates one buffer per compute pipeline on its first dispatch,
capped by APS5_SCRATCH_POOL_MIB (default 64), holding a lock word and an
invocation counter per slot followed by the slots, and binds it in descriptor
set 2 (the layout of the device-memory LDS buffer). The shader takes the slot
count from the array length. Invocation 0 of each workgroup claims a slot with
a compare-exchange scan that starts at its linear workgroup id and spins until
it wins one; a barrier publishes the slot. Every invocation decrements the
slot's counter on every compute exit and the last one frees the lock, so
divergent exits and concurrent dispatches of one pipeline share the buffer
safely. The emitter fails the compile if a compute return lacks the release.
Programs without scratch keep their SPIR-V and pipeline layout.
The buffer is decorated Coherent: without it NVIDIA lost earlier byte stores
of a sequence of byte read-modify-writes to one dword. Lane-private
read-modify-writes no longer use atomics.
Tests: a new execution test covers the private aperture reached through a
table load, a literal and a computed address (store, atomic, and both ways
through scratch_*), a callee reached through s_swappc_b64, isolation of 64
workgroups over one or two slots, divergent wave exits with a second dispatch
reusing the slots, and the module shape (no Function array, no binding in a
program without scratch). The guarded memory load fixture skips the slot
acquisition.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Null buffer descriptors still split modules on unused specialization
fields after buffer presence moved to run time. Learn the first valid
bound fields per slot and use them for absent descriptors, retaining
actual runtime presence and independent bound formats.
Snapshot the learned fields once for result memo keys and materialization
so identical null captures cannot retain a module made before learning.
Cover direct, captured and prepared calls, null stores, and changed formats.
Keep the process-local table and an off switch for main's module selection.
AI-assisted: yes
MoltenVK and lavapipe leave the tessellation evaluation stage out of
subgroupSupportedStages, so the driver's shader validation refuses every
domain program that declares GroupNonUniform ("subgroup capability 61 is
unsupported for shader stage 4"). Gran Turismo 7 stops there on macOS at
the Used Car dealership.
The device target now records whether the tessellation evaluation stage
has subgroups. When it does not, tessellation evaluation programs outside
the mesh path get a host subgroup width of 1 and the one-lane rules that
vertex programs already use without vertex subgroups: the lane id is 0,
a ballot is the invocation's own bit, a shuffle returns the invocation's
value and an elect is true. Reads of other lanes still throw. Devices
with tessellation evaluation subgroups emit the same SPIR-V as before.
The request serialization moves to version 51.
getSource keeps the program it built a plan from, for the source's first
variant compile. When Materialize then threw, no compile ran, so the
program stayed with the SourceEntry for the life of the process. With the
value pool such a program holds the slots of its peak value count. Over
the replay corpus, the 90 requests whose descriptor sources do not
evaluate kept about 1 GB this way. recompileOnce now releases the program
when materialization throws; a later request for the source prepares it
again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A compute shader whose LDS does not fit the device limit keeps it in a
storage buffer in descriptor set 1, binding 0. Specialization listed that
variable with the module's set 0 bindings, so selecting the prepared plan
failed for a shader with no guest buffers, because nothing in set 0 uses
binding 0. Only the pixel function LDS set was left out before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A program kept in a SourceEntry held its use-list arena until the entry
was destroyed, so the buffers of values ReclaimUnusedValues dropped, and
the buffers every use list outgrew, stayed allocated for as long as the
program was cached. Before a program is cached, ReleaseUseArena now
copies each value's use list into a heap vector and frees the arena.
Values created after that draw from a new arena.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A draw that blends into several targets with BLEND_CLAMP clear now carries
one merge entry per target. Each entry gets its own storage image and
pixel-shader blend, and every pending entry is stored inside the draw's one
pixel interlock region. Partial writes of packed targets still merge alone.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On NVIDIA's proprietary driver, function arrays that live in local memory
and are read under a data-dependent branch return wrong values in
subgroups 1-3 of a mesh workgroup; subgroup 0 is correct. A wave64
geometry program whose relative addressing keeps its VGPRs in the vector
window therefore loses register writes in every wave but the first. In
the clipping geometry program of Sony's PS5 PSP emulator (PPSA07550) the
empty waves never cleared their candidate primitive slots and wrote
degenerate triangles over wave 0's, so the 3D menu drew spikes.
On that driver a mesh program's window now lives in a device-local
storage buffer of 256 workgroup slots of 65536 dwords, claimed through a
per-slot lock and released after the final barrier. The pool is created
before the first draw that uses it records anything, and its header is
zeroed in a batch of its own that is submitted and waited for; the pool
is published only after that wait, and a failure releases it. Direct
register accesses keep one function variable per register; a direct
store also writes the buffer when a buffer read can see it, relative
reads load the buffer, relative writes store only to it, and a direct
read that may follow a relative write loads the buffer and refreshes the
variable. A buffer entry is zeroed only when a buffer read can see it
before a write. APS5_NO_MESH_WINDOW_BUFFER=1 keeps the arrays.
A 1D-array resource sampled only by image_sample_lz/image_sample_l no longer
gets the layerRows runtime mode up front. The capture throws
LayerRowSamplesRequired when a view of more than maxImageArrayLayers slices
reaches a shader prepared without it, and the shader is prepared again with
GuestContext::layerRowSamples, as converted samples are. Shaders that never
see such a view keep the interface they have without this path.
Test views of exactly maxImageArrayLayers and maxImageArrayLayers + 1 slices,
with image_sample_lz and image_sample_l, and report each layer-row texture
built under APS5_PROFILE_DRAW.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A sampled 1D array view with at least maxImageArrayLayers slices, read
only by image_sample_lz or image_sample_l, gets a runtime image mode that
binds a 2D image holding each viewed slice in two adjacent rows. The
shader rounds and clamps the layer as Vulkan array layer selection does
and samples on the boundary between the two copies, so filtering never
reaches another slice. Other uses keep throwing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SPIRV-Cross emits the lock's acquire compare-exchange and release
exchange as relaxed Metal atomics, so the plain LDS loads and stores of
a locked 64-bit update were not ordered against the lock and contended
ds_add_u64 lost updates on an Apple M5 Pro. On MoltenVK the driver now sets
SpirvTarget::ldsLockFences, and the lock brackets its critical section
with workgroup-scope acquire and release OpMemoryBarriers, which become
threadgroup atomic_thread_fence with no execution barrier.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MoltenVK exposes VK_KHR_fragment_shader_barycentric but its MSL step
rejects PerVertexKHR, so the extension is disabled there and a pixel
shader whose I/J VGPRs outlive their v_interp_p1/p2 pairs takes the
generated barycentric stages, which accept only triangle lists without
primitive restart on a device without geometry shaders.
A new target flag, fragmentBarycentricBuiltins, enables the extension
for its built-ins only. The host interpolation check then keeps the
leftover I/J reads (all perspective or all linear, at the fragment's
default position) and reads them from BaryCoordKHR or
BaryCoordNoPerspKHR, while the interpolated attributes stay hardware
varyings. Raw vertex parameters and I/J at an offset or a sample
position still take the generated stages. Apple GPUs return
unnormalized perspective weights unless the fragment function reads
[[position]], so each perspective read keeps a neutral use of
gl_FragCoord.w, loaded as a float. Devices with the full extension are
unaffected; the request serialization moves to version 45.
Co-authored-by: yripper <[email protected]>
TargetMerge gains a TargetBlend (factors, operations, write mask, constants, signedness) that the request serializer, the cache key and the variant hash take only when it is enabled, so every request without it keeps its bytes, key and hash.
A shader with an image table map read its fault buffer back only when a
table record was poisoned, an SRT read was poisoned, it had window lane
guards or the loop guard was on. A DescriptorFault only set the fault
buffer, so with a valid table and no copied write nothing ran the fault
check, and a lane that took a folded descriptor web path or selected an
undefined arm read arm 0 or zeros with no report.
The compiled artifact now counts its DescriptorFault instructions
(descriptorFaults, stored in the shader disk cache, format 48), and the
readback gate requests the fault check when it is nonzero. The result a
capture materializes copies the count, and also windowLaneGuards, which
that copy had dropped, so the gate's window lane guard condition never
saw it. sizeof(CompiledShaderArtifact) is 288 and sizeof(RecompileResult)
552 with the new field.
agc_driver_descriptor_web_faults adds a wave32 shader that reads an image
table and then the descriptor web, storing only to LDS so no copied write
forces completion: it binds an image table and no BDA address table, a
valid arm and an unselected undefined arm stay clean, the folded path and
a selected undefined arm fault at the read, and nothing faults with EXEC
empty. Without the gate change the folded case does not fault.
Co-Authored-By: Claude Opus 5.5 <[email protected]>