CALayer Live Resize Poor Performance - macos

I have a UI where the content of an NSCollectionViewItem's View is drawn programmatically through CALayers. I am using a CAConstraintLayoutManager to keep the layot of the sublayers consistent when resizing, but I am getting very poor performance when doing so. It seems that resizing the window, which causes the resize of two CATextLayers so that they fit the root layer's width, and the repositioning of one CATextLayer so that it stays right-aligned, is causing the application to spend most of its time executing the CGSScanConvolveAndIntegrateRGB function (I have used the Time Profiler instrument).
The most "expensive" layer (the one that causes the most stuttering even if it's the only one displayed) is a wrapped multiline CATextLayer. I have absolutely no idea how to get better performance (I have tried not using a CAConstraintLayoutManager and going with layer alignments but I'm getting the same thing). Has anyone had this problem? Is there a way around it?
PS: I have subclassed the layout manager and disabled all the animations during the execution of - (void)layoutSublayersOfLayer:(CALayer *)layer by setting YES to kCATransactionDisableActions in the CATransaction but it doesn't seems to help.
Edit: I have disabled Font Smoothing for the Text Layers and performance has increased a little bit (very little), but it spends an awful amount of time in _ZL9view_drawP7_CAViewdPK11CVTimeStampb (which is something that gets called by a thread of the ATI Radeon driver, I suppose).

