MAGE-UI Dev Log #3
Update #3 for the blog. I was noticing a lot of jank when resizing the window at point of my last update, and decided it was something I should definitely take care of before continuing. I didn’t want to go further if this is some larger issue with the app structure.
Here’s a short video showcasing methods I’ve tried and the result at the end:
At first, after playing around with some small tests with D3D11. I was convinced for a while it was with OpenGL or GLFW where the issue lied. The small D3D11 tests were much smoother during resizing than what I currently had.
So I worked to switch out the backend rendering to D3D11 and remove GLFW for the native Win32 API. This was a large amount of work, and now I will have to support various backends for different platforms. But at least it should be smooth now, and it was a good chance to further modularize my graphics-related code away from the windowing code.
Well, turns out after all was said and done. The D3D11 approach was still janky, a lot smoother, but not something I was comfortable with. Unfortunately, even with a similar app structure the smaller tests didn’t hint at this, it was only after the bigger effort and re-adding UI code. So what could be wrong then, was it the UI code, something else?
I did take some time reviewing the UI code but nothing looked obvious there. Something that had been bugging me was the idea that maybe the separation of the main event loop and the rendering threads for each window would always cause jank because viewport/swapchain resize updates are not 1:1 with the resize event. I wasn’t fully convinced on that because it’s still 60fps and doing some debugging it was very close to being 1:1. Either way it was definitely something else to check out.
Weirdly, updating the full app with D3D11 to be single-threaded I was still getting some jank! But then moving the old OpenGL+GLFW code to a single thread, finally I saw what I wanted to see, buttery smooth rendering while resizing. At least this proved it’s gotta be possible, I just need to structure this right.
The conclusion is basically that if I want separate render threads per window, it needs to include the window creation and event loop; so thread per window fully, not thread per window render context. That way the viewport/swapchain update can be done directly when receiving the resize event.
(However! …)
There are still some important questions I need to look into on whether this is actually fine. Is Windows really OK with windows being created on separate threads or is it just working by dumb luck? Are the events actually received separately per window thread? Is it a problem creating windows on separate threads with GLFW?
While I could just stick everything on one thread and be done with it, I feel like not getting this core issue right first could be more difficult to fix later on. If I do end up with everything on one thread (I can still confidently split out other threads like simulation and UI later), I would be curious to figure out how to share the context between windows instead of changing between contexts every frame.
Edit (09/12/2024):
Having the window event loop run on the main thread with a separate render thread the way I was previously doing it is AFAIK standard practice. That way, it can still render while encountering blocking DispatchMessage() calls like WM_NCMBUTTONDOWN
, which block until the mouse is released (e.g. operations like resizing while holding the mouse in place). After browsing some open source repos again trying to figure out what other people are doing, I stumbled on an approach using PostThreadMessage()
to post the WM_SIZE
to the render thread so it can handle it itself, rather than setting a separate custom flag. This seemed like it could do the trick and feels more like the right way. Their example app seemed to not have much jitter, but it’s hard to tell because it only renders text glyphs and there’s not a lot of UI. However, after trying that out it’s basically the same jank as before (i.e. compared to where I had a main event loop and separate render thread and set a flag from the main WndProc handler to get picked up by the render thread).
Although the single thread per window (event+render) is still the smoothest (with OpenGL only, idk why D3D11 is still janky in that case), it comes with the tradeoff that the rendering stops for some blocking events. I figured out a possible solution with resizing where I handle the WM_NCMBUTTONDOWN myself which saves that from blocking, but I haven’t fully implemented that idea yet.
I also found some docs from MS claiming that creating windows on their own threads should be fine: https://learn.microsoft.com/en-us/windows/win32/procthread/creating-windows-in-threads . So that answers that question.
Edit 2 (09/13/2024):
This issue seems to be difficult and generally unresolvable in any perfect way. This article is a depressing read:
https://stackoverflow.com/questions/53000291/how-to-smooth-ugly-jitter-flicker-jumping-when-resizing-windows-especially-drag/53000292
Also turns out, on Linux (tested on Fedora) the GLFW+OpenGL version seems to be quite laggy on resize which seems to track back to some issue with NVIDIA drivers. https://github.com/glfw/glfw/issues/1016 .
Anyway, apparently getting windows to resize smoothly is a problem made difficult that may not have much to do about. My last solution seems to do the trick, on my machine at least. For now I will definitely be moving forward with other fun features, that’s enough time spent on this issue…