[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
master 1079783 1/2: Improve drawing performance on macOS
From: |
Alan Third |
Subject: |
master 1079783 1/2: Improve drawing performance on macOS |
Date: |
Fri, 1 Jan 2021 17:54:39 -0500 (EST) |
branch: master
commit 107978365e17ede02d85b52fcbd99512dcc87428
Author: Alan Third <alan@idiocy.org>
Commit: Alan Third <alan@idiocy.org>
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 5f822fe..bcc0be7 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 c17a0c0..9d3ac75 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 <NSTextInput, NSWindowDelegate>
@@ -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 b34974f..e0db204 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 <Carbon/Carbon.h>
#endif
+#ifdef NS_DRAW_TO_BUFFER
+#include <IOSurface/IOSurface.h>
+#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