I solved it. Kind of. It still seems like a dirty hack to me, but I couldn't find out how to make setNeedsDisplayInRect work so I ended up doing it like this:
In the NSWindow delegate:
-(void)windowWillStartLiveResize:(NSNotification *)notification
{
[[NSNotificationCenter defaultCenter] postNotificationName:#"beginResize" object:nil];
}
-(void)windowDidEndLiveResize:(NSNotification *)notification
{
[[NSNotificationCenter defaultCenter] postNotificationName:#"endResize" object:nil];
}
In my Custom View those two notifications call, respectively, the -(void)beginResize and -(void)endResize selectors. The first one sets a BOOL inLiveResize variable to YES, while the second one sets it to NO and calls setFrameSize again with the new frame size.
I overrode (overridden? Not native english speaker, sorry) the -(void)setFrameSize:(NSSize)newSize method like this:
-(void)setFrameSize:(NSSize)newSize
{
if (inLiveResize) {
NSRect scrollFrame = [[[self superview] enclosingScrollView] documentVisibleRect];
BOOL condition1 = (self.frame.origin.y > (scrollFrame.origin.y - self.frame.size.height));
BOOL condition2 = (self.frame.origin.y < (scrollFrame.origin.y + scrollFrame.size.height + self.frame.size.height));
if (condition1 && condition2)
[super setFrameSize:newSize];
}
else {
[super setFrameSize:newSize]; }}
That's it. This way, only the visible views resize live with the window, while the others get redrawn at the end of the operation. It works, but I don't like how 'dirty' it is, I'm sure there is a more elegant, built-in(ish) way to do this by using the setNeedsDisplayInRect method. I will research more.

Related

NSView performance of wantsLayer

If I create a blank Mac XCode project and layout 500 simple NSView objects side by side in the main window it loads pretty damn fast. If I set wantsLayer=YES on each subview, performance dramatically drops, by several seconds. Why is this the case conceptually? It seems that layers would be faster not slower than regular old NSViews.
You're giving the system more work to do by layer-backing so many views. Layer-backing allows graphic acceleration (for drawing) but it adds a bit of overhead to things like layout, not to mention just creating them and putting them on screen. If used properly, it's not really much of a problem.
Typically, if you had so many "things" to manage on screen at once, you'd have one layer-backed hosting view that manages its own tree of sublayers. "But what about view-based table views?" you ask. Trickery, trickery, I say! Table views don't actually keep all the cell views they manage around; they efficiently reuse them, keeping around only enough to represent what's on screen and/or animating around.
So I'd say this isn't really a problem since it's not a particularly good approach to throw 500+ layer-backed views up for layout and drawing to begin with. :-)
as of 2021, Joshua Nozzi's answer is just the half of the truth.
When you want to utilise that much layers, you should make use of the power of CALayers with Sublayers instead of using NSViews over and over again, each with its own NSCell and possible CALayer backed as you forced it to with -wantsLayer:.
There is nothing wrong with 500 sublayers. Sublayers allow you to be fast when properly structured.
If you want to speed up even more, turn off autoanimation by force on each Layer that does not need it. Yes a lot examples out there show that you can turn off Autoanimation each time you call some property to be changed, slowing down your drawing even more, because it involves even more object messaging.
Keep in mind the following example turns off Auto-animated Keys by their Key name and kicks out the whole action and does not apply onto all Sublayers on purpose. If your structure of Sublayers of Sublayers need it you could call this on according ParentLayer to wipe out the CPU eating Autoanimation. Its much better to let the autoanimation do its job on the layers you actually want it to. Which turns around Apples default paradigm to animate everything. If most action keys are turned off be aware that your layers do show up kind of like a state machine. In the example here #"frame" animations are not wiped out..
void turnOffAutoAnimationsForLayerAndSublayers(CALayer* layer) {
NSNull *no = [NSNull null]; //(id)kCFNull
NSDictionary *dict = [NSDictionary dictionaryWithObjects:#[no,no,no,no,no,no,no,no]
forKeys:#[#"bounds",#"position",#"contents",#"hidden",#"backgroundColor",#"foregroundColor",#"borderWidth",#"borderColor"]];
for (CALayer *layertochange in layer.sublayers ) {
if (layertochange.sublayers.count) {
for (CALayer *sublayertochange in layertochange.sublayers) {
sublayertochange.actions = dict;
}
}
layertochange.actions = dict;
}
}
and use it in your CALayer backed NSView -init like..
CALayer *layer = [CALayer layer];
layer.frame = self.frame;
//...backgroundColor, contents, etc etc..
// here you could even do a for loop adding as much sublayers you want..
for (int y=0; y<100; y++) {
CALayer *sub = [CALayer layer];
//...frame, backgroundColor, contents, of sublayers etc etc..
layer.frame = CGRectMake(0, 0, 10, y*10);
[layer addSublayer:sub];
}
// structure is ready, add it to the base layer
self.layer = layer;
// now tell NSView you really want to make use this layer structure
self.wantsLayer = YES;
// turn off AutoAnimations
turnOffAutoAnimationsForLayerAndSublayers(self.layer);
For layers that are basically all the same there is even a specialised Subclass called CAReplicatorLayer. Allowing you to add even more with less internal draw calls.
You should also know that layers that are hidden are not calculated at all (meaning drawing here). You can still change properties even on hidden Layers. Unhide them, when needed, not earlier. So in example a custom CATextLayer that will change its string property to lets say #"", aka nothing, will still be drawn. But if you Subclass CATextLayer and change its string implementation to lets say..
#interface YourTextAutoHideLayer : CATextLayer
-(void)setString:(id _Nullable )string;
#end
#implementation YourTextAutoHideLayer
-(void)setString:(id _Nullable)string {
self.hidden = [string isEqual:#""];
super.string = string;
}
#end
you speed up drawing even more. A) because less object messaging to hide the layer that has no text anyway and B) once hidden it is not part of the internal draw calls effective speeding up your main CALayers drawing.
On NSTableViews that are NSCell based you usually never use CALayers. There is no need to, NSTableViews manage the visible Cells on purpose to keep in example scrolling smoothly. And still they (NSTableViewCells) have a reuse-mechanism to speed things up. You will very likely not re-invent a tableview with CALayers anyway. But in case you do reinvent, there is a CAScrollLayer class too.
And if that is not enough speed it is worth thinking about a MetalView utilising the power of GPUs.
Edit: code your CALayers not in -drawRect:. -drawRect: is called anytime something changes, in example frame/position on screen or bounds etc.. so try to avoid coding CALayers in there. You can set [self setNeedsLayout:YES]; & [self setNeedsDisplay:YES]; on purpose for a good reason, the reason is you want to avoid too much drawing for basically no change at all. -drawRect: in example was/is such method that is supposed to handle a backing with its own context to draw in. As CALayers have their very own mechanism you can let the drawRect block blank, doing nothing in there, maybe even erasing the method at all if you dont need it.
CALayers are not part of the auto layout system unless you code in -layout.. so again, keep CALayer drawing outside such and you are good. and pssst a lot CALayers properties can be changed even outside the main thread, some definitely not like layer.string.

ScreenSaverView with CAEmitterLayer choppy on second screen

Any ideas why a screen saver using just a plain ScreenSaverView subclass with a CAEmitterLayer sublayer would render fine on the primary screen and choppy (as if every 2. frame renders there..) on the secondary screen..?
This is my initialization code:
- (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview
{
self = [super initWithFrame:frame isPreview:isPreview];
if (self)
{
CAEmitterLayer* emitterLayer = [MyEmitterFactory emitterLayer:self];
[self setWantsLayer:YES];
[self.layer addSublayer:emitterLayer];
[self setAnimationTimeInterval:1/2.0];
}
return self;
}
Everything else in this subclass is default (as provided by the Xcode template).
Funny enough, backingStoreType does sound like a good candidate to tweak in a ScreenSaverView subclass using CoreAnimation, alas all other modes except the default one are not to be used as per the docs..
(As the animation is powered by Core Animation it doesn't really matter what I put in setAnimationTimeInterval - or remove the call completely, as experiments have shown)
According to the documentation of NSView setWantsLayer:
To create a layer-hosting view, you must call setLayer: and supply your layer object before you call the setWantsLayer: method; the order of these method calls is crucial.
Furthermore: Which OS version is it? Does the choppiness also come up when the two displays are mirrored (or vice versa)?

Are layer-hosting NSViews allowed to have subviews?

Layer-hosting NSViews (so NSViews that you supply a CALayer instance for and set it with setLayer:) can obviously contain subviews. Why obviously? Because in Apple's own Cocoa Slides sample code project, you can check a checkbox that switches the AssetCollectionView from being layer-backed to being layer-hosting:
- (void)setUsesQuartzCompositionBackground:(BOOL)flag {
if (usesQuartzCompositionBackground != flag) {
usesQuartzCompositionBackground = flag;
/* We can display a Quartz Composition in a layer-backed view tree by
substituting our own QCCompositionLayer in place of the default automanaged
layer that AppKit would otherwise create for the view. Eventually, hosting of
QCViews in a layer-backed view subtree may be made more automatic, rendering
this unnecessary. To minimize visual glitches during the transition,
temporarily suspend window updates during the switch, and toggle layer-backed
view rendering temporarily off and back on again while we prepare and set the
layer.
*/
[[self window] disableScreenUpdatesUntilFlush];
[self setWantsLayer:NO];
if (usesQuartzCompositionBackground) {
QCCompositionLayer *qcLayer = [QCCompositionLayer compositionLayerWithFile:[[NSBundle mainBundle] pathForResource:#"Cells" ofType:#"qtz"]];
[self setLayer:qcLayer];
} else {
[self setLayer:nil]; // Discard the QCCompositionLayer we were using, and let AppKit automatically create self's backing layer instead.
}
[self setWantsLayer:YES];
}
}
In the same AssetCollectionView class, subviews are added for each image that should be displayed:
- (AssetCollectionViewNode *)insertNodeForAssetAtIndex:(NSUInteger)index {
Asset *asset = [[[self assetCollection] assets] objectAtIndex:index];
AssetCollectionViewNode *node = [[AssetCollectionViewNode alloc] init];
[node setAsset:asset];
[[self animator] addSubview:[node rootView]];
[nodes addObject:node];
return [node autorelease];
}
When I build and run the app and play around with it, everything seems to be fine.
However, in Apple's NSView Class Reference for the setWantsLayer: method it reads:
When using a layer-hosting view you should not rely on the view for
drawing, nor should you add subviews to the layer-hosting view.
What is true? Is the sample code incorrect and it's just a coincidence that it works? Or is the documentation false (which I doubt)? Or is it OK because the subviews are added through the animator proxy?
When AppKit is "layer hosting" we assume you may (or may not) have a whole subtree of layers that AppKit doesn't know about.
If you add a subview to the layer hosted view, then it might not come out in the right sibling order that you want. Plus, we sometimes add and remove them, so it might change depending on when you call setLayer:, setWantsLayer: or when the view is added or removed from the superview. On Lion (and before) we remove the layers that we "own" (ie: layer backed) when the view is removed from the window (or superview).
It is okay to add subviews...their children-sibling-order in the sublayers array just might not be deterministic if you have sibling-layers that aren't NSViews.
I don't know what's the "right" answer to this. But I do think that the CocoaSlides example works within the boundaries of what the docs say you "shouldn't" do. In the example, look at where the insertNodeForAssetAtIndex: method is called, and you'll see that it only happens when the view is being populated, before it ever is assigned a layer or has setWantsLayer: called on it.
The docs don't say that a layer-hosted view can't contain any subviews, they just say that you can't add and subviews to one. At the point in time when those subviews are added, the main view hasn't yet become a layer-hosting view. After it has been turned into a layer-hosting view by having a manually created layer assigned to it, no more subviews are added.
So there's really no contradiction between the docs and this particular example. That being said, it could be interesting to explore this further, maybe by switching on the QC background layer right from the start, e.g. by sticking a [self setUsesQuartzCompositionBackground:YES]; right inside initWithFrame:.
SPOLIER ALERT:
It seems to work just fine. The creation of the display is a bit slower (not surprising with all that QC animation going on), but apart from that it's smooth sailing.
One comment about this code from Apple: it's busted.
When you first start the app up, note the nice gradient background. Turn QC on, then off.
Poof, no more gradient background.

Oomph MacMapKit Drawing Wrong

I've integrated Oomph MacMapKit in one of my projects, I did all the steps. But there is a problem.
I'm using a NSToolbar and switching between the views. whenever I'm in my map page if I switch into another page and then switch back to the map page, the map is drew under the MKMapView and MKMapView is white but I can navigate in map by dragging mouse in the white area.
I've tried it in another project as well, And it acts just like this again.
Normal Look
After switching back
Does anyone know how can I fix this?
This code is running in my window controller delegate for switching pages
- (NSView*)viewForTag:(int)tag
{
switch (tag)
{
case 0:
return [firstViewController view];
break;
case 1:
return [secondViewController view];
break;
default:
return [firstViewController view];
break;
}
}
- (IBAction)switchViews:(id)sender
{
NSView* currentView = [self viewForTag:[sender tag]];
NSView* previousView = [self viewForTag:currentViewTag];
currentViewTag = [sender tag];
[[[[self window] contentView] animator] replaceSubview:previousView with:currentView];
}
There is no special code in maps page.
Thanks in advance
I cannot fault your code. I presume you stepped through your code and checked that all views are properly added and removed. Also, you might want to check what frame is set for your map view after the switch. If the NSRect for the frame is where your white space is, somehow MKMapView is not keen on being added to and removed from the view stack.
It's a long shot, but you could try one of the following:
Use setHidden: to temporarily make your map view invisible, in stead of removing and adding it. Not sure, but you might gain a bit of performance from this as well as hiding and showing is probably less expensive than adding and removing...
Use addSubView: and removeFromSuperview to switch your views. This might need some code to provide for setting the framing of the views right or have a "default" frame handy that you can use when you add a view.
Bottom line MKMapView is wrapper around WebView, which sometimes seems to behave a bit odd.
This Map Kit is a bit buggy sometimes, changing my way of presenting views solved the problem.

Setting up Continuous rendering in a mac osx cocoa application using OpenGL

I'm starting to work on a 3D particle system editor and evolver. Ive done something similar in the past with OpenGL but this time I'm making a mac os x cocoa application. I just have a few questions regarding some code I keep running into on setting up OpenGL.
1) Why do I see a lot of people on the web using...
[self setNeedsDisplay:YES];
Is this the proper way to get OpenGL to render, I now understand it leads to drawRect being called, but is it the correct way?
2) Is drawRect the proper method I should be overriding for my render frame method?
Heres the code that I continue to run into on the web:
-(void) prepareOpenGL {
[[self window] makeFirstResponder:self];
glClearColor(1.0f, 1.0f, 0.0f, 10.f);
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0/60.0 target:self selector:#selector(idle:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}
-(void)idle:(NSTimer *)timer {
if(![[NSApplication sharedApplication] isHidden])
[self setNeedsDisplay:YES];
}
-(void) drawRect:(NSRect)dirtyRect {
glClear(GL_COLOR_BUFFER_BIT);
}
You haven't indicated whether you will be drawing your OpenGL content within an NSOpenGLView or a CAOpenGLLayer. These two have slightly different ways of updating their content for display to the screen.
For an NSOpenGLView, you don't need to update the view within it's -drawRect: method. In fact, I think you won't want to trigger -setNeedsDisplay: to do a refresh of the NSView because of some overhead that might incur. In one of my applications, I use a CVDisplayLink to trigger updates at 60 FPS within my own custom rendering methods in an NSOpenGLView. None of these touch -drawRect:. Frames are presented to the screen upon calling [[self openGLContext] flushBuffer], not by forcing a redraw of the NSView.
CAOpenGLLayers are a little different, in that you override - drawInCGLContext:pixelFormat:forLayerTime:displayTime: with your custom rendering code. This method is triggered in response to a manual -setNeedsDisplay or by the CAOpenGLLayer itself if its asynchronous property is set to YES. It knows when it's ready to present new content by the boolean value you provide in response to -canDrawInCGLContext:pixelFormat:forLayerTime:displayTime:.
I've used both of these, and each has its advantages. CAOpenGLLayers make it much easier to overlay other UI elements on your OpenGL rendering, but their rendering methods can be difficult to get to work correctly from a background thread. NSOpenGLViews can be updated easily on a background thread using a CVDisplayLink, but are a bear to overlay content on.

Resources