Jump to content
Vincent Parrett

VCL Handling of dpi changes - poor performance

Recommended Posts

Is it just my delphi applications that behave poorly when handling dpi changes? This is when setting high dpi in the manifest to PerMonitorV2.  I have verified that the controls (many of them mine) are handling this as they should (override ChangeScale). 

 

When dragging the application between monitors with different dpi's, it takes 3-4 seconds while the window flickers and repaints multiple times - the dragging operation pauses while it does this, and then the window eventually jumps to where you actually dragged it.

 

I've been looking at other applications (that ssupport PerMonitorV2) to see how they behave, even explorer stutters a little, due I guess to the ribbon control - but the stutter is around the 200ms mark. Thunderbird seem to repaint twice but very fast. 

 

After some debugging, as far as I can tell, this is caused by all controls  getting their ChangeScale method called (as you would expect) which results in calls to SetBounds, which invalidates the control causing more painting!  

 

TWinControl.ScaleControlsForDpi appears to be doing the right thing (control alignment is a perf hog), but calling EnableAlign inevitably invalidates the control, again. 

 

procedure TWinControl.ScaleControlsForDpi(NewPPI: Integer);
var
  I: Integer;
begin
  DisableAlign;
  try
    for I := 0 to ControlCount - 1 do
      Controls[I].ScaleForPPI(NewPPI);
  finally
    EnableAlign;
  end;
end;

 

This really show up an inherent design flaw in the vcl, there is no BeginUpdate/EndUpdate design pattern in the vcl that allows a control (or form) to disable child controls painting until it's done. Many controls implement this pattern individually, but that doesn't help in this scenario.  

 

The situation isn't helped by my using Vcl Themes either - resize (setbounds) causes serious flicker in some controls and I'm sure this is coming into play here too.  

 

I tried to fudge a BeginUpdate/EndUpdate with this :

procedure TMainForm.WMDpiChanged(var Message: TWMDpi);
begin
  SendMessage(Self.Handle, WM_SETREDRAW, NativeUInt(False), 0);
  try
    inherited;
  finally
    SendMessage(Self.Handle, WM_SETREDRAW, NativeUInt(true), 0);
    RedrawWindow(Self.Handle, nil, 0, RDW_INVALIDATE or RDW_UPDATENOW or RDW_ALLCHILDREN);
  end;
end;

It cut's out the visible repainting, but doesn't speed things up much 

 

If anyone has any ideas on how to tackle this I'm all ears.  

Share this post


Link to post
21 minutes ago, pyscripter said:

For start you can use the fix here https://quality.embarcadero.com/browse/RSP-30931

I already had that in place, but in this case enabling/disabling that didn't seem to make much difference. 

 

23 minutes ago, pyscripter said:

 

LOL! I had forgotten about that thread (which I posted in), and it didn't come up when I was searching. 

 

FWIW, LockWindowUpdate did work for me in the WMDpiChanged handler - but setredraw seemed slightly safer. 

 

I've been trying to profile what is happening but the lack of a decent profiler is killing me! I've been using the sampling profiler, and it does show AlignControls showing up a lot (which is a known performance issue), need to investigate further. 

 

Share this post


Link to post

I used the LockWindowUpdate(Handle) on BeforeMonitorDPIChanged and LockWindowUpdate(0) on AfterMonitorDPIChanged  +  set  ParentFont := True in child controls and it helped me with this issue.  (A gigantic delay was reduced to a huge delay.)

 

I haven't checked out this repo in a while - does it help 10.4.2 at all?  https://github.com/rruz/vcl-styles-utils

 

Now that IDE Fix Pack is mostly integrated, I hope VCL Styles Utils would be another repo that was really nice to have to fill in gaps, but is no longer needed. 

 

Share this post


Link to post

I spent some more time looking at this and I can only describe it as a clusterf*ck. 

 

The issue is that as the application crosses over monitors, the form receives a WM_SIZE message, which calls AlignControls (which on a complex form or container control can be very slow). Cue mega rearranging of controls and repainting.

 

Then the application received WM_DPICHANGED, which calls  SetBounds, ChangeScale (which calls AlignControls) and more arranging and repainting occurs.

 

Then add VCL Styles into the mix - for some reason with VCL Styles that WM_SIZE get's sent twice, and then the WM_DPICHANGED is sent. 

 

I have no idea why it's sent twice, but I doubt windows is doing it. I'm not even sure windows is sending the WM_SIZE at all (still investigating this).  

 

Anyway, the upshot of this is the more controls you have on a form the worse this issue will be. 

 

Share this post


Link to post

Ok, so I can definitively say this is a design flaw in the VCL - we are not doing anything wrong. 

 

The bug is in TCustomForm.ScaleForPPIRect

 

the call to  ScaleControlsForDpi(NewPPI) causes AlignControls to be called on the child controls and the form (depth first) - which is fine, and expected.

 

My debug output looks like this :

