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.
This commit is contained in:
Alan Third 2020-12-16 21:12:04 +00:00
parent aac17c9dca
commit 107978365e
3 changed files with 298 additions and 78 deletions

View file

@ -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=

View file

@ -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

View file

@ -72,6 +72,10 @@ Updated by Christian Limpach (chris@nice.ch)
#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 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
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 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen)
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 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors.
/* 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 @@ Note that CURSOR_WIDTH is meaningful only for (h)bar cursors.
else
[FRAME_CURSOR_COLOR (f) set];
ns_focus (f, &r, 1);
switch (cursor_type)
{
case DEFAULT_CURSOR:
@ -6267,7 +6273,7 @@ - (void)dealloc
object:nil];
#ifdef NS_DRAW_TO_BUFFER
CGContextRelease (drawingBuffer);
[surface release];
#endif
[toolbar release];
@ -7290,8 +7296,9 @@ - (void)viewDidResize:(NSNotification *)notification
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 @@ - (void)viewDidResize:(NSNotification *)notification
NSTRACE_MSG ("No change");
return;
}
[surface release];
surface = nil;
}
#endif
@ -7313,9 +7323,6 @@ - (void)viewDidResize:(NSNotification *)notification
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 @@ - (instancetype) initFrameFromEmacs: (struct frame *)f
[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 @@ - (instancetype)toggleToolbar: (id)sender
#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]");
if (! [self wantsUpdateLayer])
return;
NSGraphicsContext *screen;
CGColorSpaceRef colorSpace = [[[self window] colorSpace] CGColorSpace];
CGFloat scale = [[self window] backingScaleFactor];
NSRect frame = [self frame];
if (drawingBuffer != nil)
CGContextRelease (drawingBuffer);
NSTRACE ("[EmacsView focusOnDrawingBuffer]");
drawingBuffer = CGBitmapContextCreate (nil, NSWidth (frame) * scale, NSHeight (frame) * scale,
8, 0, colorSpace,
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host);
if (! surface)
{
NSRect frame = [self frame];
NSSize s = NSMakeSize (NSWidth (frame) * scale, NSHeight (frame) * 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);
surface = [[EmacsSurface alloc] initWithSize:s
ColorSpace:[[[self window] colorSpace]
CGColorSpace]];
}
CGContextRef context = [surface getContext];
CGContextTranslateCTM(context, 0, [surface getSize].height);
CGContextScaleCTM(context, 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 @@ - (void)windowDidChangeBackingProperties:(NSNotification *)notification
{
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 @@ - (void)copyRect:(NSRect)srcRect to:(NSRect)dstRect
if ([self wantsUpdateLayer])
{
#endif
CGImageRef copy;
NSRect frame = [self frame];
NSAffineTransform *setOrigin = [NSAffineTransform transform];
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);
[[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];
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 @@ - (void)updateLayer
{
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 @@ - (void) scrollWheel: (NSEvent *)theEvent
@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