emacs-diffs
[Top][All Lists]
Advanced

[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



reply via email to

[Prev in Thread] Current Thread [Next in Thread]