Demo.mp4
Try resizing a Chrome window quickly. Or Steam. Any major app. They get this wrong.
In Windows (and most operating systems), moving or resizing a window will block the main thread of that program. This is because the OS will enter a "modal loop" in which it absolutely does not want you to have any control..
If you use CS_HREDRAW | CS_VREDRAW
in your window class's style property, Windows will send you a WM_PAINT message when your contents are no longer valid, and drawing when you receive this message is the official way to do smooth resizing in Windows.
That's good! We can render when Windows tells us we need to render! Though one issue remains: No 60+ fps. Windows will only send us that message when the window's w/h actually change or the window is moved back on-screen by 1+ pixel from being previously off-screen.
Here I've solved this by rendering in a background thread. This enables two things:
- Full fps during window resizes/moves.
- The main thread can now be solely dedicated to receiving OS messages. The main benefit of this is the ability to receive key presses and such mid-frame. You could use this to start calculations or work related to user input for the next frame while the previous frame is still being drawn. This is the same solution web browsers use to keep playing a YouTube video while you stretch their windows. Only, web browsers tend to display artifacts during resizes. Check for yourself. Steam is also a program that is rather bad with these artifacts.
Those browsers (and Steam) fail to smoothly resize because they don't wait for the new frame to actually finish drawing before returning from WM_PAINT. In other words, Windows is being given the go-ahead to actually resize the window and display it in its new size before we've actually drawn a frame at the new size. In this example, we've circumvented that problem by simply putting the main thread to sleep, in the middle of WM_PAINT, until the render thread reports back that it drew our requested frame. If you do this before calling EndPaint() and returning from WM_PAINT, you get smooth resizes. Thank you, Patrik Smělý, for telling me about this approach. (He got this working on macOS first.)
- If we're not animating, we don't render! 0% CPU/GPU in that case. Laptop users love us.
- When we need to, we can wake the render thread and have it either draw a single frame when something changed or to render continuously for an animation. For example, WM_PAINT wakes it up to draw a single frame.
- If it was already rendering, not a problem. It will continue. And we can wait in WM_PAINT for the one we requested to be done.
- When we need to, we can wake the render thread and have it either draw a single frame when something changed or to render continuously for an animation. For example, WM_PAINT wakes it up to draw a single frame.
- We're using Simp (OpenGL) to render in this example. But you can do this with DirectX/Vulkan/whatever as well.
- We're not using modules/Input or modules/Window_Creation. This is a minimal example and those modules are huge. You could adapt them to do this, but they're not really designed right now to support it.
- We're syncing data between threads safely with a single critical section (a mutex). And one thread can sleep with a single condition variable to later be woken up by the other.
- The key to making inter-thread communications fast is to do them as little as possible. We only enter the critical section once in the render loop.
- If you always want to render at full fps, remove the "go to sleep" line in
render.jai
and remove the "Wake up the render thread" lines inwindow.jai
.
- If you always want to render at full fps, remove the "go to sleep" line in
- The key to making inter-thread communications fast is to do them as little as possible. We only enter the critical section once in the render loop.
jai main.jai
Beta access to the Jai compiler is required.
If you do not have access to Jai:
- The code is thankfully mostly just Windows API calls, and Jai is very C-like. You should be able to understand it.
- An executable is available in Releases to play with.
If anyone wants to create a version of this for a more available language like C/C++, send me a DM or tweet and I could link that here.
The license of this repository is the unlicense, so it's effectively public domain. No credit needed. Go nuts. Glad I could help.
If you're in the beta Discord for Jai, ping me @CookedNick
. Otherwise, I'm on Twitter. Same handle.