MOOR security

Dieser Eintrag ist auf Englisch verfasst.

The bug was in the compiler, and a semaphore that never made anyone wait

Entry 039774 of 909 tests pass

A path tracer drew black because Wine's HLSL compiler dropped one conversion, not because of the layer; and the DXGI frame-latency object now actually paces games.

What changed

Three more Diligent Engine samples (Tutorial26, Atmosphere, Shadows) were supposed to need only an optional build component. With it built, all three still failed. The cause was the shader compiler, not the layer, so they stay out of the suite and the lean build stays.

The second change is pacing. With DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT, a game gets a semaphore (a counter that a thread can wait on until it is above zero) and waits on it before each frame. DXGI should release it only when an earlier frame has left the queue. We released it the moment Present returned, so the game never waited. Now a dxgi thread waits until the present has finished on the GPU, then releases it.

Two frames of a path-traced room. Left, with shaders from Wine's compiler: black except for the white ceiling light. Right, the same shaders compiled with DXC: a red wall, a green wall, a grey back wall and reflective, glass and gold spheres on the floor, with fine noise.
Tutorial26 through the layer, with bytecode from Wine's compiler (left, black but the light) and from DXC (right).
When the semaphore is released now, and where it was before.Participants: Game, Latency semaphore, dxgi Present, dxgi latency thread, Metal queue. 1. Game → Latency semaphore: wait (count 1 -> 0); 2. Game → dxgi Present: Present(frame 7); 3. dxgi Present → Metal queue: commit present, then signal event = 7; 4. dxgi Present → dxgi latency thread: queue frame 7; 5. Note (Game, dxgi Present): before: Present released the semaphore here, so the game never waited; 6. Game → Latency semaphore: wait (blocks); 7. dxgi latency thread → Metal queue: wait until event >= 7; 8. Metal queue → dxgi latency thread: frame 7 finished on the GPU; 9. dxgi latency thread → Latency semaphore: release (count 0 -> 1); 10. Latency semaphore → Game: wait returns, start frame 8GameLatency semaphoredxgi Presentdxgi latency threadMetal queuewait (count 1 -> 0)Present(frame 7)commit present, then signal event = 7queue frame 7before: Present released the semaphorehere, so the game never waitedwait (blocks)wait until event >= 7frame 7 finished on the GPUrelease (count 0 -> 1)wait returns, start frame 8
When the semaphore is released now, and where it was before. (Mermaid source)

The interesting part

Tutorial26 is a small path tracer. Through the layer it drew a black frame with only the ceiling light. Diligent's D3D12 backend compiles HLSL with FXC, the old compiler that makes DXBC bytecode, and under Wine that is Wine's own reimplementation. With Microsoft's DXC (which makes DXIL) selected by a local edit, the same layer drew the right picture. So the fault was on the DXBC path. Bisecting the shader showed the clip position was always (-1, 1).

Before debugging our DXBC translation, I compiled the shader with the Wine prefix's compiler and read the bytecode (tools/wine_hlslc.c). The float2(XY) conversion from unsigned integers was missing whenever the same XY also fed a texture Load. The integer bits were then read as float, which gives tiny denormals, so every pixel mapped to the corner. An 8-line repro is in the note. Games ship bytecode from Microsoft's compiler, so this is not ours to fix. The lesson is to look at the bytecode first.

The review of the latency change found a real hang. Present waited for the GPU to finish the present three frames back, so it could reuse that frame's command allocator. If the application had made the queue wait on a fence it had not signalled yet, a fourth present waited forever for work that could not start. Wine's own DXGI test does exactly this. Now the whole present is encoded when the queue reaches it, so Present never waits for the GPU.

Numbers

  • Full run: 774 pass, 27 fail, 108 skip of 909, against 773 of 908 in entry 038. 0 newly failing; the one new pass is the new test api/frame_latency.
  • api/frame_latency holds 5 presents behind one queue wait. Before the fix it hung in Present (a timeout, with the stack in waitUntilSignaledValue).
Tests by suite at the end of this entry: 774 of 909 pass (data as CSV)
  • pass
  • fail
  • skip

suite · share of each row's total

  1. vkd3d-proton455 pass, 18 fail, 104 skip of 577
  2. vkd3d177 pass, 9 fail, 4 skip of 190
  3. apps77 pass, 0 fail, 0 skip of 77
  4. api (+1)61 pass, 0 fail, 0 skip of 61
  5. samples4 pass, 0 fail, 0 skip of 4