Skip to content
This repository was archived by the owner on Oct 28, 2026. It is now read-only.

fix(recompiler): give wave64 vertex programs on 32-lane hosts a 32-lane s3 - #33

Closed
AlxcBTW wants to merge 3 commits into
mainfrom
fix/vertex-narrow-wave-entry
Closed

AlxcBTW wants to merge 3 commits into
mainfrom
fix/vertex-narrow-wave-entry

Conversation

@AlxcBTW

@AlxcBTW AlxcBTW commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

What

Fixes boykopovar#2783, option A from the issue: on a 32-lane host subgroup, the vertex entry gives a wave64 program the wave the host actually runs.

A wave64 vertex program runs at one guest lane per invocation, so on NVIDIA (and other 32-lane hosts) lanes 32-63 have no invocation. The vertex entry still set s3 = 64 | 64 << 8, a wave of 64 vertices and 64 primitives. The stock Unreal NGG culling program in boykopovar#2783 keys a full-wave OR on lane < s3's primitive count and reads lanes 31 and 63, so lanes 32-63 held real cull results and the read of lane 63 could not be built. Taxi Chaos 2 (PPSA30279) and Minecraft Dungeons II (PPSA16064, boykopovar#2811) stop on it.

s3. For a wave64 program on a 32-lane host, emitEntryPrologue now sets s3 = 32 | 32 << 8. The console launches waves with fewer vertices and primitives than the wave size, so this is a wave the program can see on hardware. The host subgroup width reaches the vertex input the same way it reaches the compute and mesh inputs: BuildShaderStageInputInfo gets HostSubgroupSize(request) and stores it in ShaderVertexInputInfo, as it does in ShaderWorkgroupInputInfo. The source, prepared-shader and disk cache keys already carry HostSubgroupSize(request) for every stage (Recompiler.cpp getSource, PrepareShader, BuildPreparedShaderKey, ShaderDiskCache::BuildKey), so no key changes. The condition is the one the compute and mesh paths use for their two-lane layout: wave64 on exactly 32 lanes. Wave32 programs, 64-lane hosts and narrower hosts keep waveSize | waveSize << 8. Programs that never read s3 produce the same IR, since the write is dead.

The read. With the narrowed s3, the keys are 0 in lanes 32-63, which makes the read sound. On a 32-lane host, when the existing half-wave scan rules do not match, MatchHalfWaveScan also accepts:

  • EXEC set to -1 by s_mov_b64 exec, -1 (a constant true EXEC);
  • bound_ctrl:1 row_shr steps when 0, the fill, is the reduction's identity (OR, add, unsigned max), also when a step writes another register, since such a step writes every lane and its old value never shows;
  • keys that are 0 in lanes 32-63 (UpperLaneWalk): built from 0 through selects under a lane index (lane id, or v_mbcnt of -1) below at most 32, v_cmp_ne_u32 against 0, ORs, DPP and permlanes, in stages whose entry EXEC is every lane.

Lane 63 is then the identity and lane 31 the subgroup reduction of the host's invocations. Keeping lane 31 a shuffle of the emulated scan is not enough: on an RTX 5070, every vertex warp of a non-indexed triangle list ran with 30 of its 32 invocations, so the shuffle read an idle invocation and the new execution test covered no pixel. On a device without subgroup arithmetic, lane 31 of such a scan stays a shuffle, as before, and lane 63 still reads the identity. A v_readlane_b32 outside the host subgroup of another value that is 0 in lanes 32-63 reads 0. Only reads that used to throw change, plus lane 31 of such scans on 32-lane hosts with subgroup arithmetic. The strict rules run first and are unchanged, and 64-lane hosts never reach the new ones.

The two TechnicalDebt entries (vertex entry s3, lanes outside the host subgroup) now describe this, including that idle host invocations count as the identity where the console would see the lanes s3 claims.

Sources: the s3 layout (vertex count in bits 0-7, primitive count in bits 8-15) is the one the existing entry already uses, taken from the prologue Mesa's ACO emits for NGG vertex shaders (see its TechnicalDebt entry). bound_ctrl and the DPP/permlane lane rules are the ones EmitDppWriteCondition, EmitDppMoveU32 and EmitPermlane16U32 already implement; no instruction semantics change.

Related: boykopovar#1825 and boykopovar#2261 (both open) route NGG programs that use LDS or s_barrier to the mesh path, where wave64 runs at two lanes per invocation on 32-lane hosts. That would also cover this program, with shared LDS. This PR changes only the vertex path and does not conflict with them.

Tested