Debug Output: TMyPanel.AlignControls - dpi : 144 Process HighDPIDragTest.exe (3164)
Debug Output: TForm3.AlignControls - dpi : 144 Process HighDPIDragTest.exe (3164)

Then a few lines further down in TCustomForm.ScaleForPPIRect, it calls SetBounds and all hell breaks loose!

 

Debug Output: TForm3.WMSize- before inherited - dpi : 144 Process HighDPIDragTest.exe (3164)
Debug Output: TForm3.AlignControls - dpi : 144 Process HighDPIDragTest.exe (3164)
Debug Output: TForm3.WMSize - after inherited Process HighDPIDragTest.exe (3164)

Debug Output: TForm3.WMSize- before inherited - dpi : 144 Process HighDPIDragTest.exe (3164)
Debug Output: TForm3.AlignControls - dpi : 144 Process HighDPIDragTest.exe (3164)
Debug Output: TMyPanel.AlignControls - dpi : 144 Process HighDPIDragTest.exe (3164)
Debug Output: TForm3.WMSize - after inherited Process HighDPIDragTest.exe (3164)

Then it calls Realign

 

Debug Output: TForm3.AlignControls - dpi : 144 Process HighDPIDragTest.exe (55256)

So the form's AlignControls method is called 3 times, and on the panel 3 times! It should be noted that the number of times for each control depends on it or other child controls anchor or align settings (see Vcl.Controls AlignWork function),

 

It's no wonder that things look so bad, it's because they are. 

 

Testing in 10.4.2 with VCL Styles enabled - but the code looks the same for 10.3 so this is not new. 

 

Edited by Vincent Parrett
typo

Share this post


Link to post

One last one before I give up for the day.. I added a few more outputdebugstring calls. 

Debug Output: TForm3.WMDpiChanged - dpi : 144 Process HighDPIDragTest.exe (52784)
Debug Output: TForm3.Paint Process HighDPIDragTest.exe (52784)
Debug Output: TMyPanel.AlignControls - dpi : 144 Process HighDPIDragTest.exe (52784)
Debug Output: TForm3.AlignControls - dpi : 144 Process HighDPIDragTest.exe (52784)
Debug Output: TForm3.Paint Process HighDPIDragTest.exe (52784)
Debug Output: TMyPanel.AlignControls - dpi : 144 Process HighDPIDragTest.exe (52784)
Debug Output: TForm3.WMSize- before inherited - dpi : 144 Process HighDPIDragTest.exe (52784)
Debug Output: TForm3.AlignControls - dpi : 144 Process HighDPIDragTest.exe (52784)
Debug Output: TForm3.WMSize - after inherited Process HighDPIDragTest.exe (52784)
Debug Output: TForm3.WMSize- before inherited - dpi : 144 Process HighDPIDragTest.exe (52784)
Debug Output: TForm3.AlignControls - dpi : 144 Process HighDPIDragTest.exe (52784)
Debug Output: TMyPanel.AlignControls - dpi : 144 Process HighDPIDragTest.exe (52784)
Debug Output: TForm3.WMSize - after inherited Process HighDPIDragTest.exe (52784)
Debug Output: TForm3.AlignControls - dpi : 144 Process HighDPIDragTest.exe (52784)
Debug Output: TForm3.Paint Process HighDPIDragTest.exe (52784)
Debug Output: TForm3.Paint Process HighDPIDragTest.exe (52784)
Debug Output: TForm3.Paint Process HighDPIDragTest.exe (52784)
Debug Output: TMyPanel.Paint Process HighDPIDragTest.exe (52784)

All that painting doesn't make it look any prettier 🙄 

This doesn't look to be something that can be fixed with a patch or detour etc, it's going to take a rewrite of swathes of the vcl code :classic_unsure:

Share this post


Link to post

@Vincent Parrett Could you check if this superfluous invalidate in Vcl.Controls has an impact on it? I forgot the details on it.

 

if Message.Msg = WM_UPDATEUISTATE then
    Invalidate; // Ensure control is repainted

 

 

Share this post


Link to post
33 minutes ago, Attila Kovacs said:

Could you check if this superfluous invalidate in Vcl.Controls has an impact on it? I forgot the details on it.

In my demo on a form full of different controls this line is not executed at all.

  • Thanks 1

Share this post


Link to post

We used a workaround in our application to make a smooth transition from one monitor to another.

I think the problem is not so much as refresh time but the fact that refresh kicks in when crossing monitor boundary.

As a result user cannot continue moving the form (for refresh period) and this does not feel right.

For smooth transition we detect the start of crossing using WM_ENTERSIZEMOVE and WM_MOVING.

At that moment we ensure that the form does not update (e.g. WM_SETREDRAW) .

Once movement is finished (WM_EXITSIZEMOVE) we let the form update.

 

Still a lot of code gets executed even without refresh, so an additional trick was helpful.

In our case the content of the MainForm was on an embedded form, so on crossing we set embedded form invisible and without parent.

In that case it was not scaled so we avoided this on crossing.

After crossing was finished we reinstated content visibility and the parent, which finished the scaling.

 

