Skip to content

vk: mesh programs with control topology 1 draw points, with the point sprite coordinate (grass shadow mesh on AMD, #302) - #482

Merged
NotRexed merged 1 commit into
NotRexed:mainfrom
ZurielAtr:fix/mesh-point-topology
Nov 8, 2026
Merged

NotRexed merged 1 commit into
NotRexed:mainfrom
ZurielAtr:fix/mesh-point-topology

Conversation

@ZurielAtr

Copy link
Copy Markdown
Contributor

What was wrong

NVN2's mesh control section (+0x5f) says 3 for triangles. Four mesh stages in OoT say 1 (ef6e1a5fde0988f0,
df6cf52500d013a4, ed01e3e8ff1c102b, 644dba7ae7b8bced). They draw the grass into the 2400 x 2400 shadow map that
follows Link, in programs 02ee95fbf9e0050d and b0b3b40244c71ec7 among others. mesh_registers_of passed that 1 on
as GL's 1, lines, but these programs draw points. Their native code:

  • each lane takes its compacted index popc(ballot & lanemask_lt) and writes one map byte, at 4 + i, with the value i;
  • the primitive count at map byte 3 is popc(ballot), the number of vertices;
  • the vertex lines are PointSize and the position (1b 1c 1d 1e 1f).

Read as lines, primitive p joined vertex 2p to 2p + 1, and the second half of the index bytes was never written.
The grass patch around Link came out in the shadow map as long thin streaks spanning the patch, and the shadow mask
drew them on everything near him: a flickering dark diagonal mesh on the ground, the water, rocks and Link.

Their fragment program then reads generic 5 (s, t, r, perspective), which the mesh program never writes. It samples
an alpha mask with it and discards: that is the hardware's point sprite coordinate, which gives each point its shape.
Left at (0, 0, 0, 1), every point was a filled square.

Change

  • vk_shader.cpp: control topology 1 maps to GL 0 (points); 3 stays 4 (triangles).
  • MeshInfo::Corners(): index bytes per primitive (points 1, lines 2, triangles 3). It is used for the map region's size,
    the index builtin and the epilogue.
  • Points: OutputPoints, PrimitivePointIndicesEXT (one uint per primitive, stored as a scalar), one index byte at
    4 + primitive. The lines path stays for a real GL 1.
  • RuntimeInfo::point_coord_generics: when the mesh program draws points, the generics its fragment program reads and
    the mesh program never writes are gl_PointCoord (s, t, 0, 1). The perspective factor Eden's IPA applies to
    generics cancels out as it does for the others.
  • tests/test_mesh_point_topology.py (game-free, wired into source-regressions.yml): evaluates the control-to-GL
    mapping and Corners(), and checks that the translator accepts points, the SPIR-V side declares and stores them, and
    the point sprite coordinate replaces only the unwritten generics of point mesh programs. It fails on main, and
    without either half of the change.

This touches the translator, so the shader stamp changes and caches are rebuilt on the first start.

Tested

AMD Radeon RX 7700S 8 GB (laptop), Adrenalin 26.10.1, Windows 11, Hyrule Field, in the sun with grass around Link.

  • Before: a dark striped mesh in a large square around Link. It went away only with
    RX_VK_SKIP_PROG=53509044401d78d2,02ee95fbf9e0050d,b0b3b40244c71ec7, which also drops the grass shadow.
  • After: no mesh, and the grass casts its shadow again. Full-resolution F9 dumps of the 2400 x 2400 map (sampled by the
    shadow mask pass 3427a1dbefd5e02c) show the patch as separate rounded points instead of streaks; with points alone
    and no point coordinate they were squares.
  • The two programs translate without errors, and the startup preparation has the same 57 failures as before.

The tested build also carried #475 (the mesh stage buffer zeroed in the prologue). This change does not depend on it:
with points, the epilogue reads only the index bytes the program writes. Not tested on NVIDIA.

Addresses the grass shadow mesh in #302. The crops and bushes that vanish when Link gets close are a separate problem
and are not fixed by this.

🤖 Generated with Claude Code

… sprite coordinate

NVN2's mesh control section says 3 for triangles; OoT's four mesh stages with a 1 (the grass drawn
into the 2400 x 2400 shadow map around Link, programs 02ee95fbf9e0050d and b0b3b40244c71ec7 among
them) draw points: one index byte per primitive at 4 + i, the vertex count as the primitive count,
and a point size. Read as lines (GL's 1), each primitive joined vertex 2i to 2i + 1 of its meshlet:
long thin streaks in the shadow map and a flickering dark mesh on the ground, the water and Link
around him on AMD (issue 302).

- Control topology 1 maps to GL 0, points: OutputPoints, PrimitivePointIndicesEXT (a scalar per
  primitive), one map byte per primitive (MeshInfo::Corners()).
- Their fragment program reads generic 5 (s, t, r), which the mesh program never writes: the
  hardware's point sprite coordinate, with which it cuts each point out of an alpha mask. The
  generics a point mesh program leaves unwritten are now gl_PointCoord (s, t, 0, 1)
  (RuntimeInfo::point_coord_generics); left at (0, 0, 0, 1) every point was a filled square.
- tests/test_mesh_point_topology.py checks both, game-free.

Tested on an RX 7700S (Adrenalin 26.10.1, Windows 11) in Hyrule Field: the mesh is gone, the grass
casts its shadow again, and F9 dumps of the shadow map show rounded points instead of streaks.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@NotRexed
NotRexed merged commit c072313 into NotRexed:main Nov 8, 2026
4 checks passed
@NotRexed

NotRexed commented Nov 8, 2026

Copy link
Copy Markdown
Owner

Merged into main (c072313); it ships in the next release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants