Skip to content

feat(shader): accept every SDWA selector on v_mov_b32 - #751

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
Celegans12:feat/shader-sdwa-vmov-byte-preserve
Oct 6, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
Celegans12:feat/shader-sdwa-vmov-byte-preserve

Conversation

@Celegans12

@Celegans12 Celegans12 commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

What

v_mov_b32_sdwa threw VOP1 SDWA destination selector 1 (unused 2, source selector 0) ... is not supported when a byte source (src0_sel BYTE_0..3) met a partial destination (dst_sel BYTE_0..3, WORD_0/1): the VMovB32 rule allowed only WORD and DWORD sources there.

Tested

  • Windows 11, MinGW-w64 GCC 15.2.0, based on main 9cafa33: configure -DBUILD_TESTING=ON, full build, --target libs, ctest:
  • 339/339 tests pass (ctest --output-on-failure)
  • New agc_driver_sdwa_move_selectors runs all 294 combinations as wave32, wave64 and wave64 on 32-lane subgroups, plus the game's three in-place words under a partial exec mask, on the GPU (RTX 3080 Laptop and Intel UHD); expected values come from an ISA model. New agc_vop1_sdwa_move_decode checks the decoded operands for all 294 and that the reserved encodings throw. On main the GPU test fails at its first byte-source move with the message above, and the decode test reports the 144 byte-source partial-destination combinations.
  • The partial-exec check is skipped on subgroups narrower than 32 lanes (llvmpipe has 8), as in agc_driver_image_sample_derivatives: there threadBit reads an EXEC written with s_mov_b32 from EXEC_LO at the subgroup invocation id, on main too.
  • PPSA26344 v1.008, offline recompile of the shader: on main it fails at decode; with this change it decodes and translates (3,340 instructions) and stops at an unrelated gap, a loop-indexed image descriptor (s_load_dwordx8 at 0x131c). 352 other game shaders that use v_mov_b32_sdwa give identical results with and without this change.
  • Not tested: 64-lane subgroups (this GPU has 32), PPSA21564 and Dreaming Sarah (expect a PPSA21564 gate; not run), Linux (CI), console behaviour.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed in TechnicalDebt
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • AI-assisted: yes

@Celegans12
Celegans12 force-pushed the feat/shader-sdwa-vmov-byte-preserve branch 2 times, most recently from 6123ea3 to d82709a Compare October 5, 2026 22:35
@github-actions github-actions Bot added build shader core/shader agc libSceAgc* labels Oct 5, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

The Linux CI re-run now gets through ctest and fails in this PR's own test (run 37383475649, attempt 2, llvmpipe with 8-wide subgroups):

AGC graphics: sdwa move selectors wave64: lane 36 source 0x0000ff00 previous 0x8fa1a6c8 result 0x0000ff00, expected 0x00000000: in-place v_mov_b32_sdwa v3, v3 dst_sel:BYTE_1/2/3 dst_unused:UNUSED_PRESERVE src0_sel:BYTE_0 must copy byte 0 into the other bytes

What the log narrows down:

  • Only the broadcast part fails. For lane 36, all 294 selector combinations, which run under a full EXEC, were checked first and passed.
  • The failing run is the native wave64 dispatch (device->Target()). The wave64 split run isn't reached.
  • Lane 36 is the first upper-half lane whose expected value differs from its source: lanes 34 and 35 have all four bytes equal. So the likely picture is that every active lane in 32..63 kept its old value under EXEC_HI = 0x3c3c3c3c, set by s_mov_b32 exec_hi. The lower half under EXEC_LO passed.

Here, on NVIDIA, the test passes 20 times in a row and Windows CI is green. Because of that, this may be how a partial EXEC_HI written with s_mov_b32 is handled when a wave64 spans several 8-lane subgroups, rather than the SDWA change itself. As far as I can see, no execution test on main writes a partial EXEC_HI that way. Could you check, for example by adding a plain v_mov_b32 under the same EXEC to see whether it also skips the upper lanes on llvmpipe? If it's a main issue, a separate fix (or a skip on small subgroups, with a note) would let this land.

v_mov_b32_sdwa threw 'VOP1 SDWA destination selector ... is not
supported' when a byte source (src0_sel BYTE_0..3) met a partial
destination (dst_sel BYTE_0..3 or WORD_0/1): the VMovB32 rule allowed
only WORD and DWORD sources there. Every src0_sel, src0_sext, dst_sel
and dst_unused combination is now accepted. Translation already follows
RDNA2 for all of them (applyBitSourceModifiers, writeRawU32): the
selected field is zero- or sign-extended, and the result's low bits go
to the dst_sel field with the other bits zeroed, sign-extended above
and zeroed below, or preserved. Reserved selectors, dst_unused 3, and
neg/abs on sub-dword sources still throw.

agc_driver_sdwa_move_selectors runs all 294 combinations as wave32 and
wave64 on the GPU, plus the in-place byte broadcast under a partial
exec mask, which is not checked on subgroups narrower than 32 lanes,
as in the other EXEC-masked execution tests. agc_vop1_sdwa_move_decode
checks the decoded operands and the reserved encodings.
@Celegans12
Celegans12 force-pushed the feat/shader-sdwa-vmov-byte-preserve branch from d82709a to e5b7427 Compare October 6, 2026 00:49
@Celegans12

