Resizing MTKView scales old content before redraw - macos

I'm using a MTKView to draw Metal content. It's configured as follows:
mtkView = MTKView(frame: self.view.frame, device: device)
mtkView.colorPixelFormat = .bgra8Unorm
mtkView.delegate=self
mtkView.sampleCount=4
mtkView.isPaused=true
mtkView.enableSetNeedsDisplay=true
setFrameSize is overriden to trigger a redisplay.
Whenever the view resizes it scales its old content before it redraws everything. This gives a jittering feeling.
I tried setting the contentGravity property of the MTKView's layer to a non-resizing value, but that totally messes up the scale and position of the content. It seems MTKView doesn't want me to fiddle with that parameter.
How can I make sure that during a resize the content is always properly redrawn?

In my usage of Metal and MTKView, I tried various combinations of presentsWithTransaction and waitUntilScheduled without success. I still experienced occasional frames of stretched content in between frames of properly rendered content during live resize.
Finally, I dropped MTKView altogether and made my own NSView subclass that uses CAMetalLayer and resize looks good now (without any use of presentsWithTransaction or waitUntilScheduled). One key bit is that I needed to set the layer's autoresizingMask to get the displayLayer method to be called every frame during window resize.
Here's the header file:
#import <Cocoa/Cocoa.h>
#interface MyMTLView : NSView<CALayerDelegate>
#end
Here's the implementation:
#import <QuartzCore/CAMetalLayer.h>
#import <Metal/Metal.h>
#implementation MyMTLView
- (id)initWithFrame:(NSRect)frame
{
if (!(self = [super initWithFrame:frame])) {
return self;
}
// We want to be backed by a CAMetalLayer.
self.wantsLayer = YES;
// We want to redraw the layer during live window resize.
self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
// Not strictly necessary, but in case something goes wrong with live window
// resize, this layer placement makes it more obvious what's going wrong.
self.layerContentsPlacement = NSViewLayerContentsPlacementTopLeft;
return self;
}
- (CALayer*)makeBackingLayer
{
CAMetalLayer* metalLayer = [CAMetalLayer layer];
metalLayer.device = MTLCreateSystemDefaultDevice();
metalLayer.delegate = self;
// *Both* of these properties are crucial to getting displayLayer to be
// called during live window resize.
metalLayer.autoresizingMask = kCALayerHeightSizable | kCALayerWidthSizable;
metalLayer.needsDisplayOnBoundsChange = YES;
return metalLayer;
}
- (CAMetalLayer*)metalLayer
{
return (CAMetalLayer*)self.layer;
}
- (void)setFrameSize:(NSSize)newSize
{
[super setFrameSize:newSize];
self.metalLayer.drawableSize = newSize;
}
- (void)displayLayer:(CALayer*)layer
{
// Do drawing with Metal.
}
#end
For reference, I do all my Metal drawing in MTKView's drawRect method.

I have the same problem with glitches on view resizing. You can even reproduce it in the HelloTriangle example from the Apple's developer site. However the effect is minimized because the triangle is drawn near the middle of the screen, and it's the content closest to the edge of the window, opposite the corner that drags, that is effected worst. The developer notes regarding use of presentsWithTransaction and waitUntilScheduled do not work for me either.
My solution was to add a Metal layer beneath the window.contentView.layer, and to make that layer large enough that it rarely needs to be resized. The reason this works is that, unlike the window.contentView.layer, which sizes itself automatically to the view (in turn maintaining the window size), you have explicit control of the sublayer size. This eliminates the flickering.

This helped me - https://github.com/trishume/MetalTest
He uses MetalLayer and careful setting of various properties. Everything is pretty smooth even with two side by side in synchronised scroll views with 45megapixel images.
A link to my original problem How do I position an image correctly in MTKView?

Related

Render a CVPixelBuffer to an NSView (macOS)

