Skip to content

D3D12: IFence::Wait() returns immediately after the first time it has blocked (the manual-reset event is never reset) #816

Open

Description

@fabiomarcellusdev

Where: Graphics/GraphicsEngineD3D12/src/FenceD3D12Impl.cpp, FenceD3D12Impl::Wait, as of DiligentCore master (read on 2026-10-21) and as of commit 074060c.

What the code does. The constructor creates one event with CreateEvent(NULL, TRUE /* manual-reset */, FALSE, NULL). Wait(Value) does:

if (GetCompletedValue() >= Value) return;
if (m_FenceCompleteEvent != NULL)
{
    m_pd3d12Fence->SetEventOnCompletion(Value, m_FenceCompleteEvent);
    WaitForSingleObject(m_FenceCompleteEvent, INFINITE);
}

and nothing in Graphics/GraphicsEngineD3D12 ever calls ResetEvent on the handle. A manual-reset event stays signalled once it has been signalled, so after the first Wait that really blocked, every later Wait(Value) whose value the GPU has not reached yet calls SetEventOnCompletion and then returns from WaitForSingleObject at once, with GetCompletedValue() < Value.

What we saw. An application bounded its frames in flight with pFence->Wait(frame - 3) at the start of every frame (the fence is signalled with EnqueueSignal(pFence, frame) after each frame). With GPU-heavy frames, right after Wait(frame - 3) returned, GetCompletedValue() read frame - 4 (a gap of 4 where 3 is intended), over and over, from the second Wait that needed to block onward. Frames in flight were then unbounded, which broke the application's per-frame resource accounting. A test that records a GPU-heavy frame 40 times and checks GetCompletedValue() >= frame - 3 after every Wait fails from the fifth frame on D3D12 with this code. The Vulkan backend did not show it (by reading, its Wait is a timeline-semaphore wait where supported). I have not written a standalone reproduction outside our application; I can, if it would help.

Expected. Wait(Value) returns only when the fence has reached Value (or the device is lost).

Suggested fix. Reset the event before arming it (ResetEvent(m_FenceCompleteEvent) before SetEventOnCompletion); if Wait may be called from several threads at once, a per-call event is safer than one shared event. A loop on GetCompletedValue() after the wait is also cheap insurance.

Workaround on our side. Call Wait, then poll GetCompletedValue() (with a yield) until it reaches the value.

Activity

  1. hzqst commented on Oct 24, 2026

    @hzqst
    Contributor

    I hit this issue too, together with a separate bug, and fixed both in one PR:

    PR #817 fixes both, with one commit per issue, and adds a regression test for this one (FenceTest.RepeatedCPUWait).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions