ocornut / imgui

Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies
MIT License
60.96k stars 10.29k forks source link

DX12: One frame delay of platform window size update #7152

Open SuperWangKai opened 10 months ago

SuperWangKai commented 10 months ago

Version Info:

Dear ImGui 1.90.0 (19000)
--------------------------------
sizeof(size_t): 8, sizeof(ImDrawIdx): 2, sizeof(ImDrawVert): 20
define: __cplusplus=199711
define: _WIN32
define: _WIN64
define: _MSC_VER=1938
define: _MSVC_LANG=201402
define: IMGUI_HAS_VIEWPORT
define: IMGUI_HAS_DOCK
--------------------------------
io.BackendPlatformName: imgui_impl_win32
io.BackendRendererName: imgui_impl_dx12
io.ConfigFlags: 0x00000443
 NavEnableKeyboard
 NavEnableGamepad
 DockingEnable
 ViewportsEnable
io.ConfigViewportsNoDecoration
io.ConfigInputTextCursorBlink
io.ConfigWindowsResizeFromEdges
io.ConfigMemoryCompactTimer = 60.0
io.BackendFlags: 0x00001C0E
 HasMouseCursors
 HasSetMousePos
 PlatformHasViewports
 HasMouseHoveredViewport
 RendererHasVtxOffset
 RendererHasViewports
--------------------------------
io.Fonts: 1 fonts, Flags: 0x00000000, TexSize: 512,64
io.DisplaySize: 1264.00,761.00
io.DisplayFramebufferScale: 1.00,1.00
--------------------------------
style.WindowPadding: 8.00,8.00
style.WindowBorderSize: 1.00
style.FramePadding: 4.00,3.00
style.FrameRounding: 0.00
style.FrameBorderSize: 0.00
style.ItemSpacing: 8.00,4.00
style.ItemInnerSpacing: 4.00,4.00

My Issue/Question:

When we dragging and resizing a dock window (Platform Window), there is one frame delay of the update of the window size for dock window.

I put logs in these places to show the order:

Here is the log and I added extra comments using //. (293,692) is the size of the window when resizing.

===> New ImGui Frame.  // Frame N
WM_SIZE event of platform window: (293,692). // WM_SIZE happens in Frame N
ImGui_ImplDX12_SetWindowSize: (293, 692).  // Backbuffer recreated in Frame N

===> New ImGui Frame. // Frame N+1
UpdateViewportsNewFrame: (293,692). // Platform size info got updated in Frame N+1

===> New ImGui Frame. // Frame N+1
//...

In fact, as the order is incorrect, PlatformRequestResize flag is cleared in ImGuiViewportP::ClearRequestFlags() before it can actually be used to trigger the resize work.

Screenshots/Video By adding a sleep in the main loop to make the refresh rate low, issue can be seen easily.

//...
::Sleep(80); // sleep for 80 milliseconds.

OutputDebugStringA("===> New ImGui Frame.\n");

// Start the Dear ImGui frame
ImGui_ImplDX12_NewFrame();
ImGui_ImplWin32_NewFrame();
ImGui::NewFrame();
//...

Screen record:

https://github.com/ocornut/imgui/assets/15721569/ea571de2-fced-4a15-8f45-a16b11b59995

SuperWangKai commented 10 months ago

After changing swapchain scaling from DXGI_SCALING_STRETCH to DXGI_SCALING_NONE. I could not see resizing glitch and zoom-in/out effact when dragging the border of the window. I think this change could be a workaround.

https://github.com/ocornut/imgui/blob/8add6bcb9f5752d53f55f2bfc959238500bc0390/backends/imgui_impl_dx12.cpp#L902

SuperWangKai commented 10 months ago

I also logged for the DX9 and DX11 backends they have the same order issue. Since they don't have scaling-stretch option for swapchain, so I did not notice the issue. So I think it should be a common issue of docking (on Windows?).

ocornut commented 10 months ago

In fact, as the order is incorrect, PlatformRequestResize flag is cleared in ImGuiViewportP::ClearRequestFlags() before it can actually be used to trigger the resize work.

This is intentional. PlatformRequestResize is used when resizing from Platform/OS decorations (e.g. window resizing border) which are disabled by default as io.ConfigViewportsNoDecoration == true. I will add comments near that ClearRequestFlags() to clarify this.

Resizing Platform window from ImGui displayed decorations

Resizing Platform window from Platform/OS displayed decorations

I believe your log is partially incorrect (some incorrect recording or manipulation or missing details) ?

By adding a sleep in the main loop to make the refresh rate low, issue can be seen easily.

If I add a Sleep(80) or larger value e.g. Sleep(200) you can notice that the error only happens for very short while (at least on my setup), not for 200 ms. So it doesn't make it any "easier" to see the issue with Sleep(200), it's equally easy in both situation. Does it on your?

Right after UpdatePlatformWindows(), RenderPlatformWindowsXXX is called with the right render. I think the issue has to do with the fact that there's a delay in swapped contents appearing, and the timing of that swap compared to timing of window size update may be missadjusted. Your suggested workaround is probably a good idea in that context, but I don't think the issue is caused by the thing you think is causing it.

ocornut commented 10 months ago

