MOOR security

Dieser Eintrag ist auf Englisch verfasst.

Geometry shaders on a GPU without a geometry stage, and Microsoft's samples run unmodified

Entry 028634 of 816 tests pass

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:

  1. The vertex shader is drawn as points with rasterization off, so it only writes its outputs to a buffer.
  2. 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.
  3. 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.

How one D3D12 draw with a geometry shader becomes three Metal draws.D3D12 draw with a geometry shader → 1. vertex shader, drawn as points rasterization off writes vertex records; 1. vertex shader, drawn as points rasterization off writes vertex records → barrier vertex → object; barrier vertex → object → 2. geometry shader as object function one thread per primitive × GS instance fixed slots: maxvertexcount records; 2. geometry shader as object function one thread per primitive × GS instance fixed slots: maxvertexcount records → index slots prefilled 0xFFFFFFFF RestartStrip writes a restart; index slots prefilled 0xFFFFFFFF RestartStrip writes a restart → barrier object → vertex; barrier object → vertex → 3. indexed strip draw over all slots mtl12_gs_vertex returns records[id] restart index cuts the strip; 3. indexed strip draw over all slots mtl12_gs_vertex returns records[id] restart index cuts the strip → fragment shader, render_target_array_index relative to the RTV's first sliceD3D12 draw with ageometry shader1. vertex shader, drawn as pointsrasterization offwrites vertex recordsbarrier vertex → object2. geometry shader as object functionone thread per primitive × GS instancefixed slots: maxvertexcount recordsindex slots prefilled 0xFFFFFFFFRestartStrip writes a restartbarrier object → vertex3. indexed strip draw over all slotsmtl12_gs_vertex returns records[id]restart index cuts the stripfragment shader,render_target_array_indexrelative to the RTV's first slice
How one D3D12 draw with a geometry shader becomes three Metal draws. (Mermaid source)
Eight labelled frames from Microsoft's D3D12 samples in two rows, all on a dark blue background. HelloWindow is empty. HelloTriangle, HelloConstBuffers, HelloBundles and HelloFrameBuffering each show one triangle with red, green and blue corners. HelloTexture shows a black-and-white checkered triangle. DynamicIndexing shows rows of small city models on round bases, receding to the horizon and tinted from red to violet. ExecuteIndirect shows a narrow vertical column of many small triangles in pastel colours.
Frame 60 of the eight Microsoft samples, each checked against its reviewed 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.
Tests by suite at the end of entry 028: 634 of 816 pass (data as CSV)
  • pass
  • fail
  • skip

suite · share of each row's total

  1. vkd3d-proton (+13)412 pass, 27 fail, 138 skip of 577
  2. vkd3d (+3)173 pass, 12 fail, 5 skip of 190
  3. api37 pass, 0 fail, 0 skip of 37
  4. apps (+8)8 pass, 0 fail, 0 skip of 8
  5. samples4 pass, 0 fail, 0 skip of 4