Hi all,
let me share our experiences and solution finding proposals to an super annoying issue that created massive paint performance problems under macOS BigSur. It causes us weeks to find out and we're happy for any feedback and cocoa expert reviews (and of coarse please feel free to patch it in if you find it valuable):
The drawrect issue was discovered by macOS developers late 2020 and is well described here:
https://developer.apple.com/forums/thread/663256And also the following thread discussed the same issue in the context of the JUCE framework:
https://forum.juce.com/t/apparent-serious-juce-painting-issue-in-big-sur/43430/22Regarding Lazarus / Cocoa WidgetSet we could easily reproduce the issue by observing several UI thread blocking and unnecessary calls to the following procedure once invalidating individual controls:
cocoawscommon.pas
procedure TLCLCommonCallback.Draw(ControlContext: NSGraphicsContext; const bounds, dirty: NSRect);
Here is our solution:
#1 Issue & FixThe first problem we discovered was that the call to
procedure TLCLCommonCallback.Draw(ControlContext: NSGraphicsContext; const bounds, dirty: NSRect);
...
LCLSendPaintMsg(Target, HDC(FContext), PS)
also repaints the parent control that might not be invalidated because macOS doesn't provides the right clipping of the context by default (as expected):
The fix is to include the dirty area into the clip region:
cocoawscommon.pas
var clipr:TRect;
SaveIndex:integer;
if FContext.InitDraw(Round(bounds.size.width), Round(bounds.size.height)) then
begin
nsr:=dirty;
nsr.origin.y:=bounds.size.height-dirty.origin.y-dirty.size.height;
clipr:= NSRectToRect(nsr);
SaveIndex := SaveDC(HDC(FContext));
IntersectClipRect(HDC(FContext),clipr.Left,clipr.Top,clipr.Right,clipr.Bottom); // this will fix the unecessary redrawing of the non affected parent control
if FIsOpaque and (Target.Color<>clDefault) then
begin
FContext.BkMode:=OPAQUE;
FContext.BkColor:=Target.Color;
FContext.BackgroundFill(nsr);
//debugln('Background '+Target.name+Dbgs(NSRectToRect(dirty)));
end;
New(PS);
try
FillChar(PS^, SizeOf(TPaintStruct), 0);
PS^.hdc := HDC(FContext);
PS^.rcPaint := NSRectToRect(nsr);
LCLSendPaintMsg(Target, HDC(FContext), PS);
if FHasCaret then
DrawCaret;
finally
Dispose(PS);
end;
RestoreDC(HDC(FContext), SaveIndex);
#2 Issue and FixUnfortunately that didn't fixed the random calls of the drawrect for non invalidated areas - it became a bit better when we applied the following fix (inspired by the developer discussions mentioned above):
cocoaprivate.pas
TCocoaCustomControl = objcclass(NSControl)
...
procedure viewWillDraw; override;
..
var
kCAContentsFormatRGBA8Uint: NSString; cvar; external;
type
LCLCALayerExtensions = objccategory external(CALayer)
procedure setContentsFormat(newValue: NSString); message 'setContentsFormat:';
end;
implementation
procedure TCocoaCustomControl.viewWillDraw;
begin
inherited;
LCLCALayerExtensions(self.layer).setContentsFormat(kCAContentsFormatRGBA8Uint); //NSStringUTF8('kCAContentsFormatRGBA8Uint'));
end;
#3 Issue and FixThat reduced the calls effectively but we're still observed unnecessary calls and UI thread blockings - the last step we applied was to make the draw layers async:
cocoawscommon.pas
class function TCocoaWSCustomControl.CreateHandle(const AWinControl: TWinControl; const AParams: TCreateParams): TLCLIntfHandle;
...
ctrl.setWantsLayer(True);
ctrl.layer.setDrawsAsynchronously(True);
...
With these 3 fixes we were able to increase the ui performance under macOS BigSur massively. Happy for your review and feedback!
Best
freq