Geometry shaders on a GPU without a geometry stage, and Microsoft's samples run unmodified
D3D12 geometry shaders now run on Metal as an object function plus a strip draw, and eight of Microsoft's D3D12 samples, built unmodified, render correctly through the layer.
What changed
Metal has no geometry stage. A D3D12 geometry shader (GS) runs once per input primitive and emits a variable number of vertices as strips, cutting them wherever it likes. The layer now turns one such draw into three draws in the same render encoder:
- The vertex shader is drawn as points with rasterization off, so it only writes its outputs to a buffer.
- The GS runs as an object function, the compute-like first stage of Metal's mesh pipeline. A patch to SPIRV-Cross translates it. Each thread writes its vertices and indices into fixed slots.
- An indexed strip draw goes over all the slots.
The same work made layered rendering work, which means writing a render-target array slice from a shader. Layered rendering also works from vertex shaders now.
The second change is the first real applications. Eight of Microsoft's DirectX-Graphics-Samples build unmodified with mingw and run under Wine for 60 frames: six D3D12HelloWorld samples, DynamicIndexing and ExecuteIndirect. Frame 60 of each is checked against its golden image.

The interesting part
Two things.
Strip restarts. The fixed slots only work if unused slots can be skipped. A spike measured how Metal handles the restart index 0xFFFFFFFF. Triangle and line strips honour it, even in runs and at the start of the buffer. Point lists don't: each restart index runs the vertex function with vertex id 0xFFFFFFFF, 92 times in the spike. So the pass-through vertex function returns a position that clipping rejects for that id.
Checking ExecuteIndirect. The ExecuteIndirect sample places 1,024 triangles with rand(). To check the frame without trusting my own render, the probe script replays the C runtime's rand() sequence and the sample's float32 updates for 60 frames. It then predicts the exact colour of 14 triangle pixels. One detail mattered: XMFLOAT4(rand(), rand(), rand(), 0) evaluates its arguments right to left in this build, so the first random number becomes z. A small test program confirmed that. With left-to-right order, 14 of the 26 probes fail; with the measured order, all pass.
Numbers
- Full run: 634 pass, 39 fail, 143 skip of 816. At the end of the last entry it was 610/53/145 of 808.
- 0 tests newly failing.
- 14 fail → pass, 2 skip → pass, plus 8 new app tests.
- Two tile-mapping tests that had failed under GPU load now pass. Later queue work waits for an event after each mapping update (13 of 13 runs with a game loading the GPU).
- Apps: 8 of 8 pass, and the frames are identical across three runs.
- pass
- fail
- skip
suite · share of each row's total
- vkd3d-proton (+13)412 pass, 27 fail, 138 skip of 577
- vkd3d (+3)173 pass, 12 fail, 5 skip of 190
- api37 pass, 0 fail, 0 skip of 37
- apps (+8)8 pass, 0 fail, 0 skip of 8
- samples4 pass, 0 fail, 0 skip of 4