I have a CVPixelBuffer that I'm trying to efficiently draw on screen.
The not-efficient way of turning into an NSImage works but is very slow, dropping about 40% of my frames.
Therefore, I've tried rendering it on-screen using CIContext's drawImage:inRect:fromRect. The CIContext was initialized with a NSOpenGLContext who's view was set to my VC's view. When I have a new image, I call the drawImage method which doesn't spit out any errors... but doesn't display anything on screen either (it did log errors when my contexts were not correctly setup).
I've tried to find an example of how this is done on MacOS, but everything seems to be for iOS nowadays.
EDIT:
Here's some of the code I am using. I've left out irrelevant sections
On viewDidLoad I init the GL and CI contexts
NSOpenGLPixelFormatAttribute pixelFormatAttr[] = {
kCGLPFAAllRenderers, 0
};
NSOpenGLPixelFormat *glPixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes: pixelFormatAttr];
NSOpenGLContext *glContext = [[NSOpenGLContext alloc] initWithFormat:glPixelFormat shareContext:nil];
glContext.view = self.view;
self.ciContext = [CIContext contextWithCGLContext:glContext.CGLContextObj pixelFormat:glPixelFormat.CGLPixelFormatObj colorSpace:nil options:nil];
Then, when a new frame is ready, I do:
dispatch_async(dispatch_get_main_queue(), ^{
[vc.ciContext drawImage:ciImage inRect:vc.view.bounds fromRect:ciImage.extent];
vc.isRendering = NO;
});
I am not sure I'm calling draw in the right place, but I can't seem to find out where is this supposed to go.
If the CVPixelBuffer has the kCVPixelBufferIOSurfaceCoreAnimationCompatibilityKey attribute, the backing IOSurface (retrieved via CVPixelBufferGetIOSurface) can be passed directly to the contents property of a CALayer.
This is probably the most efficient way to display a CVPixelBuffer.

Automatically wrap NSTextField using Auto Layout

