WM-synchronized redraws for GTK3 on Windows

I know that everyone is excited about GTK4, but i’d like to leave GTK3 in a somewhat better and less conflicted shape. I repeatedly promised to do so, and this topic is part of that effort, since i can’t seem to figure out how to solve a particular problem.

Short recap:

  • drop layered windows in GTK3
  • either drop client-side decorations OR switch to shadow-less client-side decorations (i.e. solid-csd)
  • use cairo to draw directly on DC

This likely means losing selective alpha-transparency (or not; it mostly depends on what Windows WM does; nothing we can do). But one thing i’d like to keep is the synchronized window resizing. I.e. when you drag the edge of a window to change its size, window contents (as drawn by GTK) and window itself (as seen by WM) are in sync, and there’s no situations where window size increased, but GTK failed to catch up with its drawing (leaving a gap between WM frame and window content) or where window size decreased, but GTK failed to catch up again (causing WM frame to cut off a portion of existing window contents).

There are two ways to achieve that. The first one is to integrate with advanced WM API, but that requires COM, Windows 8.1-or-later, and necessitates drawing with DirectX (DC emulation on top of DirectX is just slow, though it might cut it as a last resort or something). That’s something that GTK4 will, hopefully, do. The second way is to repaint on WM_PAINT. More specifically: Whenever GTK receives WM_PAINT, it should immediately (and without returning from WndProc) repaint the contents of the window, using the DC given by a call to BeginPaint() [1]. This is different from how GTK3 currently does things. Right now:

  • GTK3 (when not using layered windows) paints on a DC it gets off the window with GetDC, not BeginPaint
  • GTK3 paints at its own time, mostly ignoring WM_PAINT (it takes some area info from it, i think, and a suggestion to repaint, but doesn’t really do anything, returning from the message processor function, and only repaints later, when the paint clock ticks)

This is a major shift in the way windows are drawn. I’ve managed to hack things together with some modest success, but that hack is not something that can be merged into GTK. Basically, it’s a bastardized version of the idle paint clock that issues RedrawWindow calls (which can be made to generate WM_PAINT internally) during the paint phase, instead of actually causing repaints. Meanwhile WM_PAINT handler can tell whether it’s being called by a paint clock via RedrawWindow (in which case it grabs some data that the paint clock leaks into GDK and does what the actual paint phase was supposed to do - i.e. asks GTK to redrawn the window), or whether it just received a WM_PAINT from the OS normally (in which case it queues a paint phase on the clock, and then manually cranks a new inner event loop, until the paint clock finally does its stuff, comes to the paint phase, and then see above). This is convoluted and breaks all manners of abstraction levels between paint clock and GDK.

Suggestions?

[1] There’s an extra condition here - WM frame must be drawn; if the window has its frame removed completely, WM stops syncing its paints - probably because it doesn’t paint the frame anymore itself; this is solved by leaving a 1-pixel-thick frame around the window; works well enough with solid-csd (yet another reason to stop using normal CSD with shadows)

2 Likes

Hi lrn,

I understand that it’s a difficult topic. Right now I can see that resizing GTK applications that have native win32 decorations is very smooth. Though we don’t draw in response to WM_PAINT, it seems that GTK is able to catch up quite nicely anyway. Good job!

Regarding custom decorations, I see that we’re using layered windows. That means that instead of drawing on WM_PAINT we are able to push a window bitmap to the OS whenever we want, using UpdateLayeredWindow. Also, for window move / resize we don’t rely on the OS modal loop, but emulate it ourselves, so we can actually resize whenever a new window bitmap is ready. That works very nicely! (is it more or less what happens on Linux?)

Layered windows have some drawbacks:

  • Layered windows are not very fast, but when using software rendering it’s OK (the bitmap has to stay on system memory for mouse input hit-testing purposes).
  • We have to use the custom move resize handling in order to get smooth resizing. That prevents us from getting native AeroSnap on resize to edge of the monitor work-area (not a very big deal)

From Windows 8 onward, we can use WS_EX_NOREDIRECTIONBITMAP windows, which enables us to have transparency like layered windows (which is required to draw shadows), but:

  • Are performant (all the drawing and the final composition can happen on the GPU, without upload to system memory).
  • There’s no need for custom move resize handling because we can resize smoothly.

Wait, what? Which version of GTK are you talking about?

Some people do use OpenGL, for example, with GTK, thus they are not “using software rendering”. This is a legitimate use-case and should be supported fully. Besides, “not very fast” stops being a minor issue at 4k resolutions.

Actually, it is a big deal for some people. I get complaints all the time. And custom AeroSnap emulation relies on keyboard hooking that is problematic by itself.

Client-drawn shadows are a dead end, currently. MS does not provide any API for us to tell the WM that this part of the window is a decoration. Thus, PrintScreen will capture the whole window (with shadows and anything under them), AeroSnap tiling will treat shadows as part of the window content, etc. Hence my desire to move to solid-csd, which has no shadows or rounded corners.

1 Like

You can achieve shadow-less CSD using a tiny CSS snippet.

See https://gitlab.gnome.org/GNOME/mutter/-/blob/7b45de941b56b506e4b676cf05b5ba24d9524247/src/tests/test-client.c#L883 for an example.

I have tested gtk3-demo-application, but err…maybe it’s too simple? :wink: anyway, it resizes flawlessly!

I agree with everything you say. I understand that supporting custom decorations on Windows is a pain. But just for the sake of discussion, I also have some ideas to work around the problems. We could:

  • Drop custom snap in favor of native AeroSnap (see below).
  • Use DwmUpdateThumbnailProperties to cut the shadows out from the window thumbnail. The thumbnail is used for the picture in the ALT+TAB dialog and the taskbar.
  • IMO, the ALT+PRINT window screenshot is not a big deal…

Then comes the big question: How to support native AeroSnap when using custom shadows?

Well, we cannot tell the OS about the size of our shadows. But we may just be smart here. We can detect if a resize / move is initiated programmatically or by user interaction (see this link). If we are moved programmatically to a side of a monitor’s workarea, we’re being snapped, and we can hide shadows at sides. What do you think about that?

I say these things just to start a discussion, give insights and hear opinions, I’m totally ok with forcing .solid-csd, of course! I also understand that implementing the smart snap feature is probably complicated…

Thanks!

We can’t. Also (not mentioned in that answer, but easy to see for yourself), it’s not “half the screen” anymore. In Windows 10 you can dock at the half of the screen, then resize horizontally to cover “not half of the screen”, then dock another window in the remaining space, and the remaining space will be filled completely (both windows not covering “half of the screen”).

TBH, i think the discussion is going in the wrong direction. I’m not asking whether solid-csd is a good idea or not. I feel that it’s a necessary measure. Extra points for it being built into GTK. What is a problem is organizing the redrawing process (an intersection of Windows WM behaviour and GDK paint frame clock). I kind of hoped for more comments from core gtk developers (people with in-depth knowledge of the frame clock and the painting cycle in GDK/GTK) and less of…whatever is that we’re having right now.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.