From 107978365e17ede02d85b52fcbd99512dcc87428 Mon Sep 17 00:00:00 2001 From: Alan Third Date: Wed, 16 Dec 2020 21:12:04 +0000 Subject: [PATCH] Improve drawing performance on macOS * configure.ac: Require IOSurface framework. * src/nsterm.h: New EmacsSurface class and update EmacsView definitions. * src/nsterm.m (ns_update_end): (ns_unfocus): Use new unfocusDrawingBuffer method. (ns_draw_window_cursor): Move ns_focus to before we set colors. ([EmacsView dealloc]): ([EmacsView viewDidResize:]): Handle new EmacsSurface class. ([EmacsView initFrameFromEmacs:]): Remove reference to old method. ([EmacsView createDrawingBuffer]): Remove method. ([EmacsView focusOnDrawingBuffer]): ([EmacsView windowDidChangeBackingProperties:]): Use new EmacsSurface class. ([EmacsView unfocusDrawingBuffer]): New method. ([EmacsView copyRect:to:]): Get information from the context instead of direct from the IOSurface. ([EmacsView updateLayer]): Use new EmacsSurface class. ([EmacsView copyRect:to:]): Use memcpy to copy bits around instead of using NS image functions. ([EmacsSurface initWithSize:ColorSpace:]): ([EmacsSurface dealloc]): ([EmacsSurface getSize]): ([EmacsSurface getContext]): ([EmacsSurface releaseContext]): ([EmacsSurface getSurface]): ([EmacsSurface copyContentsTo:]): New class and methods. --- configure.ac | 2 +- src/nsterm.h | 23 +++- src/nsterm.m | 351 ++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 298 insertions(+), 78 deletions(-) diff --git a/configure.ac b/configure.ac index 5f822fe9510..bcc0be7de03 100644 --- a/configure.ac +++ b/configure.ac @@ -5496,7 +5496,7 @@ case "$opsys" in if test "$HAVE_NS" = "yes"; then libs_nsgui="-framework AppKit" if test "$NS_IMPL_COCOA" = "yes"; then - libs_nsgui="$libs_nsgui -framework IOKit -framework Carbon" + libs_nsgui="$libs_nsgui -framework IOKit -framework Carbon -framework IOSurface" fi else libs_nsgui= diff --git a/src/nsterm.h b/src/nsterm.h index c17a0c0135e..9d3ac75caf3 100644 --- a/src/nsterm.h +++ b/src/nsterm.h @@ -414,6 +414,7 @@ typedef id instancetype; ========================================================================== */ @class EmacsToolbar; +@class EmacsSurface; #ifdef NS_IMPL_COCOA @interface EmacsView : NSView @@ -435,7 +436,7 @@ typedef id instancetype; BOOL fs_is_native; BOOL in_fullscreen_transition; #ifdef NS_DRAW_TO_BUFFER - CGContextRef drawingBuffer; + EmacsSurface *surface; #endif @public struct frame *emacsframe; @@ -478,7 +479,7 @@ typedef id instancetype; #ifdef NS_DRAW_TO_BUFFER - (void)focusOnDrawingBuffer; -- (void)createDrawingBuffer; +- (void)unfocusDrawingBuffer; #endif - (void)copyRect:(NSRect)srcRect to:(NSRect)dstRect; @@ -705,6 +706,24 @@ typedef id instancetype; @end +@interface EmacsSurface : NSObject +{ + NSMutableArray *cache; + NSSize size; + CGColorSpaceRef colorSpace; + IOSurfaceRef currentSurface; + IOSurfaceRef lastSurface; + CGContextRef context; +} +- (id) initWithSize: (NSSize)s ColorSpace: (CGColorSpaceRef)cs; +- (void) dealloc; +- (NSSize) getSize; +- (CGContextRef) getContext; +- (void) releaseContext; +- (IOSurfaceRef) getSurface; +@end + + /* ========================================================================== Rendering diff --git a/src/nsterm.m b/src/nsterm.m index b34974f1bfb..e0db204fbc6 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -72,6 +72,10 @@ GNUstep port and post-20 update by Adrian Robert (arobert@cogsci.ucsd.edu) #include #endif +#ifdef NS_DRAW_TO_BUFFER +#include +#endif + static EmacsMenu *dockMenu; #ifdef NS_IMPL_COCOA static EmacsMenu *mainMenu; @@ -1147,7 +1151,7 @@ ns_update_end (struct frame *f) if ([FRAME_NS_VIEW (f) wantsUpdateLayer]) { #endif - [NSGraphicsContext setCurrentContext:nil]; + [FRAME_NS_VIEW (f) unfocusDrawingBuffer]; #if MAC_OS_X_VERSION_MIN_REQUIRED < 101400 } else @@ -1255,6 +1259,8 @@ ns_unfocus (struct frame *f) if ([FRAME_NS_VIEW (f) wantsUpdateLayer]) { #endif + if (! ns_updating_frame) + [FRAME_NS_VIEW (f) unfocusDrawingBuffer]; [FRAME_NS_VIEW (f) setNeedsDisplay:YES]; #if MAC_OS_X_VERSION_MIN_REQUIRED < 101400 } @@ -3386,6 +3392,8 @@ ns_draw_window_cursor (struct window *w, struct glyph_row *glyph_row, /* Prevent the cursor from being drawn outside the text area. */ r = NSIntersectionRect (r, ns_row_rect (w, glyph_row, TEXT_AREA)); + ns_focus (f, &r, 1); + face = FACE_FROM_ID_OR_NULL (f, phys_cursor_glyph->face_id); if (face && NS_FACE_BACKGROUND (face) == ns_index_color (FRAME_CURSOR_COLOR (f), f)) @@ -3396,8 +3404,6 @@ ns_draw_window_cursor (struct window *w, struct glyph_row *glyph_row, else [FRAME_CURSOR_COLOR (f) set]; - ns_focus (f, &r, 1); - switch (cursor_type) { case DEFAULT_CURSOR: @@ -6267,7 +6273,7 @@ not_in_argv (NSString *arg) object:nil]; #ifdef NS_DRAW_TO_BUFFER - CGContextRelease (drawingBuffer); + [surface release]; #endif [toolbar release]; @@ -7290,8 +7296,9 @@ not_in_argv (NSString *arg) if ([self wantsUpdateLayer]) { CGFloat scale = [[self window] backingScaleFactor]; - int oldw = (CGFloat)CGBitmapContextGetWidth (drawingBuffer) / scale; - int oldh = (CGFloat)CGBitmapContextGetHeight (drawingBuffer) / scale; + NSSize size = [surface getSize]; + int oldw = size.width / scale; + int oldh = size.height / scale; NSTRACE_SIZE ("Original size", NSMakeSize (oldw, oldh)); @@ -7301,6 +7308,9 @@ not_in_argv (NSString *arg) NSTRACE_MSG ("No change"); return; } + + [surface release]; + surface = nil; } #endif @@ -7313,9 +7323,6 @@ not_in_argv (NSString *arg) FRAME_PIXEL_TO_TEXT_HEIGHT (emacsframe, newh), 0, YES, 0, 1); -#ifdef NS_DRAW_TO_BUFFER - [self createDrawingBuffer]; -#endif SET_FRAME_GARBAGED (emacsframe); cancel_mouse_face (emacsframe); } @@ -7586,10 +7593,6 @@ not_in_argv (NSString *arg) [NSApp registerServicesMenuSendTypes: ns_send_types returnTypes: [NSArray array]]; -#ifdef NS_DRAW_TO_BUFFER - [self createDrawingBuffer]; -#endif - /* Set up view resize notifications. */ [self setPostsFrameChangedNotifications:YES]; [[NSNotificationCenter defaultCenter] @@ -8309,45 +8312,41 @@ not_in_argv (NSString *arg) #ifdef NS_DRAW_TO_BUFFER -- (void)createDrawingBuffer - /* Create and store a new CGGraphicsContext for Emacs to draw into. - - We can't do this in GNUstep as there's no equivalent, so under - GNUstep we retain the old method of drawing direct to the - EmacsView. */ +- (void)focusOnDrawingBuffer { - NSTRACE ("EmacsView createDrawingBuffer]"); + CGFloat scale = [[self window] backingScaleFactor]; - if (! [self wantsUpdateLayer]) - return; + NSTRACE ("[EmacsView focusOnDrawingBuffer]"); - NSGraphicsContext *screen; - CGColorSpaceRef colorSpace = [[[self window] colorSpace] CGColorSpace]; - CGFloat scale = [[self window] backingScaleFactor]; - NSRect frame = [self frame]; + if (! surface) + { + NSRect frame = [self frame]; + NSSize s = NSMakeSize (NSWidth (frame) * scale, NSHeight (frame) * scale); + + surface = [[EmacsSurface alloc] initWithSize:s + ColorSpace:[[[self window] colorSpace] + CGColorSpace]]; + } - if (drawingBuffer != nil) - CGContextRelease (drawingBuffer); + CGContextRef context = [surface getContext]; - drawingBuffer = CGBitmapContextCreate (nil, NSWidth (frame) * scale, NSHeight (frame) * scale, - 8, 0, colorSpace, - kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host); + CGContextTranslateCTM(context, 0, [surface getSize].height); + CGContextScaleCTM(context, scale, -scale); - /* This fixes the scale to match the backing scale factor, and flips the image. */ - CGContextTranslateCTM(drawingBuffer, 0, NSHeight (frame) * scale); - CGContextScaleCTM(drawingBuffer, scale, -scale); + [NSGraphicsContext + setCurrentContext:[NSGraphicsContext + graphicsContextWithCGContext:context + flipped:YES]]; } -- (void)focusOnDrawingBuffer +- (void)unfocusDrawingBuffer { - NSTRACE ("EmacsView focusOnDrawingBuffer]"); + NSTRACE ("[EmacsView unfocusDrawingBuffer]"); - NSGraphicsContext *buf = - [NSGraphicsContext - graphicsContextWithCGContext:drawingBuffer flipped:YES]; - - [NSGraphicsContext setCurrentContext:buf]; + [NSGraphicsContext setCurrentContext:nil]; + [surface releaseContext]; + [self setNeedsDisplay:YES]; } @@ -8356,11 +8355,11 @@ not_in_argv (NSString *arg) { NSTRACE ("EmacsView windowDidChangeBackingProperties:]"); - if (! [self wantsUpdateLayer]) - return; - NSRect frame = [self frame]; - [self createDrawingBuffer]; + + [surface release]; + surface = nil; + ns_clear_frame (emacsframe); expose_frame (emacsframe, 0, 0, NSWidth (frame), NSHeight (frame)); } @@ -8378,33 +8377,28 @@ not_in_argv (NSString *arg) if ([self wantsUpdateLayer]) { #endif - CGImageRef copy; - NSRect frame = [self frame]; - NSAffineTransform *setOrigin = [NSAffineTransform transform]; - - [[NSGraphicsContext currentContext] saveGraphicsState]; - - /* Set the clipping before messing with the buffer's - orientation. */ - NSRectClip (dstRect); - - /* Unflip the buffer as the copied image will be unflipped, and - offset the top left so when we draw back into the buffer the - correct part of the image is drawn. */ - CGContextScaleCTM(drawingBuffer, 1, -1); - CGContextTranslateCTM(drawingBuffer, - NSMinX (dstRect) - NSMinX (srcRect), - -NSHeight (frame) - (NSMinY (dstRect) - NSMinY (srcRect))); - - /* Take a copy of the buffer and then draw it back to the buffer, - limited by the clipping rectangle. */ - copy = CGBitmapContextCreateImage (drawingBuffer); - CGContextDrawImage (drawingBuffer, frame, copy); - - CGImageRelease (copy); - - [[NSGraphicsContext currentContext] restoreGraphicsState]; - [self setNeedsDisplayInRect:dstRect]; + double scale = [[self window] backingScaleFactor]; + CGContextRef context = [[NSGraphicsContext currentContext] CGContext]; + int bpp = CGBitmapContextGetBitsPerPixel (context) / 8; + void *pixels = CGBitmapContextGetData (context); + int rowSize = CGBitmapContextGetBytesPerRow (context); + int srcRowSize = NSWidth (srcRect) * scale * bpp; + void *srcPixels = pixels + (int)(NSMinY (srcRect) * scale * rowSize + + NSMinX (srcRect) * scale * bpp); + void *dstPixels = pixels + (int)(NSMinY (dstRect) * scale * rowSize + + NSMinX (dstRect) * scale * bpp); + + if (NSIntersectsRect (srcRect, dstRect) + && NSMinY (srcRect) < NSMinY (dstRect)) + for (int y = NSHeight (srcRect) * scale - 1 ; y >= 0 ; y--) + memmove (dstPixels + y * rowSize, + srcPixels + y * rowSize, + srcRowSize); + else + for (int y = 0 ; y < NSHeight (srcRect) * scale ; y++) + memmove (dstPixels + y * rowSize, + srcPixels + y * rowSize, + srcRowSize); #if MAC_OS_X_VERSION_MIN_REQUIRED < 101400 } @@ -8445,9 +8439,12 @@ not_in_argv (NSString *arg) { NSTRACE ("[EmacsView updateLayer]"); - CGImageRef contentsImage = CGBitmapContextCreateImage(drawingBuffer); - [[self layer] setContents:(id)contentsImage]; - CGImageRelease(contentsImage); + /* This can fail to update the screen if the same surface is + provided twice in a row, even if its contents have changed. + There's a private method, -[CALayer setContentsChanged], that we + could use to force it, but we shouldn't often get the same + surface twice in a row. */ + [[self layer] setContents:(id)[surface getSurface]]; } #endif @@ -9490,6 +9487,210 @@ not_in_argv (NSString *arg) @end /* EmacsScroller */ +#ifdef NS_DRAW_TO_BUFFER + +/* ========================================================================== + + A class to handle the screen buffer. + + ========================================================================== */ + +@implementation EmacsSurface + + +/* An IOSurface is a pixel buffer that is efficiently copied to VRAM + for display. In order to use an IOSurface we must first lock it, + write to it, then unlock it. At this point it is transferred to + VRAM and if we modify it during this transfer we may see corruption + of the output. To avoid this problem we can check if the surface + is "in use", and if it is then avoid using it. Unfortunately to + avoid writing to a surface that's in use, but still maintain the + ability to draw to the screen at any time, we need to keep a cache + of multiple surfaces that we can use at will. + + The EmacsSurface class maintains this cache of surfaces, and + handles the conversion to a CGGraphicsContext that AppKit can use + to draw on. + + The cache is simple: if a free surface is found it is removed from + the cache and set as the "current" surface. Once Emacs is done + with drawing to the current surface, the previous surface that was + drawn to is added to the cache for reuse, and the current one is + set as the last surface. If no free surfaces are found in the + cache then a new one is created. + + When AppKit wants to update the screen, we provide it with the last + surface, as that has the most recent data. + + FIXME: It is possible for the cache to grow if Emacs draws faster + than the surfaces can be drawn to the screen, so there should + probably be some sort of pruning job that removes excess + surfaces. */ + + +- (id) initWithSize: (NSSize)s + ColorSpace: (CGColorSpaceRef)cs +{ + NSTRACE ("[EmacsSurface initWithSize:ColorSpace:]"); + + [super init]; + + cache = [[NSMutableArray arrayWithCapacity:3] retain]; + size = s; + colorSpace = cs; + + return self; +} + + +- (void) dealloc +{ + if (context) + CGContextRelease (context); + + if (currentSurface) + CFRelease (currentSurface); + if (lastSurface) + CFRelease (lastSurface); + + for (id object in cache) + CFRelease ((IOSurfaceRef)object); + + [cache removeAllObjects]; + + [super dealloc]; +} + + +/* Return the size values our cached data is using. */ +- (NSSize) getSize +{ + return size; +} + + +/* Return a CGContextRef that can be used for drawing to the screen. + This must ALWAYS be paired with a call to releaseContext, and the + calls cannot be nested. */ +- (CGContextRef) getContext +{ + IOSurfaceRef surface = NULL; + + NSTRACE ("[EmacsSurface getContextWithSize:]"); + NSTRACE_MSG (@"IOSurface count: %lu", [cache count] + (lastSurface ? 1 : 0)); + + for (id object in cache) + { + if (!IOSurfaceIsInUse ((IOSurfaceRef)object)) + { + surface = (IOSurfaceRef)object; + [cache removeObject:object]; + break; + } + } + + if (!surface) + { + int bytesPerRow = IOSurfaceAlignProperty (kIOSurfaceBytesPerRow, + size.width * 4); + + surface = IOSurfaceCreate + ((CFDictionaryRef)@{(id)kIOSurfaceWidth:[NSNumber numberWithInt:size.width], + (id)kIOSurfaceHeight:[NSNumber numberWithInt:size.height], + (id)kIOSurfaceBytesPerRow:[NSNumber numberWithInt:bytesPerRow], + (id)kIOSurfaceBytesPerElement:[NSNumber numberWithInt:4], + (id)kIOSurfacePixelFormat:[NSNumber numberWithUnsignedInt:'BGRA']}); + } + + IOReturn lockStatus = IOSurfaceLock (surface, 0, nil); + if (lockStatus != kIOReturnSuccess) + NSLog (@"Failed to lock surface: %x", lockStatus); + + [self copyContentsTo:surface]; + + currentSurface = surface; + + context = CGBitmapContextCreate (IOSurfaceGetBaseAddress (currentSurface), + IOSurfaceGetWidth (currentSurface), + IOSurfaceGetHeight (currentSurface), + 8, + IOSurfaceGetBytesPerRow (currentSurface), + colorSpace, + (kCGImageAlphaPremultipliedFirst + | kCGBitmapByteOrder32Host)); + return context; +} + + +/* Releases the CGGraphicsContext and unlocks the associated + IOSurface, so it will be sent to VRAM. */ +- (void) releaseContext +{ + NSTRACE ("[EmacsSurface releaseContextAndGetSurface]"); + + CGContextRelease (context); + context = NULL; + + IOReturn lockStatus = IOSurfaceUnlock (currentSurface, 0, nil); + if (lockStatus != kIOReturnSuccess) + NSLog (@"Failed to unlock surface: %x", lockStatus); + + /* Put lastSurface back on the end of the cache. It may not have + been displayed on the screen yet, but we probably want the new + data and not some stale data anyway. */ + if (lastSurface) + [cache addObject:(id)lastSurface]; + lastSurface = currentSurface; + currentSurface = NULL; +} + + +/* Get the IOSurface that we want to draw to the screen. */ +- (IOSurfaceRef) getSurface +{ + /* lastSurface always contains the most up-to-date and complete data. */ + return lastSurface; +} + + +/* Copy the contents of lastSurface to DESTINATION. This is required + every time we want to use an IOSurface as its contents are probably + blanks (if it's new), or stale. */ +- (void) copyContentsTo: (IOSurfaceRef) destination +{ + IOReturn lockStatus; + void *sourceData, *destinationData; + int numBytes = IOSurfaceGetAllocSize (destination); + + NSTRACE ("[EmacsSurface copyContentsTo:]"); + + if (! lastSurface) + return; + + lockStatus = IOSurfaceLock (lastSurface, kIOSurfaceLockReadOnly, nil); + if (lockStatus != kIOReturnSuccess) + NSLog (@"Failed to lock source surface: %x", lockStatus); + + sourceData = IOSurfaceGetBaseAddress (lastSurface); + destinationData = IOSurfaceGetBaseAddress (destination); + + /* Since every IOSurface should have the exact same settings, a + memcpy seems like the fastest way to copy the data from one to + the other. */ + memcpy (destinationData, sourceData, numBytes); + + lockStatus = IOSurfaceUnlock (lastSurface, kIOSurfaceLockReadOnly, nil); + if (lockStatus != kIOReturnSuccess) + NSLog (@"Failed to unlock source surface: %x", lockStatus); +} + + +@end /* EmacsSurface */ + + +#endif + + #ifdef NS_IMPL_GNUSTEP /* Dummy class to get rid of startup warnings. */ @implementation EmacsDocument -- 2.39.5