Repository navigation
fix(recompiler): run a pixel shader the guest does not iterate once per pixel - #6470
Conversation
…quests 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.
…er pixel 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.
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.
|
Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.
Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.
|
… stand in for a 2x2 grid Village's 2x checkerboard puts its two samples at x = +4/16 and -4/16 and swaps their indices on odd rows. On a device without a 2x2 sample location grid every pixel takes pixel (0,0)'s locations, so the odd rows hold the console's sample 1 in sample 0 and the reverse: a pass that reads the samples by index, as checkerboard reconstruction does, exchanges the two half-pixels of every odd row, and SampleID, sample masks and exported coverage name the wrong samples there. Such a pattern holds one set of positions in every pixel, so drawing with pixel (0,0)'s locations is exact up to the order of each pixel's samples. The draw now keeps the guest's order: the pixel shader reads SampleID, applies the sample mask and exports coverage through each pixel's order, and every render pass drawing with the stand-in puts the samples of its multisampled attachments in the stand-in's order before it and back in the guest's after it. Color targets do that through a storage view of their texels as unsigned integers, depth through a copy and a per-sample pass writing FragDepth, and stencil through VK_EXT_shader_stencil_export or one pass per bit. It needs shaderStorageImageMultisample with a storage view of the color format, sampleRateShading, and a depth format FragDepth writes back exactly (16-bit or 32-bit float); otherwise, and for patterns whose pixels hold different positions, the stand-in alone is used, and the log line says why. Devices with the grid are unchanged. Recompile requests carry the order as serialization version 32 (boykopovar#6470 takes 31), and the shader cache format becomes 40 (open pull requests take 37 and 39).
|
@EagleBearerYS an overlap note with #5907 (sample-zero rasterization of a multisampled scan into one-sample targets), which adriadam10 plans to merge once
The textual conflict is small: the debt lines in AI-assisted (Claude Code); I checked both diffs on |
|
Thanks, @simonbronk, that matches what I see.
An Onimusha (PPSA27836) run once both are on |
Main took request serialization versions 31 (output primitive size) and 32 (spanned waves), and boykopovar#6683 uses 33, so the pixel iteration fields move to version 34. A version 32 request keeps its spanned-waves flag and reads no pixel iteration.
A one-sample target drawn by a multisampled rasterizer refused a pixel shader that reads ANCILLARY with POS_X_FLOAT or POS_Y_FLOAT, because the sample index made the host shade per sample and moved FragCoord from the pixel centre. A shader the guest does not iterate now runs once per pixel with sample number 0, and on this path the rasterizer keeps one sample, so those draws are no longer refused. Resident Evil Village's (PPSA01556) 2x depth-only pass with SPI_PS_INPUT_ENA 0x2301 was one of them.
main d7262bd adds the single-sample scan entry after the custom MSAA sample locations entry in the AGC debt file. Keep it after this branch's version of that entry.
Main's EmitSetAttribute debt entry now says the generated rect-list and barycentric tessellation stages forward the vertex shader's clip array. It sits next to this branch's PERSP_SAMPLE and pixel position entries, which main left as they were; keep main's entry and this branch's two.
Resolves the shader debt file bullet by bullet: main's EmitSetAttribute entry, and this branch's PERSP_SAMPLE/LINEAR_SAMPLE and pixel-position entries.
|
Tested on Windows 11, MinGW-w64 GCC 15.2.0, NVIDIA GeForce RTX 5090 (driver 617.42), Ryzen 9 9950X3D. Head
Not done: I did not run the new tests against AI-assisted: yes. Written with AI assistance and reviewed by me. |
Since boykopovar#6470, a draw decode sets the pixel stage's iteratesSamples from the per-sample shading state and its sample locations from the draw's state, and a pixel shader that does not iterate runs once per pixel with sample 0. The sample-order test built its pixel stage from the registers alone, so its per-sample passes ran once per pixel and SampleID read 0. It now sets both fields as DrawDecode does.
…terates This branch's Metal fix for uniform sample masks treated a pixel shader that loads PERSP_SAMPLE and LINEAR_SAMPLE as running per sample: it gave the shader its own gl_SampleID and kept only the current sample's coverage bit. main now runs such a shader once per pixel unless the guest iterates samples (boykopovar#6470's pixel sample iteration), and declaring gl_SampleID makes every Vulkan driver run the shader per sample. The own SampleId input and the per-sample masks now follow the guest's sample iteration (pixelIteratesSamples, State::perSampleShading). sample_mask_multisample and front_face_all_bits still pass on MoltenVK. agc_driver_pixel_sample_iteration still fails its first case on MoltenVK: an attachment-less 4x draw runs the fragment shader per sample although the generated Metal function asks for no sample input. That is recorded in the AGC debt file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Combines this branch's sample order with #6470's once-per-pixel and once-per-sample inputs. The pixel request keeps both sides' fields: #6470's iteration, position location and sample locations are serialized at version 34, and the sample order after them at version 35. The serialization tests move the sample-order bytes after the iteration fields and drop both from older versions. The pixel info collector adds sample order to the SampleLocationsVary inputs, and the draw keys compare and hash both. EmitSampleOffset's iterated index already goes through GuestSampleIndex, since the SampleId builtin does. The shader cache stays at FormatVersion 40, and the sample-location debt entry takes #6470's sentence on the position inputs.
What
A pixel shader that reads the ancillary input's sample number ran once per sample on every multisampled draw, even when the console runs it once per pixel.
The cause: the recompiler gave that field (bits 11:8) the
SampleIdbuiltin, and Vulkan enables sample shading for any fragment shader that usesSampleId(spec). That happened whateverPA_SC_MODE_CNTL_1.PS_ITER_SAMPLEsaid. So these shaders did N× the fragment work, andFragCoord, and with it POS_X_FLOAT and POS_Y_FLOAT, moved from the pixel centre to the sample.SPI_BARYC_CNTLputs POS_X/Y_FLOAT (POS_FLOAT_LOCATION,POS_FLOAT_ULC);InterpolateAtOffsetby the guest's offset fromPA_SC_AA_SAMPLE_LOCS;SPI_BARYC_CNTLsays.SampleId;sampleRateShading(InterpolationFunction) and the fragment-shader barycentric path. Devices without them keep the host's default positions.maintook 31 with feat(agc): draw line-strip output of NGG geometry programs as mesh lines #5884 and 32 with feat(recompiler): run single-wave wave64 compute programs at one lane per invocation on 32-lane hosts #5405, and fix(agc): keep the guest's sample order where pixel (0,0)'s locations stand in for a 2x2 grid #6683 takes 33). The shader disk cache goes toFormatVersion39, which no open PR uses:mainis 38 since fix(shader): bind an unnormalized S# normalized when it samples only null images #7296, fix(agc): keep the guest's sample order where pixel (0,0)'s locations stand in for a 2x2 grid #6683 takes 40 and feat(shader): read samplers through nested and pointer-keyed descriptor tables #7395 41.DB_EQAA.PS_ITER_SAMPLES(still not read). SAMPLE_COVERAGE stays untranslated, as before.With #5907 (merged):
FragCoord. This PR drops that refusal (3e802e4), as simonbronk raised here (comment) and we agreed (reply): a draw feat(agc): rasterize a multisampled draw into one-sample targets at sample 0's locations #5907 collapses reaches the pixel stage as not iterated, so it reads sample number 0 and noSampleId. PPSA01556's 0x2301 pass then draws once per pixel at the pixel centre. PPSA07411's 0x2001 pass (PERSP_SAMPLE and ANCILLARY), which feat(agc): rasterize a multisampled draw into one-sample targets at sample 0's locations #5907 already accepts, now gets sample 0's barycentrics and sample number explicitly.Effect on open PRs:
Tested
Resident Evil Village (PPSA01556), Windows 11, RTX 3070 Laptop GPU (driver 617.42):
SPI_PS_INPUT_ENA0x2301):[gpu] driver failure: AGC graphics: the depth target's sample count differs from the rasterizer'sIt's the rule refusing ANCILLARY with POS_X/Y that stops this draw.
merge check: PPSA01556 0x2301 decodes to samples=1, request sample 0 locations 04 04 0c 0c.ctest -R '^(agc|shader|recompiler)'passes, 608/608.Linux, lavapipe (CI configuration):
agc_driver_pixel_sample_iteration:SampleIdonce per pixel; sample 0's offset; the guest location table when iterated, standard and per-pixel custom; no offsets withoutsampleRateShading.pixel_ancillary(iterated and not),agc_driver_sample_mask_multisample(its per-sample pass setsperSampleShading, as a guest would),request_serializer(version 31 and its upgrades),agc_shader_memory(the request version) andagc_driver_graphics(theSPI_BARYC_CNTLdecode, and feat(agc): rasterize a multisampled draw into one-sample targets at sample 0's locations #5907's three ANCILLARY with POS_X/Y cases, which now rasterize sample 0 alone instead of throwing).ctest -R '^(agc|shader|recompiler)'passes, 607 tests (608 with the new test). The one exception,agc_driver_preparation_scope_settle, is a load flake that also fails on main; it passed when rerun alone.--repeat until-fail:10.Windows (GitHub's runner, MinGW, no Vulkan device), at this head (run):
request_serializer,pixel_ancillary,agc_shader_memory,agc_driver_graphicsand the other 19agctests run with them pass 3 of 3; 4 device draws skip without Vulkan. Only these tests' targets were built: onmain0029e62 a whole-tree MinGW build stops attests/execution/GpuIndirectArgumentsMisalignedWriterAhead.cpp(alignas(65536), from #5747), which this change does not touch. The libc and libkernel suites pass apart fromguest_kqueue, which fails onmainon that runner.Merged with
mainb260d8d (bd09c49), on GitHub's runners (run):request_serializer,agc_shader_memory,agc_driver_graphics,pixel_ancillary,agc_driver_pixel_sample_iteration,agc_driver_sample_mask_multisampleand #5907's state tests pass 3 of 3 on Linux and Windows. The run shows as failed because the Windows libkernel suite'sguest_kqueueand the Linux timing testprecise_waitfail on those runners with and without this PR (nomaincomparison was run).agc_driver_graphicsfails on Linux and Windows on the three cases; with the commit, it,pixel_ancillaryandagc_driver_pixel_sample_iterationpass 3 of 3 on both.Windows 11, RTX 3070 Laptop GPU (driver 617.42), native fragment-shader barycentrics:
agc_driver_pixel_sample_iterationpasses 5 of 5 (21 draws). Withmain's code generation (the fix commit's non-test files at d5cbfd1) it fails with "2x: a pixel shader the guest runs once per pixel reads SampleId". lavapipe runs the same draws through the emulated barycentrics.Checklist
main0029e62, merged withmainb260d8d; no other open PR implements the same functions. Overlaps (git merge-tree):FormatVersionline inShaderDiskCache.hpp: perf(shader): select the record of a T# whose offset the CPU evaluates #6243, fix(agc): keep texture images inside their extent's mip chain #6280, fix(recompiler): defer the TYPE check of a guarded read-only static V# #6412 and perf(agc): share one image table per heap and write epoch through capture, compare, resolution and descriptors #6454 change it, and feat(recompiler): add an uncached compile entry point and entry key helpers for offline tools #6296 and feat(tools): add an offline shader archive tool that publishes prebuilt packs #6032 add lines next to it. Keep both sides; the version takes the next free number.docs/dev/debt/shader.md; keep both.NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file (none added).mdfiles or images added to the repository (attach them to this PR)