To improve visual outlook we captured content onto scaled TImage and put it on top of the MainForm.

As a result only TImage gets quickly scaled on crossing and provides (possibly blurred) visual feedback until content finishes scaling and

is ready to show.

Edited by everybyte
  • Like 3
  • Thanks 3

Share this post


Link to post

My point is that from all things, happening during rescaling of a form, the most resource consuming are SetWindowPos calls, which are called from SetBounds, which are themselfs called from different places, including AlignControls.

Drawing and especially async invalidation takes much less resources and time, imho.

 

So, when we speak about form rescaling performance, we can denote it as O(n), where n - is mostly the number of SetWindowPos calls. To trace how many times SetWindowPos is actually called we can use WM_WINDOWPOSCHANGED event handlers on child controls.

 

So, given very simple example with a single TPanel control, aligned with alClient on a form, I see three SetWindowPos on each dpi boundary cross:

 

procedure TPanel.WMWindowPosChanged(var M: TWMWindowPosChanged);
var
  cr: TRect;
begin
  if (M.WindowPos.flags and SWP_NOSIZE) = 0 then
  begin
    Winapi.Windows.GetWindowRect(Handle, cr);
    OutputDebugString(PChar('WMWindowPosChanged: ' + cr.Width.ToString +
                            ',' + cr.Height.ToString));
  end;
  inherited;
end;

 

image.png.285665a60bbc6d4e6185c66ec80660f6.png

 

As seen from the events log the child panel is repositioned three times, and each time its size is set to different value

  • 638 * 380
  • 510 * 304
  • 640 * 382

So, in this particular case three times more work is done, than it actually required.

 

Test project: dpi_test.zip

 

PS: Looking more generally at this issue I have to conclude that layouting should be asynchronous. The concept of async layouting is a some kind of replacement of the global BeginUpdate/EndUpdate mentioned earlier. But, this will be too big and breaking change for VCL. And moreover, this is almost impossible for native Windows controls, such as TEdit, TListBox, etc.

 

 

Edited by balabuev
  • Like 1

Share this post


Link to post

After some of my VCL applications, I find the worst performer for repaints when dragging between different resolution screens is Regedit.

Share this post


Link to post

So most users here think VCL have poor implementaton for DPI on monitor changes. So what about other platforms on windows. Is it better handled in C#. Java, QT etc?

Share this post


Link to post
Guest

as I said before:

Quote

ONLY A REMASTER CODE WILL SAVE THE DELPHI ... MAYBE, ALL RAD STUDIO

 

hug

Share this post


Link to post

If Embarcadero wants to produce a per-monitor DPI-aware IDE (expected soon) they will have to deal with this issue.  Let's see what they come up with.

Edited by pyscripter

Share this post


Link to post
21 minutes ago, Berocoder said:

Delphi IDE is built with Delphi?

In short: Yes

Share this post


Link to post
2 hours ago, Berocoder said:

Well in that case it would be the ultimate test of VCL DPI handling.😊

You would think so, however, VCL Styles were added in XE2 and I think it wasn't until 10.3 they were added to the IDE, and even then the IDE is using custom themes (notice we can't just select any vcl theme).  So they don't have a great track record of dogfooding new features. 

Share this post


Link to post
On 3/24/2021 at 2:18 PM, Darian Miller said:

  set  ParentFont := True in child controls

Just a word of warning about setting ParentFont := true - I had ParentFont := true on a base form class and this completely disabled scaling on all the descendant forms!  This just took me an hour of messing around to reproduce in a test project. I have no idea why ParentFont was set on that form - the project is 20yrs old and I've changed version control several times over the years so couldn't find the commit where that was set. 

 

Not sure if this is expected behaviour on forms or a bug. 

 

Share this post


Link to post
6 hours ago, Vincent Parrett said:

Just a word of warning about setting ParentFont := true

I am sure you know but,  the form should not have ParentFont := True (disables per monitor scaling) but controls within the form could and should.

Share this post


Link to post
1 hour ago, pyscripter said:

I am sure you know but,  the form should not have ParentFont := True (disables per monitor scaling) but controls within the form could and should.

Yes, the project started in Delphi 5, been upgraded a few times over the years, so not sure when that property was set but it did take a while to figure out what was going on.  

Share this post


Link to post
On 3/24/2021 at 2:07 AM, Vincent Parrett said:

I tried to fudge a BeginUpdate/EndUpdate with this :

This is an old thread, but I have just noticed that Delphi 11 deals with this issue by calling the new LockDrawing/UnlockDrawing wrapper around WM_SETREDRAW in DoBeforeMonitorDpiChanged/DoAfterMonitorDpiChanged.  They have also fixed https://quality.embarcadero.com/browse/RSP-30931.

 

It seems to work reasonably well now.

Share this post


Link to post
On 7/12/2023 at 2:47 AM, pyscripter said:

It seems to work reasonably well now.

Yes, works about as well as it did with my hack, it's essentially doing the same thing. Still nowhere near as smooth as other applications though.

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×