Additional comments: I think this issue seems new to me, I don't recall seeing when initially implementing multi-viewport for the DX12 backend, and I believe it may also be due to some DX12/Windows version/SDK version/drivers subtleties which I don't quite understand now. Note that DX12 backend also have a "flicker" when dragging a viewport outside, it doesn't appear as far as with other backends, and that's perhaps related to the same underlying thing.

SuperWangKai commented 10 months ago

Hi @ocornut ,

Thank you so much for the investigation and explanation.

I have uploaded the testing code for you to check: (Edit: commit updated) https://github.com/SuperWangKai/imgui/commit/21c4f82a1ad81085ab0db10cf0e0aacbcbb27692

Here is the screenshot of the logs happened when I dragged the border of the "Hello world!" platform window when it was a standalone window out of the main viewport:

log screenshot

As you could see from the log, viewport->Size update did not happen according to the logging I already added to the following code:

if (viewport->PlatformRequestResize)
{
    viewport->Size = viewport->LastPlatformSize = g.PlatformIO.Platform_GetWindowSize(viewport);

    // Debug only:
    char log[1024];
    std::snprintf(log, sizeof(log), "Viewport size update, size=(%u, %u)\n",
        static_cast<uint32_t>(viewport->Size.x), static_cast<uint32_t>(viewport->Size.y));
    OutputDebugStringA(log);
}

If my debug is correct, PlatformRequestResize flag was already cleared by ClearRequestFlags. But by design, as you explained in the Resizing from Platform/OS decorations part, viewport->Size should be updated in the way bullet point ii explained.

To me, it seems ImGui was rendering in old size but the swapchain was in new size, causing the ghosting stretch effect.

I believe your log is partially incorrect (some incorrect recording or manipulation or missing details) ?

I commented out PlatformRequestResize flag clearing code in ClearRequestFlags and had the logs as you saw last time and the update was still one frame behind.

If I add a Sleep(80) or larger value e.g. Sleep(200) you can notice that the error only happens for very short while (at least on my setup), not for 200 ms. So it doesn't make it any "easier" to see the issue with Sleep(200), it's equally easy in both situation. Does it on your?

From my observation, Sleep(200) seems to help reproduce the issue better if I revert the swapchain scaling mode to stretch.

ocornut commented 10 months ago

when I dragged the border of the "Hello world!" platform window when it was a standalone window out of the main viewport:

The resizing borders of PLATFORM WINDOWS don't appears by default, don't appear in your first-post video, and don't appear in https://github.com/SuperWangKai/imgui/commit/21c4f82a1ad81085ab0db10cf0e0aacbcbb27692#diff-66e3146b58f2d5a61041ee73d7320664a6028a7eccc468df72166822b09ea976 (unless you modified things to set io.ConfigViewportsNoDecoration = false but then you need more logistic to do a "live" resizing) so I am not sure we are talking about the same thing.

You are exactly the situation described in my "Resize from ImGui decorations -> point 3" and your log matches that. The update of viewport->Size appears in "point 2" of that same description.


PS: You can use IMGUI_DEBUG_LOG() to simplify your debugging:

   char log[1024];
    std::snprintf(log, sizeof(log), "Swapchain update, size=(%u, %u)\n",
        static_cast<uint32_t>(size.x), static_cast<uint32_t>(size.y));
    OutputDebugStringA(log);

Becomes:

  IMGUI_DEBUG_LOG("Swapchain update, size=(%u, %u)\n", (uint32_t)size.x, (uint32_t)size.y);

Appears in console and Demo->Tools->Debug Log.

From my observation, Sleep(200) seems to help reproduce the issue better if I revert the swapchain scaling mode to stretch.

It's difficult to compare without a video as it may depends on OS/drivers/GPU and other settings.

SuperWangKai commented 10 months ago

Hi @ocornut

Thanks for the reply!

The resizing borders of PLATFORM WINDOWS don't appears by default, don't appear in your first-post video, and don't appear in https://github.com/SuperWangKai/imgui/commit/21c4f82a1ad81085ab0db10cf0e0aacbcbb27692#diff-66e3146b58f2d5a61041ee73d7320664a6028a7eccc468df72166822b09ea976 (unless you modified things to set io.ConfigViewportsNoDecoration = false but then you need more logistic to do a "live" resizing) so I am not sure we are talking about the same thing.

I'm not sure if I totally understand this but I can use my mouse to resize the platform window as you could see from the video, though the window is not in thick border style of Windows platform. ConfigViewportsNoDecoration is remained as default. I did the resizing test after the platform window was placed out of the main viewport.

The update of viewport->Size appears in "point 2" of that same description.

This is the problem I'm trying to understand. As you can see from my log, there is no Viewport size update ... log record meaning "point 2" did not happen.

PS: You can use IMGUI_DEBUG_LOG() to simplify your debugging:

Sorry missed this useful function. Thanks for the education.

ocornut commented 10 months ago

resize the platform window as you could see from the video

You are resizing from imgui displayed borders and using imgui resizing logic, not from Windows displayed borders using Windows code. They are two different paths. Windows has its own resizing logic. In your situation it is not used because the Windows borders are hidden

there is no Viewport size update ... log record meaning "point 2" did not happen.

point 2 refer to a different location in the code inside Begin(), where you didn’t add a log entry. If you search for “viewport->Size = “ you will find it.