MOOR security

Dieser Eintrag ist auf Englisch verfasst.

Godot 4.7 renders on our D3D12, after two shader-translator fixes

Entry 061844 of 948 tests pass

The unmodified Godot 4.7.2 Windows release now runs a small 3D scene with its D3D12 Forward+ renderer on Metal through mtl12, and the frame is checked in the test suite.

What changed

Godot is the first full game engine in the apps suite (the suite that runs real programs under Wine and checks a frame they present). I run the official Windows build, unmodified, on a small scene: a red box with a sun shadow on a grey plane. Godot's own renderer line reads "D3D12 12_0 - Forward+ - Using Device #0: Apple - Apple M1 Max". Fallback to Vulkan or OpenGL is switched off, and the test checks that line.

Two pipelines failed before the scene drew. Both failures were in SPIRV-Cross, the library that turns SPIR-V (the intermediate shader form we get from DXIL) into Metal's shading language:

  • A loop whose whole body was one bounds-checked store. SPIRV-Cross put that store, an if (...) { ... }, inside the for (...) header as the loop's increment, and Metal rejected it. Patch 0039 has such a loop's last block emitted as ordinary statements.
  • A compute shader that reads one image before it writes another, both from the descriptor heap (the GPU-visible table of resource descriptors). The second fix is below.
A simple Godot scene: a red cube casting a small shadow on a grey plane under a dark blue sky.
Frame 60 of the godot-scene test, Godot's Forward+ renderer on D3D12 through mtl12 (the scene is static, so no GIF).
The Godot editor running through D3D12: a 3D viewport with a grid and red, green and blue axes, the scene and file panels on the left, an empty inspector on the right and a loading dialog in the middle.
The Godot editor's tenth frame on mtl12, still loading its layout (scaled to half size).
Why the read-before-write shader ran out of translation passes, and what patch 0040 changes.before patch 0040: pass 1: B written, so A becomes read_write, pass 2: aliased descriptor array renamed for A, pass 3 would start: Maximum compilation loops; with patch 0040: access settled a pass earlier: within the limit, MSL to Metal; Godot compute shader: read image A, then write image B (both from the descriptor heap) → pass 0: read A emitted, no write seen yet; pass 0: read A emitted, no write seen yet → pass 1: B written, so A becomes read_write; pass 1: B written, so A becomes read_write → pass 2: aliased descriptor array renamed for A; pass 2: aliased descriptor array renamed for A → pass 3 would start: Maximum compilation loops; Godot compute shader: read image A, then write image B (both from the descriptor heap) → pass 0: OpImageWrite found up front, A is read_write at once; pass 0: OpImageWrite found up front, A is read_write at once → access settled a pass earlier: within the limit, MSL to Metalwith patch 0040Godot compute shader:read image A, then write image B(both from the descriptor heap)pass 0: read A emitted,no write seen yetpass 1: B written, so Abecomes read_writepass 2: aliased descriptorarray renamed for Apass 3 would start:Maximum compilation loopspass 0: OpImageWrite found up front,A is read_write at onceaccess settled a pass earlier:within the limit, MSL to Metalbefore patch 0040
Why the read-before-write shader ran out of translation passes, and what patch 0040 changes. (Mermaid source)

The interesting part

SPIRV-Cross translates a shader in passes. When it learns something late, it starts again. It allows three passes, and a fourth only if the restart that asked for it counted as "forward progress". It decides whether a storage image is read-only as it goes. When an image is read before any write has been seen, the access settles a pass late, and renaming the shared descriptor array for it costs another pass. That was one pass too many: "Maximum compilation loops detected". Patch 0040 looks for image write and atomic instructions up front, so the access is known from the first pass.

A later review of these changes found that patch 0039's own restart didn't count as progress either, so it could have used up the same limit. A block only ever goes from "expression" to "statements", so that restart is now marked as progress.

Each fix has an API test that fails without it: four new tests. The Godot test fails without 0040 on Godot's own ERROR: Create(Compute)PipelineState failed with error 0x80070057. The editor starts too and draws its interface and 3D viewport, but there is no editor test yet.

Numbers

  • Full run: 844 pass, 28 fail, 76 skip of 948, against 839 of 943 at the end of entry 060. The 5 newly passing tests are this session's new ones, and none is newly failing.
  • A Godot run to frame 60 takes about 9 s; two runs gave identical frames (0 pixels differ).
Tests by suite at the end of this entry: 844 of 948 pass (data as CSV)
  • pass
  • fail
  • skip

suite · share of each row's total

  1. vkd3d-proton490 pass, 15 fail, 72 skip of 577
  2. vkd3d177 pass, 9 fail, 4 skip of 190
  3. api (+4)79 pass, 0 fail, 0 skip of 79
  4. apps (+1)78 pass, 0 fail, 0 skip of 78
  5. wine-dxgi16 pass, 4 fail, 0 skip of 20
  6. samples4 pass, 0 fail, 0 skip of 4