How does one go about having auto-layout automatically wrap an NSTextField to multiple lines as the width of the NSTextField changes?
I have numerous NSTextFields displaying static text (i.e.: labels) in an inspector pane. As the inspector pane is resized by the user, I would like the right hand side labels to reflow to multiple lines if need be.
(The Finder's Get Info panel does this.)
But I haven't been able to figure out the proper combination of auto layout constraints to allow this behavior. In all case, the NSTextFields on the right refuse to wrap. (Unless I explicitly add a height constraint that would allow it to.)
The view hierarchy is such that each gray band is a view containing two NSTextFields, the property name on the left and the property value on the right. As the user resizes the inspector pane, I would like the property value label to auto-resize it's height as need-be.
Current situation:
What I would like to have happen:
(Note that this behavior is different than most Stack Overflow questions I came across regarding NSTextFields and auto layout. Those questions wanted the text field to grow while the user is typing. In this situation, the text is static and the NSTextField is configured to look like a label.)
Update 1.0
Taking #hamstergene's suggestion, I subclassed NSTextField and made a little sample application. For the most part, it now works, but there's now a small layout issue that I suspect is a result of the NSTextField's frame not being entirely in sync with what auto-layout expects it to be. In the screenshot below, the right-hand side labels are all vertically spaced with a top constraint. As the window is resized, the Where field is getting properly resized and wrapped. However, the Kind text field does not get pushed down until I resize the window "one more pixel".
Example: If I resize the window to just the right width that the Where textfield does it's first wrap, then I get the results in the middle image. If I resize the window one more pixel, then the Kind field's vertical location is properly set.
I suspect that's because auto-layout is doing it's pass and then the frames are getting explicitly set. I imagine auto-layout doesn't see that on that pass but does it it on the next pass, and updates the positions accordingly.
Assuming that's the issue, how do I inform auto-layout of these changes I'm doing in setFrameSize so that it can run the layout again. (And, most importantly, not get caught in recursive state of layout-setFrameSize-layout-etc...)
Solution
I've come up with a solution that appears to work exactly how I was hoping. Instead of subclassing NSTextField, I just override layout in the superview of the NSTextField in question. Within layout, I set the preferredMaxLayoutWidth on the text field and then trigger a layout pass. That appears to be enough to get it mostly working, but it leaves the annoying issue of the layout being briefly "wrong". (See note above).
The solution to that appears to be to call setNeedsDisplay and then everything Just Works.
- (void)layout {
NSTextField *textField = ...;
NSRect oldTextFieldFrame = textField.frame;
[textField setPreferredMaxLayoutWidth:NSWidth(self.bounds) - NSMinX(textField.frame) - 12.0];
[super layout];
NSRect newTextFieldFrame = textField.frame;
if (oldTextFieldFrame.size.height != newTextFieldFrame.size.height) {
[self setNeedsDisplay:YES];
}
}
The simplest way to get this working, assuming you're using an NSViewController-based solution is this:
- (void)viewDidLayout {
[super viewDidLayout];
self.aTextField.preferredMaxLayoutWidth = self.aTextField.frame.size.width;
[self.view layoutSubtreeIfNeeded];
}
This simply lets the constraint system solve for the width (height will be unsolvable on this run so will be what ever you initially set it to), then you apply that width as the max layout width and do another constraint based layout pass.
No subclassing, no mucking with a view's layout methods, no notifications. If you aren't using NSViewController you can tweak this solution so that it works in most cases (subclassing textfield, in a custom view, etc.).
Most of this came from the swell http://www.objc.io/issue-3/advanced-auto-layout-toolbox.html (look at the Intrinsic Content Size of Multi-Line Text section).
If inspector pane width will never change, just check "First Runtime Layout Width" in IB (note it's 10.8+ feature).
But allowing inspector to have variable width at the same time is not possible to achieve with constraints alone. There is a weak point somewhere in AutoLayout regarding this.
I was able to achieve reliable behaviour by subclassing the text field like this:
- (NSSize) intrinsicContentSize;
{
const CGFloat magic = -4;
NSSize rv;
if ([[self cell] wraps] && self.frame.size.height > 1)
rv = [[self cell] cellSizeForBounds:NSMakeRect(0, 0, self.bounds.size.width + magic, 20000)];
else
rv = [super intrinsicContentSize];
return rv;
}
- (void) layout;
{
[super layout];
[self invalidateWordWrappedContentSizeIfNeeded];
}
- (void) setFrameSize:(NSSize)newSize;
{
[super setFrameSize:newSize];
[self invalidateWordWrappedContentSizeIfNeeded];
}
- (void) invalidateWordWrappedContentSizeIfNeeded;
{
NSSize a = m_previousIntrinsicContentSize;
NSSize b = self.intrinsicContentSize;
if (!NSEqualSizes(a, b))
{
[self invalidateIntrinsicContentSize];
}
m_previousIntrinsicContentSize = b;
}
In either case, the constraints must be set the obvious way (you have probably already tried it): high vertical hugging priority, low horizontal, pin all four edges to superview and/or sibling views.
Set in the size inspector tab in section Text Field Preferred Width to "First Runtime layout Width"
This works for me and is a bit more elegant. Additionally i've made a little sample project on Github
public class DynamicTextField: NSTextField {
public override var intrinsicContentSize: NSSize {
if cell!.wraps {
let fictionalBounds = NSRect(x: bounds.minX, y: bounds.minY, width: bounds.width, height: CGFloat.greatestFiniteMagnitude)
return cell!.cellSize(forBounds: fictionalBounds)
} else {
return super.intrinsicContentSize
}
}
public override func textDidChange(_ notification: Notification) {
super.textDidChange(notification)
if cell!.wraps {
validatingEditing()
invalidateIntrinsicContentSize()
}
}
}

UICollectionView fix paging position after rotation

I have a UICollectionView with the frame of
[UIScreen mainScreen].bounds
and these attributes:
_collectionView.pagingEnabled = YES;
_collectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
A cell has also the size of the collection view. When I rotate, the contentOffset of the collection view does not fit to the new orientation. It has still the same offset as before the rotation.
To fix this, I changed the contentOffset manually in the didRotate method.
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
_collectionView.contentOffset = CGPointMake(_newContentOffsetX, _collectionView.contentOffset.y);
}
This works, but it looks terrible. I also tried to scroll to the current IndexPath, but it hast the same ugly behaviour:
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
[_collectionView scrollToItemAtIndexPath:_currentIndexPath atScrollPosition:UICollectionViewScrollPositionLeft animated:NO];
}
I need a clean transition and behaviour of updating the collection view's content offset when rotating the device.
Its better To use the
willAnimateRotationToInterfaceOrientation:duration:
When this method is called the bound of your view controllers view are already updated to the current device orientation. It seems that this method gets called inside of the rotation animation block. This means all the positions you will set inside of willAnimateRotationToInterfaceOrientation:duration: are animated.
This helps me a lot and I use it to update my view Controllers view content insets on rotation. Works like a charm! :-)
Instead of using didRotateFromInterfaceOrientation, try updating contentOffset in willRotateToInterfaceOrientation.
You'll need to make sure to compensate for using the other width/height dimension that you'll be rotating to since it won't have done the rotation yet.
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
_collectionView.contentOffset = CGPointMake(_newContentOffsetX, _collectionView.contentOffset.y);
}