Copy link
Copy Markdown
Contributor Author

Thanks. As you suspected, it is on main, and it is not specific to SDWA or to EXEC_HI. On subgroups narrower than 32 lanes, when EXEC is written as a value (s_mov_b32/s_mov_b64), each lane reads its bit from EXEC_LO at its subgroup invocation id. So lanes 32..63 follow EXEC_LO's pattern rather than all keeping their old value; lane 36 is active and kept its source, as you read it.

  • Mechanism. On a host subgroup narrower than 32 lanes, a wave64 runs one guest lane per invocation (laneCount is 1 in SpirvEmitter::Emit). LaneId lowers to gl_SubgroupInvocationID (EmitLaneId in SpirvAluEmitterMath.cpp, via EmitSubgroupLocalInvocationId in SpirvSubgroup.cpp). TranslationContext::threadBit (ExecMask.cpp) picks the lane's bit with lane < 32 ? exec_lo : exec_hi and lane & 31, so on llvmpipe (id 0..7) every lane takes bit id of EXEC_LO.

    • The test's EXEC_LO 0xa5a5a5a5 repeats every 8 bits, so the lower half and the wave32 run come out right.
    • Lanes 32..63 follow EXEC_LO's 0xa5 pattern (the probe below shows 0xa5a5a5a5_a5a5a5a5): 32, 34, 37 and 39 are written, 33, 35, 36 and 38 are not. The test stops at the first wrong lane. Lanes 32 and 35 are wrong too but hide it because all their bytes are equal, so 36 is the first visible failure.
  • Plain v_mov_b32. Probe on main cb33f85, no SDWA: s_mov_b32 exec_lo/exec_hi, then v_mov_b32 and v_add_nc_u32, every lane checked. It ran on the Intel UHD iGPU with compute subgroups forced to 8 and to 16 lanes (a local VulkanDevice change that sets the required subgroup size; not pushed). Both instructions wrote the same lanes:

    EXEC lanes written
    0x3c3c3c3c_a5a5a5a5 0xa5a5a5a5_a5a5a5a5
    0x00000000_ffffffff all 64
    0xffffffff_00000000 none
    wave32 0x0000ffff all 32
  • Why it is not fixed on main here. EXEC from a compare is consistent on 8 lanes: v_cmp_gt_u32 + s_and_saveexec_b64 gives the expected 0x00000fff_ffffffff, because the ballot is subgroup-local too. Taking only LaneId from the wave lane (LocalInvocationIndex modulo the wave size) fixed the literal masks but broke that compare case: only lanes 0..7 stayed active. A fix would need LaneId from the wave lane and each subgroup's ballot placed at its wave offset, plus matching changes in readfirstlane, mbcnt and the cross-lane ops (shuffles, DPP, permlane), and wave-wide ballots for whole-mask scalar uses. That is a larger change than this PR, and main already skips EXEC-masked cases below 32 lanes (agc_driver_image_sample_derivatives).

  • Change. Rebased on current main. The broadcast check now follows that precedent: when Target().subgroupSize < 32, it prints EXEC-masked broadcast skipped, subgroup size N cannot hold a wave32 and skips the check. The 294 full-EXEC combinations still run on every device.

    • With 8 or 16 lanes forced, the test passes and prints that line.
    • It passes 20 times in a row on the RTX 3080 and 5 of 5 on the Intel UHD at 32 lanes.

One side note: in that run Windows CI reports this test as Skipped, like the other GPU tests, so llvmpipe is the only CI job that actually executes it.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks for tracking that down. The skip follows the existing DPP/swizzle precedent (Target().subgroupSize < 32), it only drops the masked broadcast check, and the 294 full-EXEC combinations still run in all three passes, so this looks good to me.

The literal-EXEC behaviour you found on narrow subgroups is a main limitation rather than something for this PR. It might be worth a TechnicalDebt line at some point (EXEC written as a value is read per subgroup invocation from EXEC_LO when the host subgroup is narrower than 32), since it isn't written down anywhere yet.

@oneandonlydean
oneandonlydean merged commit cf139f3 into boykopovar:main Oct 6, 2026
3 checks passed
@Celegans12
Celegans12 deleted the feat/shader-sdwa-vmov-byte-preserve branch October 6, 2026 03:33
@Celegans12

Copy link
Copy Markdown
Contributor Author

Thanks. The TechnicalDebt line is up as #800 (documentation only).

Celegans12 added a commit to Celegans12/AnyPS5 that referenced this pull request Oct 6, 2026
On a host subgroup narrower than 32 lanes, threadBit reads an EXEC
written with s_mov_b32 or s_mov_b64 at bit LaneId & 31 of EXEC_LO, and
LaneId is the subgroup invocation id. Every lane at or above the host
subgroup size therefore reads the bit of a lower lane, in wave32 and
wave64 alike. An EXEC produced by a compare stays consistent because
its ballot is subgroup-local as well. Found while investigating the
llvmpipe failure of boykopovar#751.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* build shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants