Triangle fans on a GPU API that has none
Metal can't draw triangle fans, so mtl12 rewrites each fan into a triangle list on the GPU just before the draw, keeping the vertex that flat shading reads in the right place.
What changed
A triangle fan is a primitive type where every triangle shares the first vertex, the "hub": vertices h, a, b, c draw triangles (h, a, b) and (h, b, c). Direct3D 12 offers fans as an optional feature (TriangleFanSupported), mostly for translation layers that run Direct3D 9 games on top of it. Metal has no fan primitive.
mtl12 already rewrote some index buffers on the GPU just before the draws that use them, for strip cut values Metal lacks. Fans now use the same pre-pass. A fan of n vertices becomes a triangle list of 3(n − 2) 32-bit indices, and triangle t is written as (v[t+1], v[t+2], hub). The rotation matters. A triangle's "provoking vertex" is the one a non-interpolated (flat) attribute comes from. For fans it is v[t+1], while Metal takes the first vertex of a list triangle. Rotating puts v[t+1] first and keeps the winding, so back-face culling still works. A fan that isn't indexed gets generated indices 0, 1, 2….
Fans with a "cut" value (a special index that ends one fan and starts the next) are converted in a single threadgroup per draw. It walks the fan in chunks of 256 triangles and carries the current hub from one chunk to the next. My first version had each GPU thread scan backwards for the last cut. That costs O(n²) for a long fan, and the review after the milestone flagged it as a GPU watchdog risk.

The interesting part
The new test passed with the hub carry deliberately broken. A broken carry makes a triangle use the cut value 0xffffffff as its hub's index. That index lies past the vertex buffer, and the results fit that vertex being drawn with a position of zeros. In the test every fan's hub sat at (0, 0), so the wrong triangles covered exactly the right pixels. The test now stores positions 0.25 off the origin and the vertex shader subtracts it again. With that change the same mutation fails. The lesson is to break the code once on purpose and watch the new test fail.
Numbers
- Full run: 856 pass, 27 fail, 73 skip of 956, up from 853.
- vkd3d-proton test_topology_triangle_fan went from skipped to passing. The new api/triangle_fan test passes all 691 checks, and 561 of them fail without the change.
- pass
- fail
- skip
suite · share of each row's total
- vkd3d-proton (+1)494 pass, 14 fail, 69 skip of 577
- vkd3d177 pass, 9 fail, 4 skip of 190
- api (+1)85 pass, 0 fail, 0 skip of 85
- apps (+1)80 pass, 0 fail, 0 skip of 80
- wine-dxgi16 pass, 4 fail, 0 skip of 20
- samples4 pass, 0 fail, 0 skip of 4