This is an alternative proposal to !5830. It fixes the same glitches in Horizon Zero Dawn, but attempts to do that by implementing the proper behavior expected from the swapchain. More precisely, it fixes a few mistakes I had made the last time I touched this code, it implements the frame latency waitable with a semaphore instead of an event (as it should be) and at last it uses the frame latency waitable to implement an appropriate wait after presentation when the client didn't request to manage the waitable directly.
This implementation is not yet complete, because the semaphore should be released when the frame is presented. Knowing when this is the case would require using `VK_KHR_present_wait`. We currently approximate that with the moment in which the frame is submitted for presentation to Vulkan, which as far as I can tell is good enough. Eventually I'll try to write the proper thing.
This MR depends on https://gitlab.winehq.org/wine/vkd3d/-/merge_requests/1343, which has been submitted in the meantime to vkd3d. The vkd3d change is included in the first commit, so this MR is already functional.
-- v6: dxgi/tests: Use an explicit frame latency waitable when testing the back buffer index. dxgi: Wait on the frame latency semaphore when the client doesn't do it.
From: Giovanni Mascellani gmascellani@codeweavers.com
The new vkd3d API allows submitting presentation as soon as the internal vkd3d work is flushed to Vulkan. Now we're also waiting for it to be executed, which is useless. --- dlls/dxgi/swapchain.c | 2 +- dlls/wined3d/wined3d.spec | 1 + 2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/dlls/dxgi/swapchain.c b/dlls/dxgi/swapchain.c index 4797fcecd8d..dddf87f10b7 100644 --- a/dlls/dxgi/swapchain.c +++ b/dlls/dxgi/swapchain.c @@ -2363,7 +2363,7 @@ static HRESULT d3d12_swapchain_present(struct d3d12_swapchain *swapchain, } }
- if (FAILED(hr = ID3D12CommandQueue_Signal(swapchain->command_queue, + if (FAILED(hr = vkd3d_queue_signal_on_cpu(swapchain->command_queue, swapchain->present_fence, swapchain->frame_number))) { ERR("Failed to signal present fence, hf %#lx.\n", hr); diff --git a/dlls/wined3d/wined3d.spec b/dlls/wined3d/wined3d.spec index 151bcaf9751..73c62264677 100644 --- a/dlls/wined3d/wined3d.spec +++ b/dlls/wined3d/wined3d.spec @@ -360,6 +360,7 @@ @ cdecl vkd3d_resource_incref(ptr) @ cdecl vkd3d_serialize_root_signature(ptr long ptr ptr) @ cdecl vkd3d_serialize_versioned_root_signature(ptr ptr ptr) +@ cdecl vkd3d_queue_signal_on_cpu(ptr ptr long)
@ cdecl vkd3d_shader_compile(ptr ptr ptr) @ cdecl vkd3d_shader_convert_root_signature(ptr long ptr)
From: Giovanni Mascellani gmascellani@codeweavers.com
This is only partially correct, because the frame latency waitable should be a semaphore instead. However, it's a step in the right direction, in preparation to make the waitable an actual semaphore. --- dlls/dxgi/swapchain.c | 9 +++++++++ dlls/dxgi/tests/dxgi.c | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/dlls/dxgi/swapchain.c b/dlls/dxgi/swapchain.c index dddf87f10b7..4f217a7388d 100644 --- a/dlls/dxgi/swapchain.c +++ b/dlls/dxgi/swapchain.c @@ -2886,6 +2886,15 @@ static HRESULT STDMETHODCALLTYPE d3d12_swapchain_SetMaximumFrameLatency(IDXGISwa return DXGI_ERROR_INVALID_CALL; }
+ if (max_latency > swapchain->frame_latency) + { + if (!SetEvent(swapchain->frame_latency_event)) + { + ERR("Failed to set event, last error %lu.\n", GetLastError()); + return HRESULT_FROM_WIN32(GetLastError()); + } + } + swapchain->frame_latency = max_latency; return S_OK; } diff --git a/dlls/dxgi/tests/dxgi.c b/dlls/dxgi/tests/dxgi.c index 912bcd8fa61..7cf52d626da 100644 --- a/dlls/dxgi/tests/dxgi.c +++ b/dlls/dxgi/tests/dxgi.c @@ -7143,7 +7143,7 @@ static void test_frame_latency_event(IUnknown *device, BOOL is_d3d12) ok(frame_latency == 3, "Got unexpected frame latency %#x.\n", frame_latency);
wait_result = WaitForSingleObject(semaphore, 0); - todo_wine ok(!wait_result, "Got unexpected wait result %#lx.\n", wait_result); + ok(!wait_result, "Got unexpected wait result %#lx.\n", wait_result); wait_result = WaitForSingleObject(semaphore, 0); todo_wine ok(!wait_result, "Got unexpected wait result %#lx.\n", wait_result); wait_result = WaitForSingleObject(semaphore, 100);
From: Giovanni Mascellani gmascellani@codeweavers.com
The bias was a broken solution for handling the maximum frame latency changes. However we now already set the event (and, in the future, release the sempahore) when the maximum frame latency is increased, so there is no need for the bias anymore. --- dlls/dxgi/swapchain.c | 12 ++---------- dlls/dxgi/tests/dxgi.c | 2 +- 2 files changed, 3 insertions(+), 11 deletions(-)
diff --git a/dlls/dxgi/swapchain.c b/dlls/dxgi/swapchain.c index 4f217a7388d..35460691918 100644 --- a/dlls/dxgi/swapchain.c +++ b/dlls/dxgi/swapchain.c @@ -2296,12 +2296,8 @@ static HRESULT d3d12_swapchain_op_present_execute(struct d3d12_swapchain *swapch
if (swapchain->frame_latency_fence) { - /* Use the same bias as d3d12_swapchain_present(). Add one to - * account for the "++swapchain->frame_number" there. */ - uint64_t number = op->present.frame_number + DXGI_MAX_SWAP_CHAIN_BUFFERS + 1; - if (FAILED(hr = ID3D12CommandQueue_Signal(swapchain->command_queue, - swapchain->frame_latency_fence, number))) + swapchain->frame_latency_fence, op->present.frame_number + 1))) { ERR("Failed to signal frame latency fence, hr %#lx.\n", hr); return hr; @@ -2351,12 +2347,8 @@ static HRESULT d3d12_swapchain_present(struct d3d12_swapchain *swapchain, ++swapchain->frame_number; if ((frame_latency_event = swapchain->frame_latency_event)) { - /* Bias the frame number to avoid underflowing in - * SetEventOnCompletion(). */ - uint64_t number = swapchain->frame_number + DXGI_MAX_SWAP_CHAIN_BUFFERS; - if (FAILED(hr = ID3D12Fence_SetEventOnCompletion(swapchain->frame_latency_fence, - number - swapchain->frame_latency, frame_latency_event))) + swapchain->frame_number, frame_latency_event))) { ERR("Failed to enqueue frame latency event, hr %#lx.\n", hr); return hr; diff --git a/dlls/dxgi/tests/dxgi.c b/dlls/dxgi/tests/dxgi.c index 7cf52d626da..4ff27715459 100644 --- a/dlls/dxgi/tests/dxgi.c +++ b/dlls/dxgi/tests/dxgi.c @@ -7222,7 +7222,7 @@ static void test_frame_latency_event(IUnknown *device, BOOL is_d3d12) for (i = 0; i < 3; i++) { wait_result = WaitForSingleObject(semaphore, 100); - todo_wine_if(i != 0) + todo_wine ok(!wait_result, "Got unexpected wait result %#lx.\n", wait_result); }
From: Giovanni Mascellani gmascellani@codeweavers.com
Instead of going through the command queue again. That's because at that point we just submitted the frame for presentation to Vulkan, so the best approximation we have for when the frame will be presented is now, not after the currently outstanding work in the command queue is processed. It's still a rather poor approximation, but for something better we need VK_KHR_present_wait. --- dlls/dxgi/swapchain.c | 3 +-- dlls/dxgi/tests/dxgi.c | 1 - 2 files changed, 1 insertion(+), 3 deletions(-)
diff --git a/dlls/dxgi/swapchain.c b/dlls/dxgi/swapchain.c index 35460691918..94551fe14eb 100644 --- a/dlls/dxgi/swapchain.c +++ b/dlls/dxgi/swapchain.c @@ -2296,8 +2296,7 @@ static HRESULT d3d12_swapchain_op_present_execute(struct d3d12_swapchain *swapch
if (swapchain->frame_latency_fence) { - if (FAILED(hr = ID3D12CommandQueue_Signal(swapchain->command_queue, - swapchain->frame_latency_fence, op->present.frame_number + 1))) + if (FAILED(hr = ID3D12Fence_Signal(swapchain->frame_latency_fence, op->present.frame_number + 1))) { ERR("Failed to signal frame latency fence, hr %#lx.\n", hr); return hr; diff --git a/dlls/dxgi/tests/dxgi.c b/dlls/dxgi/tests/dxgi.c index 4ff27715459..b8cf3597483 100644 --- a/dlls/dxgi/tests/dxgi.c +++ b/dlls/dxgi/tests/dxgi.c @@ -7222,7 +7222,6 @@ static void test_frame_latency_event(IUnknown *device, BOOL is_d3d12) for (i = 0; i < 3; i++) { wait_result = WaitForSingleObject(semaphore, 100); - todo_wine ok(!wait_result, "Got unexpected wait result %#lx.\n", wait_result); }
From: Giovanni Mascellani gmascellani@codeweavers.com
It is now useless: we signal signal it directly and use it to set an event, without having anything to do with the CPU. We can set the event directly instead. --- dlls/dxgi/swapchain.c | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-)
diff --git a/dlls/dxgi/swapchain.c b/dlls/dxgi/swapchain.c index 94551fe14eb..a9d4ee0e2aa 100644 --- a/dlls/dxgi/swapchain.c +++ b/dlls/dxgi/swapchain.c @@ -1191,7 +1191,6 @@ struct d3d12_swapchain DXGI_SWAP_CHAIN_FULLSCREEN_DESC fullscreen_desc; LONG in_set_fullscreen_state;
- ID3D12Fence *frame_latency_fence; HANDLE frame_latency_event;
uint64_t frame_number; @@ -2057,9 +2056,6 @@ static void d3d12_swapchain_destroy(struct d3d12_swapchain *swapchain) if (swapchain->frame_latency_event) CloseHandle(swapchain->frame_latency_event);
- if (swapchain->frame_latency_fence) - ID3D12Fence_Release(swapchain->frame_latency_fence); - if (swapchain->command_queue) ID3D12CommandQueue_Release(swapchain->command_queue);
@@ -2294,12 +2290,12 @@ static HRESULT d3d12_swapchain_op_present_execute(struct d3d12_swapchain *swapch return hresult_from_vk_result(vr); }
- if (swapchain->frame_latency_fence) + if (swapchain->frame_latency_event) { - if (FAILED(hr = ID3D12Fence_Signal(swapchain->frame_latency_fence, op->present.frame_number + 1))) + if (!SetEvent(swapchain->frame_latency_event)) { - ERR("Failed to signal frame latency fence, hr %#lx.\n", hr); - return hr; + ERR("Failed to set frame latency event, last error %ld.\n", GetLastError()); + return HRESULT_FROM_WIN32(GetLastError()); } }
@@ -2310,7 +2306,6 @@ static HRESULT d3d12_swapchain_present(struct d3d12_swapchain *swapchain, unsigned int sync_interval, unsigned int flags) { struct d3d12_swapchain_op *op; - HANDLE frame_latency_event; HRESULT hr;
if (sync_interval > 4) @@ -2344,15 +2339,6 @@ static HRESULT d3d12_swapchain_present(struct d3d12_swapchain *swapchain, LeaveCriticalSection(&swapchain->worker_cs);
++swapchain->frame_number; - if ((frame_latency_event = swapchain->frame_latency_event)) - { - if (FAILED(hr = ID3D12Fence_SetEventOnCompletion(swapchain->frame_latency_fence, - swapchain->frame_number, frame_latency_event))) - { - ERR("Failed to enqueue frame latency event, hr %#lx.\n", hr); - return hr; - } - }
if (FAILED(hr = vkd3d_queue_signal_on_cpu(swapchain->command_queue, swapchain->present_fence, swapchain->frame_number))) @@ -3358,14 +3344,6 @@ static HRESULT d3d12_swapchain_init(struct d3d12_swapchain *swapchain, IWineDXGI { swapchain->frame_latency = 1;
- if (FAILED(hr = ID3D12Device_CreateFence(device, DXGI_MAX_SWAP_CHAIN_BUFFERS, - 0, &IID_ID3D12Fence, (void **)&swapchain->frame_latency_fence))) - { - WARN("Failed to create frame latency fence, hr %#lx.\n", hr); - d3d12_swapchain_destroy(swapchain); - return hr; - } - if (!(swapchain->frame_latency_event = CreateEventW(NULL, FALSE, TRUE, NULL))) { hr = HRESULT_FROM_WIN32(GetLastError());
From: Giovanni Mascellani gmascellani@codeweavers.com
As it should be, given that it is supposed to keep the count of how many frames are currently in flight. --- dlls/dxgi/swapchain.c | 24 ++++++++++++------------ dlls/dxgi/tests/dxgi.c | 4 +--- 2 files changed, 13 insertions(+), 15 deletions(-)
diff --git a/dlls/dxgi/swapchain.c b/dlls/dxgi/swapchain.c index a9d4ee0e2aa..a3c2b9858b3 100644 --- a/dlls/dxgi/swapchain.c +++ b/dlls/dxgi/swapchain.c @@ -1191,7 +1191,7 @@ struct d3d12_swapchain DXGI_SWAP_CHAIN_FULLSCREEN_DESC fullscreen_desc; LONG in_set_fullscreen_state;
- HANDLE frame_latency_event; + HANDLE frame_latency_semaphore;
uint64_t frame_number; uint32_t frame_latency; @@ -2053,8 +2053,8 @@ static void d3d12_swapchain_destroy(struct d3d12_swapchain *swapchain) if (swapchain->present_fence) ID3D12Fence_Release(swapchain->present_fence);
- if (swapchain->frame_latency_event) - CloseHandle(swapchain->frame_latency_event); + if (swapchain->frame_latency_semaphore) + CloseHandle(swapchain->frame_latency_semaphore);
if (swapchain->command_queue) ID3D12CommandQueue_Release(swapchain->command_queue); @@ -2290,11 +2290,11 @@ static HRESULT d3d12_swapchain_op_present_execute(struct d3d12_swapchain *swapch return hresult_from_vk_result(vr); }
- if (swapchain->frame_latency_event) + if (swapchain->frame_latency_semaphore) { - if (!SetEvent(swapchain->frame_latency_event)) + if (!ReleaseSemaphore(swapchain->frame_latency_semaphore, 1, NULL)) { - ERR("Failed to set frame latency event, last error %ld.\n", GetLastError()); + ERR("Failed to release frame latency semaphore, last error %ld.\n", GetLastError()); return HRESULT_FROM_WIN32(GetLastError()); } } @@ -2865,9 +2865,9 @@ static HRESULT STDMETHODCALLTYPE d3d12_swapchain_SetMaximumFrameLatency(IDXGISwa
if (max_latency > swapchain->frame_latency) { - if (!SetEvent(swapchain->frame_latency_event)) + if (!ReleaseSemaphore(swapchain->frame_latency_semaphore, max_latency - swapchain->frame_latency, NULL)) { - ERR("Failed to set event, last error %lu.\n", GetLastError()); + ERR("Failed to release frame latency semaphore, last error %lu.\n", GetLastError()); return HRESULT_FROM_WIN32(GetLastError()); } } @@ -2900,10 +2900,10 @@ static HANDLE STDMETHODCALLTYPE d3d12_swapchain_GetFrameLatencyWaitableObject(ID
TRACE("iface %p.\n", iface);
- if (!swapchain->frame_latency_event) + if (!swapchain->frame_latency_semaphore) return NULL;
- ret = DuplicateHandle(GetCurrentProcess(), swapchain->frame_latency_event, GetCurrentProcess(), + ret = DuplicateHandle(GetCurrentProcess(), swapchain->frame_latency_semaphore, GetCurrentProcess(), &dup, 0, FALSE, DUPLICATE_SAME_ACCESS);
if (!ret) @@ -3344,10 +3344,10 @@ static HRESULT d3d12_swapchain_init(struct d3d12_swapchain *swapchain, IWineDXGI { swapchain->frame_latency = 1;
- if (!(swapchain->frame_latency_event = CreateEventW(NULL, FALSE, TRUE, NULL))) + if (!(swapchain->frame_latency_semaphore = CreateSemaphoreW(NULL, swapchain->frame_latency, LONG_MAX, NULL))) { hr = HRESULT_FROM_WIN32(GetLastError()); - WARN("Failed to create frame latency event, hr %#lx.\n", hr); + WARN("Failed to create frame latency semaphore, hr %#lx.\n", hr); d3d12_swapchain_destroy(swapchain); return hr; } diff --git a/dlls/dxgi/tests/dxgi.c b/dlls/dxgi/tests/dxgi.c index b8cf3597483..a5a76a6158a 100644 --- a/dlls/dxgi/tests/dxgi.c +++ b/dlls/dxgi/tests/dxgi.c @@ -7145,7 +7145,7 @@ static void test_frame_latency_event(IUnknown *device, BOOL is_d3d12) wait_result = WaitForSingleObject(semaphore, 0); ok(!wait_result, "Got unexpected wait result %#lx.\n", wait_result); wait_result = WaitForSingleObject(semaphore, 0); - todo_wine ok(!wait_result, "Got unexpected wait result %#lx.\n", wait_result); + ok(!wait_result, "Got unexpected wait result %#lx.\n", wait_result); wait_result = WaitForSingleObject(semaphore, 100); ok(wait_result == WAIT_TIMEOUT, "Got unexpected wait result %#lx.\n", wait_result);
@@ -7184,7 +7184,6 @@ static void test_frame_latency_event(IUnknown *device, BOOL is_d3d12) for (i = 0; i < 5; i++) { wait_result = WaitForSingleObject(semaphore, 100); - todo_wine_if(i != 0) ok(!wait_result, "Got unexpected wait result %#lx.\n", wait_result); }
@@ -7236,7 +7235,6 @@ static void test_frame_latency_event(IUnknown *device, BOOL is_d3d12) for (i = 0; i < 4; i++) { wait_result = WaitForSingleObject(semaphore, 100); - todo_wine_if(i != 0) ok(!wait_result, "Got unexpected wait result %#lx.\n", wait_result); }
From: Giovanni Mascellani gmascellani@codeweavers.com
The client is expected to wait on the frame latency semaphore when the swapchain is created with DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT. But when it is not, we're supposed to do it ourselves. --- dlls/dxgi/swapchain.c | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-)
diff --git a/dlls/dxgi/swapchain.c b/dlls/dxgi/swapchain.c index a3c2b9858b3..b81063c5816 100644 --- a/dlls/dxgi/swapchain.c +++ b/dlls/dxgi/swapchain.c @@ -2290,13 +2290,10 @@ static HRESULT d3d12_swapchain_op_present_execute(struct d3d12_swapchain *swapch return hresult_from_vk_result(vr); }
- if (swapchain->frame_latency_semaphore) + if (!ReleaseSemaphore(swapchain->frame_latency_semaphore, 1, NULL)) { - if (!ReleaseSemaphore(swapchain->frame_latency_semaphore, 1, NULL)) - { - ERR("Failed to release frame latency semaphore, last error %ld.\n", GetLastError()); - return HRESULT_FROM_WIN32(GetLastError()); - } + ERR("Failed to release frame latency semaphore, last error %ld.\n", GetLastError()); + return HRESULT_FROM_WIN32(GetLastError()); }
return S_OK; @@ -2322,6 +2319,17 @@ static HRESULT d3d12_swapchain_present(struct d3d12_swapchain *swapchain, return S_OK; }
+ if (!(swapchain->desc.Flags & DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT)) + { + /* After around two seconds Present() will succeed anyway, without any error. */ + DWORD ret = WaitForSingleObject(swapchain->frame_latency_semaphore, 2000); + if (ret != WAIT_OBJECT_0 && ret != WAIT_TIMEOUT) + { + ERR("Failed to wait for frame latency semaphore, last error %ld.\n", GetLastError()); + return HRESULT_FROM_WIN32(GetLastError()); + } + } + if (!(op = calloc(1, sizeof(*op)))) { WARN("Cannot allocate memory.\n"); @@ -2900,8 +2908,11 @@ static HANDLE STDMETHODCALLTYPE d3d12_swapchain_GetFrameLatencyWaitableObject(ID
TRACE("iface %p.\n", iface);
- if (!swapchain->frame_latency_semaphore) + if (!(swapchain->desc.Flags & DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT)) + { + WARN("DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT not set for swap chain %p.\n", iface); return NULL; + }
ret = DuplicateHandle(GetCurrentProcess(), swapchain->frame_latency_semaphore, GetCurrentProcess(), &dup, 0, FALSE, DUPLICATE_SAME_ACCESS); @@ -3341,16 +3352,16 @@ static HRESULT d3d12_swapchain_init(struct d3d12_swapchain *swapchain, IWineDXGI swapchain->current_buffer_index = 0;
if (swapchain_desc->Flags & DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT) - { swapchain->frame_latency = 1; + else + swapchain->frame_latency = 3;
- if (!(swapchain->frame_latency_semaphore = CreateSemaphoreW(NULL, swapchain->frame_latency, LONG_MAX, NULL))) - { - hr = HRESULT_FROM_WIN32(GetLastError()); - WARN("Failed to create frame latency semaphore, hr %#lx.\n", hr); - d3d12_swapchain_destroy(swapchain); - return hr; - } + if (!(swapchain->frame_latency_semaphore = CreateSemaphoreW(NULL, swapchain->frame_latency, LONG_MAX, NULL))) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + WARN("Failed to create frame latency semaphore, hr %#lx.\n", hr); + d3d12_swapchain_destroy(swapchain); + return hr; }
if (FAILED(hr = ID3D12Device_CreateFence(device, 0, 0,
From: Giovanni Mascellani gmascellani@codeweavers.com
We're not going to use it anyway, but prevent DXGI from implicitly wait on it. That wouldn't change the result result because Present() eventually times out anyway, but it would uselessly waste a few seconds. --- dlls/dxgi/tests/dxgi.c | 1 + 1 file changed, 1 insertion(+)
diff --git a/dlls/dxgi/tests/dxgi.c b/dlls/dxgi/tests/dxgi.c index a5a76a6158a..7897a7158ae 100644 --- a/dlls/dxgi/tests/dxgi.c +++ b/dlls/dxgi/tests/dxgi.c @@ -4978,6 +4978,7 @@ static void test_swapchain_backbuffer_index(IUnknown *device, BOOL is_d3d12) { swapchain_desc.BufferCount = 4; swapchain_desc.SwapEffect = swap_effects[i]; + swapchain_desc.Flags = DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT; expected_hr = is_d3d12 && !is_flip_model(swap_effects[i]) ? DXGI_ERROR_INVALID_CALL : S_OK; hr = IDXGIFactory_CreateSwapChain(factory, device, &swapchain_desc, &swapchain); ok(hr == expected_hr, "Got unexpected hr %#lx, expected %#lx.\n", hr, expected_hr);
On Wed Feb 26 21:53:04 2025 +0000, Elizabeth Figura wrote:
It happens with both radv and llvmpipe, from Mesa 24.3.4-3. Timeline semaphores are available.
Actually that's correct, `Present()` is really expected to block in that situation, with the explicit frame latency waitable. So I was puzzled why it wouldn't do that on native, and it turns out that it did, but it also seem to have a 2 seconds timeout, which I implemented. But I also enabled an explicit frame latency waitable so that we don't waste time in useless waits.
On Thu Feb 27 15:11:58 2025 +0000, Giovanni Mascellani wrote:
Actually that's correct, `Present()` is really expected to block in that situation, with the explicit frame latency waitable. So I was puzzled why it wouldn't do that on native, and it turns out that it did, but it also seem to have a 2 seconds timeout, which I implemented. But I also enabled an explicit frame latency waitable so that we don't waste time in useless waits.
In theory I could add tests for that, but they would likely be rather flaky anyway. And take at least two seconds on additional wall clock time. Let me know if you think we want that anyway.
This merge request was approved by Elizabeth Figura.
This merge request was approved by Jan Sikorski.