NSSplitView resizes the custom NSView contained

I've a vertical NSSplitView, the bottom subview contains a custom view (eg NSView) and a NSTextView.
The NSView contains inside it two NSButtons.
When I resize the splitView, making it smaller, the NSView containing the buttons is resized, too.
I don't want this behavior.
To better explain my problem please view the attached image.
Image 1: the window at application startup, everything is ok
Image 2: I've resized making smaller the split view, only a little part of buttons is visible
Image 3: I've enlarged again the split view but as you can see the NSView remains smaller and buttons are no longer visible (if I resize the splitView to bottom the NSView 'disappears')
This is a vicious problem that's based on the legacy workings of Cocoa views. The best solution I've seen is to constrain the minimum dimension of any portion of the split view. If the subviews never collapse, their metrics don't cross into another dimension and they should re-enlarge just fine.
To do this, set up a delegate for your split view, which will implement - splitView:constrainMaxCoordinate:ofSubviewAt:. The split view will call your delegate method hoping it can leave the max divider position at the height of the split view (passing this in as the second argument), but you can simply subtract some quantity from that value (say, 60) to return it as the minimum height for the bottom view.
- (CGFloat)splitView:(NSSplitView *)aSplitView
constrainMaxCoordinate:(CGFloat)proposedMin
ofSubviewAt:(NSInteger)dividerIndex {
return proposedMin - 60;
}
Of course, you'll probably want to do more checking in this method to make sure you're talking about the right split view, and the right subview, to avoid overreaching effects, but this is the basic idea.
(See also this fabulicious article on the subject.)
Constraining the divider position did not help in my case, as I'm animating the subviews and subviews can be collapsed.
I managed to achieve an acceptable solution by implementing the splitView delegate method -splitviewWillResizeSubviews: (means, you have to connect the delegate property from the split view to your controller in IB or in code) to maintain a minimum width by setting the subview to hidden instead of shrinking it to zero:
- (void)splitViewWillResizeSubviews:(NSNotification *)notification {
NSUInteger divider = [[[notification userInfo] valueForKey:#"NSSplitViewDividerIndex"] intValue];
NSView *subview = nil;
if(divider == SPLITVIEW_DIVIDER_SIDEBAR) {
subview = (NSView*)[self.splitView.subviews objectAtIndex:SPLITVIEW_SIDEBAR_INDEX];
}
if(subview) {
if(subview.frame.size.width < SPLITVIEW_MINIMUM_SIDEBAR_WIDTH) {
CGRect correctedFrame = subview.frame;
correctedFrame.size.width = SPLITVIEW_MINIMUM_SIDEBAR_WIDTH;
subview.frame = correctedFrame;
subview.hidden = YES;
} else {
subview.hidden = NO;
}
}
}

Drawing into transparent overlay subview NSView

I've got a relatively simple Cocoa on Mac OS X 10.6 question to ask. I have a main NSView (ScreensaverView, actually) that is not layer backed and is otherwise unremarkable. It does some basic drawing in its drawRect via NSRectFill and NSBezierPath:stroke calls (dots and lines, basically).
I've also got a NSView-derived subclass that is acting as a child subview. I'm doing this with the goal to draw simple lines in the subview that draw on top of whatever is drawn in the main view, but then can be somehow "erased" revealing whatever the lines obscured. The code for this subview is quite simple:
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
}
return self;
}
- (void)dealloc
{
[super dealloc];
}
- (void)drawRect:(NSRect)dirtyRect {
// Transparent background
[[NSColor clearColor] set];
NSRectFillUsingOperation(dirtyRect, NSCompositeCopy);
// If needed for this update, draw line
if (drawLine) {
// OMITTED: Code that sets opaque NSColor and draws a line using NSBezierPath:stroke
}
// If needed for this update, "erase" line
if (eraseLine) {
[[NSColor blackColor] set]; // clearColor?
// OMITTED: Code that draws the same line using NSBezierPath:stroke
}
}
With the code as shown above, any time the subview draws, the main view goes black and you only see the subview line. When the subview isn't being updated, the main view contents appear again.
Things I've tried, with varying results:
I tried experimenting with making the subview return YES from an overridden isOpaque (which I realize isn't really correct). When I do this, both views draw properly during a subview update, however the subview line overwrites anything it is drawn on (ok) and then when erased, also leaves a large black line where it was (what I was trying to avoid). Trying to "erase" the line using clearColor instead of blackColor results in the line remaining on screen.
I tried making both (and/or just the subview) layer-backed views via calling [self setWantsLayer:YES] in init, and this results in a completely black screen.
I feel like I'm missing something really basic, but for whatever reason, I can't seem to figure it out. Any and all suggestions are greatly appreciated.
I think you are fundamentally misunderstanding how view drawing works. Basically, every time drawRect: is called on your view, the drawing commands in drawRect: are executed. This might happen automatically or when you issue the view a setNeedsDisplay: message.
Normally, when drawRect: is called on the view, the view is erased and drawing begins anew. Nothing remains in the view from the previous call to drawRect:. You do not need to fill the view with [NSColor clearColor] in order for it to be transparent.
Note that this is only the case if your view returns NO from isOpaque, which is the default. If your view is returning YES from isOpaque then you will need to ensure you erase the view before drawing, however in your particular case you should not return YES from isOpaque because you want your view to be transparent.
You do not need to "erase" the line you've drawn. It will be erased for you. Instead, you need to simply not draw it when you don't want it drawn.
Basically, your view should store some flag (such as your BOOL ivar named drawLine or something similar). Then, in your drawing code you should simply check if this value is set. If it is, you should draw the line. If not, then just do nothing. Your code should be reduced to this:
- (void)drawRect:(NSRect)rect
{
// If needed for this update, draw line
if (drawLine) {
// OMITTED: Code that sets opaque NSColor and draws a line using NSBezierPath:stroke
}
}
If you want to change the state and redraw the view, all you need to do is change the value of the drawLine ivar and ask the view to redraw using [yourView setNeedsDisplay:YES].
You can easily do this with a timer:
- (void)awakeFromNib
{
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(update:) userInfo:nil repeats:YES];
}
- (void)update:(NSTimer*)timer
{
drawLine = !drawLine;
[self setNeedsDisplay:YES];
}

Resources