Suboptimal window drawing on Windows
Currently GTK3 uses layered windows in W32 GDK backend, and GTK4 inherited that trait. But let's start at the beginning.
Long-long-long-long time ago there was a desire to add alpha-transparency support to W32 GDK backend for GTK3. Cairo had to be patched for that, but we succeeded. It also allowed GTK3 to draw translucent shadows around windows, and, therefore, to use CSD there.
But CSD brought to the surface a hideous bug - a desynchronization between window resize and window drawing. It wasn't anything new, this bug existed from the earliest days of GTK3 (as soon as GDK switched away from drawing stuff in response to WM_PAINT
), but CSD made it obvious and ugly (in SSD the decorations still look good, it's just the content that can't catch up; in CSD the whole thing can't catch up with window resizing, and it shows).
The main issue is that Windows WM expects windows to paint themselves in response to WM_PAINT, but GDK doesn't do that anymore, it paints on a 60 FPS idle timer instead. WM_PAINT
handler becomes almost useless because of that (it does some thing with a paint region, but i'm not sure how useful that is).
I look to and fro, and eventually came to a solution that seemed fine at the time: layered windows. It allowed us to redraw and resize a window in one atomic call, and it supported alpha-transparency trivially. Sure, it completely prevented Windows WM from drawing decorations, but we were aiming at CSD already, so it's not like we needed that, right? All windows were just switched to WS_EX_LAYERED
and WS_POPUP
, and [most of] all other styles were dropped as unneeded, and we went on our merry way, drawing everything ourselves. This also allowed us to completely ignore WM_PAINT messages.
Problems cropped up soon.
-
Layered windows don't work with OpenGL, since you must submit the data from RAM and can't use GPU memory (not unsurprising - this API dates back to Windows 2000). The solution was to not use them for windows that have OpenGL rendering (which brought back the desync bug, but there was no way around that). There's a possible solution where you render with OpenGL and then read the pixels back from GPU memory and then submit them to Windows WM via
UpdateLayeredWindow()
. Obviously, this is slow, so we didn't do that. -
Windows WM has special magic for drawing shadows around windows. In Windows 7 that was achieved by the WM maintaining a non-input decorative window that stayed behind your own window in Z-order, and drawing the shadow on that. Since it was internal to the WM, the WM could maintain its position and size in sync with the main window, so it all looked nice. In Windows 10 that was changed - now shadow border is part of the non-client decoration. In fact, resize areas are located there (h-m-m, i wonder, where did MS steal that trick from?), although, unlike GTK, the top edge didn't have much of a shadow, and the resize area there was part of the window titlebar. Anyway, the problem with all that is that this special magic shadow is completely unavailable for us. In Windows 7 we were unable to keep a separate shadow window in sync with the main window, and in Windows 10 it's impossible to override the WM on this (more on that later). So we were left with drawing the shadow just as normal part of the window. Obviously, the WM had no idea that some part of our windows was purely decorative, and there was no way to communicate that information to it. Because of that doing things like resizing the window to cover the whole screen in one direction, or snapping it to the right, looked wrong (made especially bad due to the fact that GTK3 Adwaita shadows are huge, much bigger than they look).
-
Lack of window styles caused windows to lose AeroSnap support almost completely. My solution for that was to re-implement the needed parts of AeroSnap in GDK. This worked well enough, but in Windows 10 the WM learned some new AeroSnap-related tricks which i couldn't possibly reproduce (specifically -
winkey + left/right
prompts you to put another window alongside the one you snapped to the left/right half of the screen). Though this custom AeroSnap code kind of hid the problem mentioned in point 2 - because snapping was handled by GDK, it was possible to account for shadow size in this, and snap windows around correctly.
And here's the kicker: the devs assured me that GTK4 is going to switch to all-OpenGL drawing evetually, which means that layered windows won't be usable anymore.
That got me to think about this issue again. I prototyped some solutions, and found one that should have been obvious, but wasn't at the time. It came from my work on DnD in GTK3 and GTK4. The W32 GDK backend in GTK3 used to do (and still does) stuff like this:
- Get a callback from the OS due to some input
- Queue an event that reflected this input in GDK terms
- Keep spinning the main loop (thus making the whole thing recursive) until the GTK layer reacts to that input
- Return from the callback with the result obtained from GTK
And that got me thinking - why can't we use this to repaint windows in response to WM_PAINT, like the OS expects us to:
- Get
WM_PAINT
- Call
BeginPaint()
- Connect to an after-paint callback of the frameclock
- Ask frameclock for a PAINT phase
- Spin the main loop until the callback is invoked
- Call
EndPaint()
Obviously, this can potentially waste CPU when redraws are fast, but unless Windows sends us WM_PAINT
faster than 60 times per second, this isn't really much of a problem. Message processing would kind of stall on slow redraws, but that is kind of the case already (it just stalls in a different place - GTK isn't multithreaded after all).
Even better, we could implement a new frameclock based on WM_PAINT
: instead of waiting for one from the OS, we could just send our own (by calling the RedrawWindow()
API, which causes the OS to send WM_PAINT
to us). That eliminates the steps 3 to 5 above (no need to mess with the frameclock and spin anything when you are the frameclock and can just emit the paint event yourself; emitting (vs queuing) means that there's no need to spin the loop, as the event is handled right there).
Another thing to note here is window styles. My testing suggests that AeroSnap depends on the following styles: WM_SIZEBOX
(can't resize a window without it), WM_MINIMIZEBOX
and WM_MAXIMIZEBOX
(can't snap up/down a window that can't be maximized/minimized). These were removed for layered windows, because the OS doesn't draw anything for us, so why bother? That was a mistake. We can totally keep any styles we want - even if the OS doesn't draw things for use based on these styles, WM still processes inputs for us and does AeroSnap, if the styles are right.
Therefore, my proposal is the following:
- A new W32-specific frameclock that sends WM_PAINT, and the main frameclock procedure (where it goes through all requested paint phases and emits signals) will be its WM_PAINT handler (though there may be some design quirks there - what if the PAINT phase is not requested? Do we still make the OS send us a
WM_PAINT
, even if we don't draw anything?). - Removal of all layered windows, custom resize and custom AeroSnap code.
- Give appropriate styles to GDK windows to make native AeroSnap work. We might need add a special GTK-only GDK api that will allow GTK to tell GDK that a particular window is not resizable, which means removal of the styles from it (otherwise window would be resizable at GDK level but not on GTK level, leading to bad results).
Some caveats:
- SSD won't be coming back. The minute i allow
WM_NCPAINT
to be handled byDefWindowProc()
, the alpha channel for the window immediately goes to hell (this might not have been the case for Windows 7, when we first got alpha-transparency in W32 GDK, but is so for Windows 10). And since lettingWM_NCPAINT
be handled byDefWindowProc()
is the only way to render SSD, that means that we can have either SSD or alpha-transparency, but not both. And believe me, i've tried. You do anything to the arguments toDefWindowProc()
(like altering the region to exclude window client area from it), and it breaks down and switches to the old Windows-7-with-no-composition style of drawing, without shadows. Googling suggests that this is due to the fact that SSDs are drawn by WM on GPU or something, which precludes us from interfering in the process in any way. I don't know. It might be possible to tiptoe around that by using DirectX for drawing (there's a special "WM, just forget about us and let us draw stuff directly on GPU" style, which only works with DirectX, as far as i've been able to google up), but GDK can't use that in place of OpenGL, so that's a non-starter. Trying to "reimplement" OpenGL in DirectX (i think Firefox does that, for WebGL?) is completely outside of my abilities (and might not be feasible, depending on which features OpenGL-in-GTK needs). - Because we can't use SSD for window shadows, and because drawing shadows on our own brings about a lot of issues (which are not solved by custom AeroSnap code anymore), we have to lose the shadows. This is easy to do with a small patch to GTK. This activates the .solid-csd theme variant, which has no shadows (resize areas become solid parts of the window; looks like a cross between Windows XP and Windows 7 (only without shadows)).
Here's how GTK CSD looks like right now (note that this is the real window size as reported by GetWindowRect()
; have i mentioned that shadows are huge, much bigger than they look?)
Here's how it would look in .solid-csd variant:
If accepted, this should definitely go into GTK4. I also see no reason for it to not to go into GTK3 (most changes are internal, and full AeroSnap support is worth an unexpected change in how window decorations look).