Windows 11 26200, WinLibs GCC 15.2.0, Ninja Release, NVIDIA GeForce RTX 5070 Laptop (616.92, 32-lane subgroups).

  • New unit test verifyNarrowVertexWave in agc_shader_memory: a synthetic wave64 NGG vertex program with the listing's shape (v_mbcnt lane index, v_cmpx_lt_u32_sdwa against s3 byte 1, s_mov_b64 exec, -1, v_cndmask key, bound_ctrl:1 OR row scan, v_permlanex16_b32, v_readlane_b32 of lanes 31 and 63) prepares on 32 lanes through PrepareShader, with lane 31 as a subgroup OR, and keeps its DPP scan on 64 lanes. On main it fails with v_readlane_b32 of lane 63 is outside the 32-lane host subgroup. It also checks the vertex count byte, a constant bound of 32, a scan step written to another register, a device without subgroup arithmetic (lane 31 stays a shuffle), and a three-step scan read through the fallback; and that a bound of 33, a count from user data, the vertex index instead of the lane, and a step into another register without bound_ctrl:1 still throw.
  • Same test, subgroup widths: a wave64 program that exports s3 is prepared for 32 and for 64 lanes in both orders with one ShaderPreparationContext. The two get separate sources, MatchesPreparedShader rejects the other width, the second preparation equals a first one, and the two programs differ. With the s3 change reverted, only that last check fails. A wave32 program gives the same SPIR-V on 32 and 64 lanes.
  • New execution test agc_driver_ngg_vertex_wave_reduction: the listing's scan in a real draw; each vertex compares its wave's OR with the ballot of its keys and collapses its triangle when they differ, so every pixel must be covered. Passes on the RTX 5070. On main it fails to build the program; with lane 31 left as a shuffle no pixel is covered. It skips on other subgroup widths.
  • agc_driver* on NVIDIA: 254/254 (the new and related tests rerun after a reboot: 4/4).
  • Non-GPU suite (ctest -E "^agc_driver|^video_out|^guest_video"): 456/456.
  • AMD (Radeon 880M) was not tested: its kernel driver crashed this machine twice under GPU workloads, so the AMD Vulkan driver is disabled here. The new execution test skips on 64-lane subgroups, and the fork CI covers Linux lavapipe.
  • check_conventions.py passes.
  • Fork CI (run 38091700624, merged into current main): Windows, relinker, register-context and write-watch jobs pass (write-watch macOS-intel after a rerun of the known write_watch_threads timeout). Linux fails only agc_driver_flat_apertures with a 120 s timeout on lavapipe, twice. That test came to main after this branch was cut, and the fork's own main run of the same cycle (38091700844) fails it the same way.
  • Not run in a title from this branch. A local prototype with the same s3 change got Taxi Chaos 2 past both culling programs to its intro on the RTX 5070.

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
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • AI-assisted: yes (Claude Code)

🤖 Generated with Claude Code

AlxcBTW and others added 3 commits October 11, 2026 00:22
…ne s3

A wave64 vertex program runs at one guest lane per invocation, so on a
32-lane host subgroup its lanes 32-63 have no invocation. The vertex
entry still set s3 = 64 | 64 << 8, telling the program that its wave
holds 64 vertices and 64 primitives. An NGG culling program then keys a
full-wave OR on lane < s3's primitive count, lanes 32-63 hold real cull
results, and its v_readlane_b32 of lane 63 cannot be built (boykopovar#2783).

The entry now sets s3 = 32 | 32 << 8 for a wave64 program on a 32-lane
host, the wave the host runs; the console launches waves with fewer
vertices and primitives. The host subgroup width reaches the vertex
input the way it reaches the compute and mesh inputs
(BuildShaderStageInputInfo from HostSubgroupSize), and the source,
prepared-shader and disk cache keys already carry it for every stage.
Programs that do not read s3 are unchanged; wave32 programs and other
host widths keep the full wave.

The keys are then 0 in lanes 32-63, so the read is sound. On a 32-lane
host, when the existing half-wave scan rules fail, the scan may also:
- run under s_mov_b64 exec, -1 (EXEC constant true);
- use bound_ctrl:1 row_shr steps when 0, the fill, is the identity (OR,
  add, unsigned max), also into another register, whose old value never
  shows since such a step writes every lane;
- take keys shown to be 0 in lanes 32-63: built from 0 through selects
  under a lane id or v_mbcnt of -1 below at most 32, v_cmp_ne_u32 against
  0, ORs, DPP and permlanes, in stages whose entry EXEC is every lane.
Lane 63 is then the identity and lane 31 the subgroup reduction of the
host's invocations. A shuffle from lane 31 is not enough: an RTX 5070
runs the vertex warps of a non-indexed triangle list with 30 of their 32
invocations. A v_readlane_b32 outside the host subgroup of another value
that is 0 in lanes 32-63 reads 0. Only reads that used to throw change,
besides lane 31 of such scans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A wave64 vertex program with the culling scan of boykopovar#2783 (lane < s3's
primitive count, row_shr bound_ctrl:1 OR scan, v_permlanex16_b32,
v_readlane_b32 of lanes 31 and 63) compares the wave OR with the ballot
of its keys and collapses its triangle when they differ. On a 32-lane
device every pixel of the grid must be covered.

Before the s3 change the program does not build on 32 lanes; with a
shuffle from lane 31 instead of the subgroup reduction no pixel is
covered, as the device leaves lanes of each vertex warp idle. Other
subgroup widths skip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The half-wave scan rules for 32-lane hosts made lane 31 of a matched
scan a subgroup reduction and threw on a device without subgroup
arithmetic, where the read used to build as a shuffle of the scan. Such
a device now keeps the shuffle for lane 31; lane 63 still reads the
identity, which needs no arithmetic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AlxcBTW

AlxcBTW commented Oct 11, 2026

Copy link
Copy Markdown
Owner Author

Rebased onto current main and opened upstream as boykopovar#3546.

@AlxcBTW AlxcBTW closed this Oct 11, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NVIDIA: wave64 NGG culling vertex program reads lane 63 of a full-wave OR reduction (Taxi Chaos 2, PPSA30279)

1 participant