CFrame paint pessimisation [Windows]

When there’s a patch change in my plug, most controls are updated. Their rects are collected and draw()'s are called.

Update rectangles overlap; in a major update 2/3 of rect updates overlap with earlier rects.
Many controls are thus repainted multiple times. (I noted this due to semitransparent text, which gets brighter with each redraw.)
-----EDIT much later:
The above was my misunderstanding. Controls dont overlap, the update rects don’t overlap, but update rects hit the same CViewContainers multiple times.

The update code seems terribly O(N^2+), because every update causes a wide search for all controls that might be touched by the update rect.


WHAT HAPPENS

(i) We call setDirty to mark controls for update.
(ii) Send their rects to Windows.
(iii) Get back rects from Windows (sorted by Y and X, hacked into more thinner wider rects).
(iv) Then search for controls overlapping the update rects and repaint these.

In detail,
(i) Dirty controls are collected by CFrame::CollectInvalidRects.
(ii) and sent to WinAPI InvalidRect() function.

(iii) When WM_PAINT fires, win32Frame::paint asks to get them back using GetRegionData().
(There are usually more rects than we sent in. I see a bunch of 1-pixel lines split off from their controls.
This must be due to the slice-and-sort behavior. Details in github wine/dlls/gdi32/region.c.
Not sure if this is the best behavior for controls.)
-----EDIT this above is the real problem -----

(iv) Here’s the O(N^2) bit: For each rect that we get from the Windows collection, CFrame::platformDrawRect calls CViewContainer::drawRect, which loops over all CFrame children and (because they are containers) most of their subchildren, to find the ones to redraw.


In a typical update of my 35 main controls (=> 60 rects), there are 120+ loops over 10-40 controls, in all 2500+ CView object accesses.
40/60 drawRect updates are fully overlapped by previous rects, so 2/3 of the loops are unnecessary.

This may be my own fault. Maybe we’re not supposed to use CViewContainers lightly?
I have them as group boxes just to draw a nice frame.


WHAT TO DO ABOUT IT
Unless this is known behaviour solved by using the library correctly, as anybody with half a pixel for brains would,

(1) Skip the overlapping rects.
There are still multiple redraw of some controls. But it reduces accesses by 2/3, so it’s a start.

(2) In win32frame::paint: instead of calling platformDrawRect for each rect, merge/enclose them, then make a single call to platformDrawRect.
I’ve tried this, it seems to work but needs more testing. It’s a 5-line change in just one place.
(2b) The same could be done in CFrame::CollectInvalidRects::addRect (), but that’s more complex, haven’t worked it out yet.

The downside of (2) is very crude paint behavour: There’s a full-frame update when two subcontrols in different corners of the plug need repaint.

(3) Another option is to stash dirty children in a std::set and roll that in win32Frame::pain() instead of searching, then CFrame just uses the rects for sort order. (Haven’t thought through the details of this one. Some might want multiple calls with sub-rects for their controls…)
The upside here is it could end up being forgiving of sloppy programmers such as myself, since even if a child is marked dirty multiple times it would only be repainted once.

Here’s a link to instrumented win32frame.cpp and cviewcontainer.cpp to demo the problems (or not) in your code.
(though I’ll remove these before you can say “legal consequences”, so they won’t be there in a few days)

I’ve wrapped changes in the existing macro VSTGUI_LOG_COLLECT_INVALID_RECTS, so look for that
https://www.abc.se/~re/Stuff/vstgui/win32frame-rect-debug.cpp
https://www.abc.se/~re/Stuff/vstgui/cviewcontainer-rect-debug.cpp


Any thoughts?

To follow up after some more testing,
My local fix is simple & crude: Use the BeginPaint bounding rect to redraw and completely skip the GetRegionData rects.
This way there is only one search for controls to draw and no double-painting, but still some uncalled-for repaints.

Only 2 line changes.
In win32paint.cpp / Win32Frame::paint() function:

        if (deviceContext)
        {
            deviceContext->setClipRect (updateRect);
++            getFrame()->platformDrawRect(drawContext, updateRect);

--            DWORD len = GetRegionData(rgn, 0, NULL);
++            DWORD len = 0; //GetRegionData(rgn, 0, NULL);  // Skip the region rects Mikado stack

The last line and the following block should ofc be deleted if this stands.

Had to chase around for GetRegionData info.
Feng Yuan “Windows Graphics Programming” (2000), ch 9.6 has a detailed (and convoluted) description of memory layout and warns about memory costs. He suggests using it for bitmap masking and color keying for transparency. Fairly advanced old-school stuff.
(PS now I get it: Remember round windows anyone? A bit cheesy, never quite worked? Those.
There’s a function to get the exclusion region then you can blit from your bitmap and background.)

Wine description of Regions is easier to follow;
Region functions create the widest possible bands from its rects, and returns them strictly sorted by Y first, then X.
See: wine/region.c at d265dd88cd93ce6ffe56c9cfd640b064d7c14e29 · wine-mirror/wine · GitHub

So in my case there’s 1- and 2-pixel bands because some controls have a border, then there’s a same-size but borderless control way off to the left or right.
The control sticking up/down is sliced into 2-3 bands and the full search+paint is applied 2-3 times per call.
This will vary depending on app layouts.

Anyway,
Feeding the dirty rects into InvalidateRect, to tell Windows the area that will be updated, is the right and simple thing to do.
Windows also assumes paint will cover entire smallest rect covering all parts (it’s what we get back in BeginPaint struct).
But while it would be useful to get the original rects back (or even better, the controls themselves), the Region logic has the wrong behavior for the task.

Cheers,
/rasmus

Hi rasmus,
as you have seen, VSTGUI is not optimized for redrawing overlapped controls, so avoid it. Your naiv solution may works for your simple case, but if you have continuous updating regions in the upper left and lower right you’re redrawing the whole area which is slow on some platforms.

Cheers,
Arne

This is not about overlapping controls, it is about GetRegionData:

GRD can return up to 4 rects from 2 controls, this means 4 searches for matching controls and 3 draw calls to control A.
More non-aligned controls means even more rects.

This is Windows only.
Anyway, yes, it’s crude as noted, but less overshoot than current impl.

Proper solution could be that Win32Frame stores the dirty pointers in a setCView::Impl* at time of CFrame::CollectInvalidRects, and consumes them in Win32Frame::paint(). I haven’t implemented that yet, since it’s a big rework.

-----EDIT I saw now where you got the overlapping idea from, the first para in my OP :slight_smile:
I shouldn’t be snide about any of this, VSTGUI is generally in a good place, this all is just a technical detail.
Also I write too long&convoluted.

Regards,
/rasmus

OK, now I understand and I made a test. You can expect this to be fixed in the next version.

Thanks,
Arne

The ‘fix-up rectangles split’ commit applied on Oct28 2018 seems to be causing some drawing problems.

I’ve not had time to reduce this to a simple example. It appears that if there is a VU meter (or other indicator) updating off screen (due to the window position straddling the edge of the screen) then the onscreen area can fail to draw resulting in a blank area on screen.

Reverting back to drawing the raw updateRegionList solves the problem.

Cheers,
Mike