Browse Source

CocoaSpice: refactor cursor drawing into display

There was a misunderstanding previouly that the input code and
cursor drawing code should reside in the same class. This led to
one CocoaSpice class handling both the SPICE input and cursor
channel. The problem is that SPICE protocol specifies a 1-to-1
mapping of cursor channel and display channel (sharing the same
channel id). To implement support for multiple displays, we
would have to follow this convention.

Additionally, there was some akwardness in forcing CSInput to
comply with the UTMRenderSource protocol because it shared the
viewport size with CSDisplay. "Outside" code would have to
syncronize the display size.

As part of the refactor, we also tried to improve the code style
by first moving as many ivars to properties as possible and by
also renaming some methods and inits.
osy 4 years ago
parent
commit
23dde8cfaa

+ 21 - 3
CocoaSpice/CSDisplayMetal.h

@@ -32,11 +32,29 @@ NS_ASSUME_NONNULL_BEGIN
 @property (nonatomic, readonly, assign) NSInteger monitorID;
 @property (nonatomic, assign) CGSize displaySize;
 @property (nonatomic, readonly) UTMScreenshot *screenshot;
-
-- (id)initWithSession:(nonnull SpiceSession *)session channelID:(NSInteger)channelID monitorID:(NSInteger)monitorID;
-- (id)initWithSession:(nonnull SpiceSession *)session channelID:(NSInteger)channelID;
+@property (nonatomic, assign) BOOL inhibitCursor;
+@property (nonatomic) CGSize cursorSize;
+
+// properties from UTMRenderSource
+@property (nonatomic, readonly) BOOL cursorVisible;
+@property (nonatomic) CGPoint cursorOrigin;
+@property (nonatomic) CGPoint viewportOrigin;
+@property (nonatomic) CGFloat viewportScale;
+@property (nonatomic, readonly) dispatch_semaphore_t drawLock;
+@property (nonatomic, nullable) id<MTLDevice> device;
+@property (nonatomic, nullable, readonly) id<MTLTexture> displayTexture;
+@property (nonatomic, nullable, readonly) id<MTLTexture> cursorTexture;
+@property (nonatomic, readonly) NSUInteger displayNumVertices;
+@property (nonatomic, readonly) NSUInteger cursorNumVertices;
+@property (nonatomic, nullable, readonly) id<MTLBuffer> displayVertices;
+@property (nonatomic, nullable, readonly) id<MTLBuffer> cursorVertices;
+
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithSession:(nonnull SpiceSession *)session channelID:(NSInteger)channelID monitorID:(NSInteger)monitorID NS_DESIGNATED_INITIALIZER;
+- (instancetype)initWithSession:(nonnull SpiceSession *)session channelID:(NSInteger)channelID;
 - (void)updateVisibleAreaWithRect:(CGRect)rect;
 - (void)requestResolution:(CGRect)bounds;
+- (void)forceCursorPosition:(CGPoint)pos;
 
 @end
 

+ 261 - 51
CocoaSpice/CSDisplayMetal.m

@@ -36,6 +36,19 @@
 @property (nonatomic, readwrite, assign) NSInteger monitorID;
 @property (nonatomic, nullable) SpiceDisplayChannel *display;
 @property (nonatomic, nullable) SpiceMainChannel *main;
+@property (nonatomic, nullable) SpiceCursorChannel *cursor;
+@property (nonatomic, readwrite) CGPoint cursorHotspot;
+@property (nonatomic, readwrite) BOOL cursorHidden;
+@property (nonatomic, readwrite) BOOL hasCursor;
+
+// UTMRenderSource properties
+@property (nonatomic, readwrite) dispatch_semaphore_t drawLock;
+@property (nonatomic, nullable, readwrite) id<MTLTexture> displayTexture;
+@property (nonatomic, nullable, readwrite) id<MTLTexture> cursorTexture;
+@property (nonatomic, readwrite) NSUInteger displayNumVertices;
+@property (nonatomic, readwrite) NSUInteger cursorNumVertices;
+@property (nonatomic, nullable, readwrite) id<MTLBuffer> displayVertices;
+@property (nonatomic, nullable, readwrite) id<MTLBuffer> cursorVertices;
 
 @end
 
@@ -47,13 +60,11 @@
     CGRect                  _canvasArea;
     CGRect                  _visibleArea;
     GWeakRef                _overlay_weak_ref;
-    id<MTLDevice>           _device;
-    id<MTLTexture>          _texture;
-    id<MTLBuffer>           _vertices;
-    NSUInteger              _numVertices;
-    dispatch_semaphore_t    _drawLock;
+    CGPoint                 _mouse_guest;
 }
 
+#pragma mark - Display events
+
 static void cs_primary_create(SpiceChannel *channel, gint format,
                            gint width, gint height, gint stride,
                            gint shmid, gpointer imgdata, gpointer data) {
@@ -159,6 +170,90 @@ whole:
     self.ready = YES;
 }
 
+#pragma mark - Cursor events
+
+static void cs_update_mouse_mode(SpiceChannel *channel, gpointer data)
+{
+    CSDisplayMetal *self = (__bridge CSDisplayMetal *)data;
+    enum SpiceMouseMode mouse_mode;
+    
+    g_object_get(channel, "mouse-mode", &mouse_mode, NULL);
+    DISPLAY_DEBUG(self, "mouse mode %u", mouse_mode);
+    
+    if (mouse_mode == SPICE_MOUSE_MODE_SERVER) {
+        self->_mouse_guest.x = -1;
+        self->_mouse_guest.y = -1;
+    }
+}
+
+static void cs_cursor_invalidate(CSDisplayMetal *self)
+{
+    // We implement two different textures so invalidate is not needed
+}
+
+static void cs_cursor_set(SpiceCursorChannel *channel,
+                          G_GNUC_UNUSED GParamSpec *pspec,
+                          gpointer data)
+{
+    CSDisplayMetal *self = (__bridge CSDisplayMetal *)data;
+    SpiceCursorShape *cursor_shape;
+    
+    g_object_get(G_OBJECT(channel), "cursor", &cursor_shape, NULL);
+    if (G_UNLIKELY(cursor_shape == NULL || cursor_shape->data == NULL)) {
+        if (cursor_shape != NULL) {
+            g_boxed_free(SPICE_TYPE_CURSOR_SHAPE, cursor_shape);
+        }
+        return;
+    }
+    
+    cs_cursor_invalidate(self);
+    
+    CGPoint hotspot = CGPointMake(cursor_shape->hot_spot_x, cursor_shape->hot_spot_y);
+    CGSize newSize = CGSizeMake(cursor_shape->width, cursor_shape->height);
+    if (!CGSizeEqualToSize(newSize, self.cursorSize) || !CGPointEqualToPoint(hotspot, self.cursorHotspot)) {
+        [self rebuildCursorWithSize:newSize center:hotspot];
+    }
+    [self drawCursor:cursor_shape->data];
+    self.cursorHidden = NO;
+    cs_cursor_invalidate(self);
+}
+
+static void cs_cursor_move(SpiceCursorChannel *channel, gint x, gint y, gpointer data)
+{
+    CSDisplayMetal *self = (__bridge CSDisplayMetal *)data;
+    
+    cs_cursor_invalidate(self); // old pointer buffer
+    
+    self->_mouse_guest.x = x;
+    self->_mouse_guest.y = y;
+    
+    cs_cursor_invalidate(self); // new pointer buffer
+    
+    /* apparently we have to restore cursor when "cursor_move" */
+    if (self.hasCursor) {
+        self.cursorHidden = NO;
+    }
+}
+
+static void cs_cursor_hide(SpiceCursorChannel *channel, gpointer data)
+{
+    CSDisplayMetal *self = (__bridge CSDisplayMetal *)data;
+    
+    self.cursorHidden = YES;
+    cs_cursor_invalidate(self);
+}
+
+static void cs_cursor_reset(SpiceCursorChannel *channel, gpointer data)
+{
+    CSDisplayMetal *self = (__bridge CSDisplayMetal *)data;
+    
+    DISPLAY_DEBUG(self, "%s",  __FUNCTION__);
+    [self destroyCursor];
+    cs_cursor_invalidate(self);
+}
+
+#pragma mark - Channel events
+
 static void cs_channel_new(SpiceSession *s, SpiceChannel *channel, gpointer data) {
     CSDisplayMetal *self = (__bridge CSDisplayMetal *)data;
     gint channel_id;
@@ -194,8 +289,35 @@ static void cs_channel_new(SpiceSession *s, SpiceChannel *channel, gpointer data
         return;
     }
     
+    if (SPICE_IS_CURSOR_CHANNEL(channel)) {
+        gpointer cursor_shape;
+        if (channel_id != self.channelID) {
+            return;
+        }
+        self.cursor = SPICE_CURSOR_CHANNEL(channel);
+        g_signal_connect(channel, "notify::cursor",
+                         G_CALLBACK(cs_cursor_set), GLIB_OBJC_RETAIN(self));
+        g_signal_connect(channel, "cursor-move",
+                         G_CALLBACK(cs_cursor_move), GLIB_OBJC_RETAIN(self));
+        g_signal_connect(channel, "cursor-hide",
+                         G_CALLBACK(cs_cursor_hide), GLIB_OBJC_RETAIN(self));
+        g_signal_connect(channel, "cursor-reset",
+                         G_CALLBACK(cs_cursor_reset), GLIB_OBJC_RETAIN(self));
+        spice_channel_connect(channel);
+        
+        g_object_get(G_OBJECT(channel), "cursor", &cursor_shape, NULL);
+        if (cursor_shape != NULL) {
+            g_boxed_free(SPICE_TYPE_CURSOR_SHAPE, cursor_shape);
+            cs_cursor_set(self.cursor, NULL, (__bridge void *)self);
+        }
+        return;
+    }
+    
     if (SPICE_IS_MAIN_CHANNEL(channel)) {
         self.main = SPICE_MAIN_CHANNEL(channel);
+        g_signal_connect(channel, "main-mouse-update",
+                         G_CALLBACK(cs_update_mouse_mode), GLIB_OBJC_RETAIN(self));
+        cs_update_mouse_mode(channel, data);
         return;
     }
 }
@@ -223,17 +345,31 @@ static void cs_channel_destroy(SpiceSession *s, SpiceChannel *channel, gpointer
         return;
     }
     
+    if (SPICE_IS_CURSOR_CHANNEL(channel)) {
+        if (channel_id != self.channelID) {
+            return;
+        }
+        self.cursor = NULL;
+        g_signal_handlers_disconnect_by_func(channel, G_CALLBACK(cs_cursor_set), GLIB_OBJC_RELEASE(self));
+        g_signal_handlers_disconnect_by_func(channel, G_CALLBACK(cs_cursor_move), GLIB_OBJC_RELEASE(self));
+        g_signal_handlers_disconnect_by_func(channel, G_CALLBACK(cs_cursor_hide), GLIB_OBJC_RELEASE(self));
+        g_signal_handlers_disconnect_by_func(channel, G_CALLBACK(cs_cursor_reset), GLIB_OBJC_RELEASE(self));
+        return;
+    }
+    
+    if (SPICE_IS_MAIN_CHANNEL(channel)) {
+        self.main = NULL;
+        g_signal_handlers_disconnect_by_func(channel, G_CALLBACK(cs_update_mouse_mode), GLIB_OBJC_RELEASE(self));
+        return;
+    }
+    
     return;
 }
 
 - (void)setDevice:(id<MTLDevice>)device {
     _device = device;
-    [self rebuildTexture];
-    [self rebuildVertices];
-}
-
-- (id<MTLDevice>)device {
-    return _device;
+    [self rebuildDisplayTexture];
+    [self rebuildDisplayVertices];
 }
 
 - (UTMScreenshot *)screenshot {
@@ -265,29 +401,16 @@ static void cs_channel_destroy(SpiceSession *s, SpiceChannel *channel, gpointer
     }
 }
 
-@synthesize drawLock = _drawLock;
-@synthesize texture = _texture;
-@synthesize numVertices = _numVertices;
-@synthesize vertices = _vertices;
-@synthesize viewportOrigin;
-@synthesize viewportScale;
+#pragma mark - Methods
 
-- (id)init {
-    self = [super init];
-    if (self) {
-        _drawLock = dispatch_semaphore_create(1);
-        self.viewportScale = 1.0f;
-        self.viewportOrigin = CGPointMake(0, 0);
-    }
-    return self;
-}
-
-- (id)initWithSession:(nonnull SpiceSession *)session channelID:(NSInteger)channelID monitorID:(NSInteger)monitorID {
-    self = [self init];
-    if (self) {
+- (instancetype)initWithSession:(nonnull SpiceSession *)session channelID:(NSInteger)channelID monitorID:(NSInteger)monitorID {
+    if (self = [super init]) {
         GList *list;
         GList *it;
         
+        self.drawLock = dispatch_semaphore_create(1);
+        self.viewportScale = 1.0f;
+        self.viewportOrigin = CGPointMake(0, 0);
         self.channelID = channelID;
         self.monitorID = monitorID;
         self.session = session;
@@ -314,7 +437,7 @@ static void cs_channel_destroy(SpiceSession *s, SpiceChannel *channel, gpointer
     return self;
 }
 
-- (id)initWithSession:(nonnull SpiceSession *)session channelID:(NSInteger)channelID {
+- (instancetype)initWithSession:(nonnull SpiceSession *)session channelID:(NSInteger)channelID {
     return [self initWithSession:session channelID:channelID monitorID:0];
 }
 
@@ -322,6 +445,9 @@ static void cs_channel_destroy(SpiceSession *s, SpiceChannel *channel, gpointer
     if (self.display) {
         cs_channel_destroy(self.session, SPICE_CHANNEL(self.display), (__bridge void *)self);
     }
+    if (_cursor) {
+        cs_channel_destroy(self.session, SPICE_CHANNEL(_cursor), (__bridge void *)self);
+    }
     UTMLog(@"%s:%d", __FUNCTION__, __LINE__);
     g_signal_handlers_disconnect_by_func(self.session, G_CALLBACK(cs_channel_new), GLIB_OBJC_RELEASE(self));
     g_signal_handlers_disconnect_by_func(self.session, G_CALLBACK(cs_channel_destroy), GLIB_OBJC_RELEASE(self));
@@ -339,12 +465,12 @@ static void cs_channel_destroy(SpiceSession *s, SpiceChannel *channel, gpointer
         _visibleArea = visible;
     }
     self.displaySize = _visibleArea.size;
-    [self rebuildTexture];
-    [self rebuildVertices];
+    [self rebuildDisplayTexture];
+    [self rebuildDisplayVertices];
 }
 
-- (void)rebuildTexture {
-    if (CGRectIsEmpty(_canvasArea) || !_device) {
+- (void)rebuildDisplayTexture {
+    if (CGRectIsEmpty(_canvasArea) || !self.device) {
         return;
     }
     MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc] init];
@@ -352,13 +478,13 @@ static void cs_channel_destroy(SpiceSession *s, SpiceChannel *channel, gpointer
     textureDescriptor.pixelFormat = (_canvasFormat == SPICE_SURFACE_FMT_32_xRGB) ? MTLPixelFormatBGRA8Unorm : (MTLPixelFormat)43;// FIXME: MTLPixelFormatBGR5A1Unorm is supposed to be available.
     textureDescriptor.width = _visibleArea.size.width;
     textureDescriptor.height = _visibleArea.size.height;
-    dispatch_semaphore_wait(_drawLock, DISPATCH_TIME_FOREVER);
-    _texture = [_device newTextureWithDescriptor:textureDescriptor];
-    dispatch_semaphore_signal(_drawLock);
+    dispatch_semaphore_wait(self.drawLock, DISPATCH_TIME_FOREVER);
+    self.displayTexture = [self.device newTextureWithDescriptor:textureDescriptor];
+    dispatch_semaphore_signal(self.drawLock);
     [self drawRegion:_visibleArea];
 }
 
-- (void)rebuildVertices {
+- (void)rebuildDisplayVertices {
     // We flip the y-coordinates because pixman renders flipped
     UTMVertex quadVertices[] =
     {
@@ -372,15 +498,15 @@ static void cs_channel_destroy(SpiceSession *s, SpiceChannel *channel, gpointer
         { {  _visibleArea.size.width/2,  -_visibleArea.size.height/2 },  { 1.f, 1.f } },
     };
     
-    dispatch_semaphore_wait(_drawLock, DISPATCH_TIME_FOREVER);
+    dispatch_semaphore_wait(self.drawLock, DISPATCH_TIME_FOREVER);
     // Create our vertex buffer, and initialize it with our quadVertices array
-    _vertices = [_device newBufferWithBytes:quadVertices
-                                     length:sizeof(quadVertices)
-                                    options:MTLResourceStorageModeShared];
+    self.displayVertices = [self.device newBufferWithBytes:quadVertices
+                                                    length:sizeof(quadVertices)
+                                                   options:MTLResourceStorageModeShared];
 
     // Calculate the number of vertices by dividing the byte length by the size of each vertex
-    _numVertices = sizeof(quadVertices) / sizeof(UTMVertex);
-    dispatch_semaphore_signal(_drawLock);
+    self.displayNumVertices = sizeof(quadVertices) / sizeof(UTMVertex);
+    dispatch_semaphore_signal(self.drawLock);
 }
 
 - (void)drawRegion:(CGRect)rect {
@@ -390,14 +516,14 @@ static void cs_channel_destroy(SpiceSession *s, SpiceChannel *channel, gpointer
         { rect.origin.x-_visibleArea.origin.x, rect.origin.y-_visibleArea.origin.y, 0 }, // MTLOrigin
         { rect.size.width, rect.size.height, 1} // MTLSize
     };
-    dispatch_semaphore_wait(_drawLock, DISPATCH_TIME_FOREVER);
+    dispatch_semaphore_wait(self.drawLock, DISPATCH_TIME_FOREVER);
     if (_canvasData != NULL) { // canvas may be destroyed by this time
-        [_texture replaceRegion:region
-                    mipmapLevel:0
-                      withBytes:(const char *)_canvasData + (NSUInteger)(rect.origin.y*_canvasStride + rect.origin.x*pixelSize)
-                    bytesPerRow:_canvasStride];
+        [self.displayTexture replaceRegion:region
+                               mipmapLevel:0
+                                 withBytes:(const char *)_canvasData + (NSUInteger)(rect.origin.y*_canvasStride + rect.origin.x*pixelSize)
+                               bytesPerRow:_canvasStride];
     }
-    dispatch_semaphore_signal(_drawLock);
+    dispatch_semaphore_signal(self.drawLock);
 }
 
 - (BOOL)visible {
@@ -420,4 +546,88 @@ static void cs_channel_destroy(SpiceSession *s, SpiceChannel *channel, gpointer
     spice_main_channel_send_monitor_config(self.main);
 }
 
+#pragma mark - Cursor drawing
+
+- (void)rebuildCursorWithSize:(CGSize)size center:(CGPoint)hotspot {
+    // hotspot is the offset in buffer for the center of the pointer
+    if (!self.device) {
+        UTMLog(@"MTL device not ready for cursor draw");
+        return;
+    }
+    dispatch_semaphore_wait(self.drawLock, DISPATCH_TIME_FOREVER);
+    MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc] init];
+    // don't worry that that components are reversed, we fix it in shaders
+    textureDescriptor.pixelFormat = MTLPixelFormatBGRA8Unorm;
+    textureDescriptor.width = size.width;
+    textureDescriptor.height = size.height;
+    self.cursorTexture = [self.device newTextureWithDescriptor:textureDescriptor];
+
+    // We flip the y-coordinates because pixman renders flipped
+    UTMVertex quadVertices[] =
+    {
+     // Pixel positions, Texture coordinates
+     { { -hotspot.x + size.width, hotspot.y               },  { 1.f, 0.f } },
+     { { -hotspot.x             , hotspot.y               },  { 0.f, 0.f } },
+     { { -hotspot.x             , hotspot.y - size.height },  { 0.f, 1.f } },
+     
+     { { -hotspot.x + size.width, hotspot.y               },  { 1.f, 0.f } },
+     { { -hotspot.x             , hotspot.y - size.height },  { 0.f, 1.f } },
+     { { -hotspot.x + size.width, hotspot.y - size.height },  { 1.f, 1.f } },
+    };
+
+    // Create our vertex buffer, and initialize it with our quadVertices array
+    self.cursorVertices = [self.device newBufferWithBytes:quadVertices
+                                                   length:sizeof(quadVertices)
+                                                  options:MTLResourceStorageModeShared];
+
+    // Calculate the number of vertices by dividing the byte length by the size of each vertex
+    self.cursorNumVertices = sizeof(quadVertices) / sizeof(UTMVertex);
+    self.cursorSize = size;
+    self.cursorHotspot = hotspot;
+    self.hasCursor = YES;
+    dispatch_semaphore_signal(self.drawLock);
+}
+
+- (void)destroyCursor {
+    dispatch_semaphore_wait(self.drawLock, DISPATCH_TIME_FOREVER);
+    self.cursorNumVertices = 0;
+    self.cursorVertices = nil;
+    self.cursorTexture = nil;
+    self.cursorSize = CGSizeZero;
+    self.cursorHotspot = CGPointZero;
+    self.hasCursor = NO;
+    dispatch_semaphore_signal(self.drawLock);
+}
+
+- (void)drawCursor:(const void *)buffer {
+    const NSInteger pixelSize = 4;
+    MTLRegion region = {
+        { 0, 0 }, // MTLOrigin
+        { self.cursorSize.width, self.cursorSize.height, 1} // MTLSize
+    };
+    dispatch_semaphore_wait(self.drawLock, DISPATCH_TIME_FOREVER);
+    [self.cursorTexture replaceRegion:region
+                          mipmapLevel:0
+                            withBytes:buffer
+                          bytesPerRow:self.cursorSize.width*pixelSize];
+    dispatch_semaphore_signal(self.drawLock);
+}
+
+- (BOOL)cursorVisible {
+    return !self.inhibitCursor && self.hasCursor && !self.cursorHidden;
+}
+
+- (CGPoint)cursorOrigin {
+    CGPoint point = _mouse_guest;
+    point.x -= self.displaySize.width/2;
+    point.y -= self.displaySize.height/2;
+    point.x *= self.viewportScale;
+    point.y *= self.viewportScale;
+    return point;
+}
+
+- (void)forceCursorPosition:(CGPoint)pos {
+    _mouse_guest = pos;
+}
+
 @end

+ 3 - 7
CocoaSpice/CSInput.h

@@ -40,17 +40,13 @@ typedef NS_ENUM(NSInteger, CSInputScroll) {
 
 NS_ASSUME_NONNULL_BEGIN
 
-@interface CSInput : NSObject <UTMRenderSource>
+@interface CSInput : NSObject
 
 @property (nonatomic, readonly, nullable) SpiceSession *session;
 @property (nonatomic, readonly, assign) NSInteger channelID;
 @property (nonatomic, readonly, assign) NSInteger monitorID;
 @property (nonatomic, readonly, assign) BOOL serverModeCursor;
-@property (nonatomic, readonly, assign) BOOL hasCursor;
 @property (nonatomic, assign) BOOL disableInputs;
-@property (nonatomic, readonly) CGSize cursorSize;
-@property (nonatomic, assign) CGSize displaySize;
-@property (nonatomic, assign) BOOL inhibitCursor;
 
 - (void)sendKey:(CSInputKey)type code:(int)scancode;
 - (void)sendPause:(CSInputKey)type;
@@ -60,9 +56,9 @@ NS_ASSUME_NONNULL_BEGIN
 - (void)sendMouseScroll:(CSInputScroll)type button:(CSInputButton)button dy:(CGFloat)dy;
 - (void)sendMouseButton:(CSInputButton)button pressed:(BOOL)pressed point:(CGPoint)point;
 - (void)requestMouseMode:(BOOL)server;
-- (void)forceCursorPosition:(CGPoint)pos;
 
-- (id)initWithSession:(nonnull SpiceSession *)session channelID:(NSInteger)channelID monitorID:(NSInteger)monitorID;
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithSession:(nonnull SpiceSession *)session channelID:(NSInteger)channelID monitorID:(NSInteger)monitorID NS_DESIGNATED_INITIALIZER;
 
 @end
 

+ 10 - 244
CocoaSpice/CSInput.m

@@ -27,115 +27,18 @@
 @property (nonatomic, readwrite, nullable) SpiceSession *session;
 @property (nonatomic, readwrite, assign) NSInteger channelID;
 @property (nonatomic, readwrite, assign) NSInteger monitorID;
-@property (nonatomic, readwrite, assign) BOOL serverModeCursor;
-@property (nonatomic, readwrite, assign) BOOL hasCursor;
-@property (nonatomic, readwrite) CGSize cursorSize;
-@property (nonatomic, readwrite) CGPoint cursorHotspot;
 @property (nonatomic, nullable) SpiceMainChannel *main;
 @property (nonatomic, nullable) SpiceInputsChannel *inputs;
 
 @end
 
 @implementation CSInput {
-    SpiceCursorChannel      *_cursor;
-    
-    CGPoint                 _mouse_guest;
     CGFloat                 _scroll_delta_y;
     
     uint32_t                _key_state[512 / 32];
-    
-    // Drawing cursor
-    id<MTLDevice>           _device;
-    id<MTLTexture>          _texture;
-    id<MTLBuffer>           _vertices;
-    NSUInteger              _numVertices;
-    dispatch_semaphore_t    _drawLock;
-    BOOL                    _cursorHidden;
-}
-
-#pragma mark - glib events
-
-static void cs_update_mouse_mode(SpiceChannel *channel, gpointer data)
-{
-    CSInput *self = (__bridge CSInput *)data;
-    enum SpiceMouseMode mouse_mode;
-    
-    g_object_get(channel, "mouse-mode", &mouse_mode, NULL);
-    DISPLAY_DEBUG(self, "mouse mode %u", mouse_mode);
-    
-    self.serverModeCursor = (mouse_mode == SPICE_MOUSE_MODE_SERVER);
-    
-    if (self.serverModeCursor) {
-        self->_mouse_guest.x = -1;
-        self->_mouse_guest.y = -1;
-    }
-}
-
-static void cs_cursor_invalidate(CSInput *self)
-{
-    // We implement two different textures so invalidate is not needed
-}
-
-static void cs_cursor_set(SpiceCursorChannel *channel,
-                          G_GNUC_UNUSED GParamSpec *pspec,
-                          gpointer data)
-{
-    CSInput *self = (__bridge CSInput *)data;
-    SpiceCursorShape *cursor_shape;
-    
-    g_object_get(G_OBJECT(channel), "cursor", &cursor_shape, NULL);
-    if (G_UNLIKELY(cursor_shape == NULL || cursor_shape->data == NULL)) {
-        if (cursor_shape != NULL) {
-            g_boxed_free(SPICE_TYPE_CURSOR_SHAPE, cursor_shape);
-        }
-        return;
-    }
-    
-    cs_cursor_invalidate(self);
-    
-    CGPoint hotspot = CGPointMake(cursor_shape->hot_spot_x, cursor_shape->hot_spot_y);
-    CGSize newSize = CGSizeMake(cursor_shape->width, cursor_shape->height);
-    if (!CGSizeEqualToSize(newSize, self.cursorSize) || !CGPointEqualToPoint(hotspot, self.cursorHotspot)) {
-        [self rebuildTexture:newSize center:hotspot];
-    }
-    [self drawCursor:cursor_shape->data];
-    self->_cursorHidden = NO;
-    cs_cursor_invalidate(self);
 }
 
-static void cs_cursor_move(SpiceCursorChannel *channel, gint x, gint y, gpointer data)
-{
-    CSInput *self = (__bridge CSInput *)data;
-    
-    cs_cursor_invalidate(self); // old pointer buffer
-    
-    self->_mouse_guest.x = x;
-    self->_mouse_guest.y = y;
-    
-    cs_cursor_invalidate(self); // new pointer buffer
-    
-    /* apparently we have to restore cursor when "cursor_move" */
-    if (self.hasCursor) {
-        self->_cursorHidden = NO;
-    }
-}
-
-static void cs_cursor_hide(SpiceCursorChannel *channel, gpointer data)
-{
-    CSInput *self = (__bridge CSInput *)data;
-    
-    self->_cursorHidden = YES;
-    cs_cursor_invalidate(self);
-}
-
-static void cs_cursor_reset(SpiceCursorChannel *channel, gpointer data)
-{
-    CSInput *self = (__bridge CSInput *)data;
-    
-    DISPLAY_DEBUG(self, "%s",  __FUNCTION__);
-    [self destroyTexture];
-    cs_cursor_invalidate(self);
-}
+#pragma mark - Channel events
 
 static void cs_channel_new(SpiceSession *s, SpiceChannel *channel, gpointer data)
 {
@@ -145,32 +48,6 @@ static void cs_channel_new(SpiceSession *s, SpiceChannel *channel, gpointer data
     g_object_get(channel, "channel-id", &chid, NULL);
     if (SPICE_IS_MAIN_CHANNEL(channel)) {
         self.main = SPICE_MAIN_CHANNEL(channel);
-        g_signal_connect(channel, "main-mouse-update",
-                         G_CALLBACK(cs_update_mouse_mode), GLIB_OBJC_RETAIN(self));
-        cs_update_mouse_mode(channel, data);
-        return;
-    }
-    
-    if (SPICE_IS_CURSOR_CHANNEL(channel)) {
-        gpointer cursor_shape;
-        if (chid != self.channelID)
-            return;
-        self->_cursor = SPICE_CURSOR_CHANNEL(channel);
-        g_signal_connect(channel, "notify::cursor",
-                         G_CALLBACK(cs_cursor_set), GLIB_OBJC_RETAIN(self));
-        g_signal_connect(channel, "cursor-move",
-                         G_CALLBACK(cs_cursor_move), GLIB_OBJC_RETAIN(self));
-        g_signal_connect(channel, "cursor-hide",
-                         G_CALLBACK(cs_cursor_hide), GLIB_OBJC_RETAIN(self));
-        g_signal_connect(channel, "cursor-reset",
-                         G_CALLBACK(cs_cursor_reset), GLIB_OBJC_RETAIN(self));
-        spice_channel_connect(channel);
-        
-        g_object_get(G_OBJECT(channel), "cursor", &cursor_shape, NULL);
-        if (cursor_shape != NULL) {
-            g_boxed_free(SPICE_TYPE_CURSOR_SHAPE, cursor_shape);
-            cs_cursor_set(self->_cursor, NULL, (__bridge void *)self);
-        }
         return;
     }
     
@@ -189,22 +66,8 @@ static void cs_channel_destroy(SpiceSession *s, SpiceChannel *channel, gpointer
     g_object_get(channel, "channel-id", &chid, NULL);
     DISPLAY_DEBUG(self, "channel_destroy %d", chid);
     
-    [self destroyTexture];
-    
     if (SPICE_IS_MAIN_CHANNEL(channel)) {
         self.main = NULL;
-        g_signal_handlers_disconnect_by_func(channel, G_CALLBACK(cs_update_mouse_mode), GLIB_OBJC_RELEASE(self));
-        return;
-    }
-    
-    if (SPICE_IS_CURSOR_CHANNEL(channel)) {
-        if (chid != self.channelID)
-            return;
-        self->_cursor = NULL;
-        g_signal_handlers_disconnect_by_func(channel, G_CALLBACK(cs_cursor_set), GLIB_OBJC_RELEASE(self));
-        g_signal_handlers_disconnect_by_func(channel, G_CALLBACK(cs_cursor_move), GLIB_OBJC_RELEASE(self));
-        g_signal_handlers_disconnect_by_func(channel, G_CALLBACK(cs_cursor_hide), GLIB_OBJC_RELEASE(self));
-        g_signal_handlers_disconnect_by_func(channel, G_CALLBACK(cs_cursor_reset), GLIB_OBJC_RELEASE(self));
         return;
     }
     
@@ -216,6 +79,14 @@ static void cs_channel_destroy(SpiceSession *s, SpiceChannel *channel, gpointer
     return;
 }
 
+#pragma mark - Properties
+
+- (BOOL)serverModeCursor {
+    enum SpiceMouseMode mouse_mode;
+    g_object_get(self.main, "mouse-mode", &mouse_mode, NULL);
+    return (mouse_mode == SPICE_MOUSE_MODE_SERVER);
+}
+
 #pragma mark - Key handling
 
 - (void)sendPause:(CSInputKey)type {
@@ -405,24 +276,10 @@ static int cs_button_to_spice(CSInputButton button)
     }
 }
 
-- (void)forceCursorPosition:(CGPoint)pos {
-    _mouse_guest = pos;
-}
-
 #pragma mark - Initializers
 
-- (id)init {
+- (instancetype)initWithSession:(nonnull SpiceSession *)session channelID:(NSInteger)channelID monitorID:(NSInteger)monitorID {
     self = [super init];
-    if (self) {
-        _drawLock = dispatch_semaphore_create(1);
-        self.viewportScale = 1.0f;
-        self.viewportOrigin = CGPointMake(0, 0);
-    }
-    return self;
-}
-
-- (id)initWithSession:(nonnull SpiceSession *)session channelID:(NSInteger)channelID monitorID:(NSInteger)monitorID {
-    self = [self init];
     if (self) {
         GList *list;
         GList *it;
@@ -454,9 +311,6 @@ static int cs_button_to_spice(CSInputButton button)
 }
 
 - (void)dealloc {
-    if (_cursor) {
-        cs_channel_destroy(self.session, SPICE_CHANNEL(_cursor), (__bridge void *)self);
-    }
     if (self.main) {
         cs_channel_destroy(self.session, SPICE_CHANNEL(self.main), (__bridge void *)self);
     }
@@ -467,92 +321,4 @@ static int cs_button_to_spice(CSInputButton button)
     self.session = NULL;
 }
 
-#pragma mark - Drawing Cursor
-
-@synthesize device = _device;
-@synthesize drawLock = _drawLock;
-@synthesize texture = _texture;
-@synthesize numVertices = _numVertices;
-@synthesize vertices = _vertices;
-@synthesize viewportOrigin;
-@synthesize viewportScale;
-
-- (void)rebuildTexture:(CGSize)size center:(CGPoint)hotspot {
-    // hotspot is the offset in buffer for the center of the pointer
-    if (!_device) {
-        UTMLog(@"MTL device not ready for cursor draw");
-        return;
-    }
-    dispatch_semaphore_wait(_drawLock, DISPATCH_TIME_FOREVER);
-    MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc] init];
-    // don't worry that that components are reversed, we fix it in shaders
-    textureDescriptor.pixelFormat = MTLPixelFormatBGRA8Unorm;
-    textureDescriptor.width = size.width;
-    textureDescriptor.height = size.height;
-    _texture = [_device newTextureWithDescriptor:textureDescriptor];
-
-    // We flip the y-coordinates because pixman renders flipped
-    UTMVertex quadVertices[] =
-    {
-     // Pixel positions, Texture coordinates
-     { { -hotspot.x + size.width, hotspot.y               },  { 1.f, 0.f } },
-     { { -hotspot.x             , hotspot.y               },  { 0.f, 0.f } },
-     { { -hotspot.x             , hotspot.y - size.height },  { 0.f, 1.f } },
-     
-     { { -hotspot.x + size.width, hotspot.y               },  { 1.f, 0.f } },
-     { { -hotspot.x             , hotspot.y - size.height },  { 0.f, 1.f } },
-     { { -hotspot.x + size.width, hotspot.y - size.height },  { 1.f, 1.f } },
-    };
-
-    // Create our vertex buffer, and initialize it with our quadVertices array
-    _vertices = [_device newBufferWithBytes:quadVertices
-                                    length:sizeof(quadVertices)
-                                   options:MTLResourceStorageModeShared];
-
-    // Calculate the number of vertices by dividing the byte length by the size of each vertex
-    _numVertices = sizeof(quadVertices) / sizeof(UTMVertex);
-    self.cursorSize = size;
-    self.cursorHotspot = hotspot;
-    self.hasCursor = YES;
-    dispatch_semaphore_signal(_drawLock);
-}
-
-- (void)destroyTexture {
-    dispatch_semaphore_wait(_drawLock, DISPATCH_TIME_FOREVER);
-    _numVertices = 0;
-    _vertices = nil;
-    _texture = nil;
-    self.cursorSize = CGSizeZero;
-    self.cursorHotspot = CGPointZero;
-    self.hasCursor = NO;
-    dispatch_semaphore_signal(_drawLock);
-}
-
-- (void)drawCursor:(const void *)buffer {
-    const NSInteger pixelSize = 4;
-    MTLRegion region = {
-        { 0, 0 }, // MTLOrigin
-        { self.cursorSize.width, self.cursorSize.height, 1} // MTLSize
-    };
-    dispatch_semaphore_wait(_drawLock, DISPATCH_TIME_FOREVER);
-    [_texture replaceRegion:region
-                mipmapLevel:0
-                  withBytes:buffer
-                bytesPerRow:self.cursorSize.width*pixelSize];
-    dispatch_semaphore_signal(_drawLock);
-}
-
-- (BOOL)visible {
-    return !self.inhibitCursor && self.hasCursor && !_cursorHidden;
-}
-
-- (CGPoint)viewportOrigin {
-    CGPoint point = _mouse_guest;
-    point.x -= self.displaySize.width/2;
-    point.y -= self.displaySize.height/2;
-    point.x *= self.viewportScale;
-    point.y *= self.viewportScale;
-    return point;
-}
-
 @end

+ 0 - 20
Managers/UTMSpiceIO.m

@@ -39,7 +39,6 @@ typedef void (^connectionCallback_t)(BOOL success, NSString * _Nullable msg);
 @property (nonatomic, nullable) CSSession *session;
 @property (nonatomic, nullable, copy) NSURL *sharedDirectory;
 @property (nonatomic) NSInteger port;
-@property (nonatomic) BOOL hasObservers;
 @property (nonatomic) BOOL dynamicResolutionSupported;
 
 @end
@@ -85,15 +84,6 @@ typedef void (^connectionCallback_t)(BOOL success, NSString * _Nullable msg);
     }
 }
 
-- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
-    // make sure the CSDisplay properties are synced with the CSInput
-    if ([keyPath isEqualToString:@"primaryDisplay.viewportScale"]) {
-        self.primaryInput.viewportScale = self.primaryDisplay.viewportScale;
-    } else if ([keyPath isEqualToString:@"primaryDisplay.displaySize"]) {
-        self.primaryInput.displaySize = self.primaryDisplay.displaySize;
-    }
-}
-
 #pragma mark - UTMInputOutput
 
 - (BOOL)startWithError:(NSError **)err {
@@ -141,11 +131,6 @@ typedef void (^connectionCallback_t)(BOOL success, NSString * _Nullable msg);
 
 - (void)disconnect {
     @synchronized (self) {
-        if (self.hasObservers) {
-            [self removeObserver:self forKeyPath:@"primaryDisplay.viewportScale"];
-            [self removeObserver:self forKeyPath:@"primaryDisplay.displaySize"];
-            self.hasObservers = NO;
-        }
         [self.spiceConnection disconnect];
         self.spiceConnection.delegate = nil;
         self.spiceConnection = nil;
@@ -207,11 +192,6 @@ typedef void (^connectionCallback_t)(BOOL success, NSString * _Nullable msg);
         _primaryInput = input;
         _delegate.vmDisplay = display;
         _delegate.vmInput = input;
-        @synchronized (self) {
-            [self addObserver:self forKeyPath:@"primaryDisplay.viewportScale" options:0 context:nil];
-            [self addObserver:self forKeyPath:@"primaryDisplay.displaySize" options:0 context:nil];
-            self.hasObservers = YES;
-        }
         if (self.connectionCallback) {
             self.connectionCallback(YES, nil);
             self.connectionCallback = nil;

+ 3 - 2
Platform/iOS/Display/VMCursor.m

@@ -16,6 +16,7 @@
 
 #import "VMCursor.h"
 #import "VMDisplayMetalViewController+Touch.h"
+#import "CSDisplayMetal.h"
 
 @implementation VMCursor {
     CGPoint _start;
@@ -46,8 +47,8 @@
 
 - (CGRect)bounds {
     CGRect bounds = CGRectZero;
-    bounds.size.width = MAX(1, _controller.vmInput.cursorSize.width);
-    bounds.size.height = MAX(1, _controller.vmInput.cursorSize.height);
+    bounds.size.width = MAX(1, _controller.vmDisplay.cursorSize.width);
+    bounds.size.height = MAX(1, _controller.vmDisplay.cursorSize.height);
     return bounds;
 }
 

+ 2 - 2
Platform/iOS/Display/VMDisplayMetalViewController+Touch.m

@@ -338,7 +338,7 @@ static CGFloat CGPointToPixel(CGFloat point) {
     translated = [self clipCursorToDisplay:translated];
     if (!self.vmInput.serverModeCursor) {
         [self.vmInput sendMouseMotion:self.mouseButtonDown point:translated];
-        [self.vmInput forceCursorPosition:translated]; // required to show cursor on screen
+        [self.vmDisplay forceCursorPosition:translated]; // required to show cursor on screen
     } else {
         UTMLog(@"Warning: ignored mouse set (%f, %f) while mouse is in server mode", translated.x, translated.y);
     }
@@ -571,7 +571,7 @@ static CGFloat CGPointToPixel(CGFloat point) {
 - (BOOL)switchMouseType:(VMMouseType)type {
     BOOL shouldHideCursor = (type == VMMouseTypeAbsoluteHideCursor);
     BOOL shouldUseServerMouse = (type == VMMouseTypeRelative);
-    self.vmInput.inhibitCursor = shouldHideCursor;
+    self.vmDisplay.inhibitCursor = shouldHideCursor;
     if (shouldUseServerMouse != self.vmInput.serverModeCursor) {
         UTMLog(@"Switching mouse mode to server:%d for type:%ld", shouldUseServerMouse, type);
         [self.vm requestInputTablet:!shouldUseServerMouse];

+ 2 - 4
Platform/iOS/Display/VMDisplayMetalViewController.m

@@ -63,8 +63,7 @@
     
     // Initialize our renderer with the view size
     [_renderer mtkView:self.mtkView drawableSizeWillChange:self.mtkView.drawableSize];
-    _renderer.sourceScreen = self.vmDisplay;
-    _renderer.sourceCursor = self.vmInput;
+    _renderer.source = self.vmDisplay;
     
     [_renderer changeUpscaler:self.vmConfiguration.displayUpscalerValue
                    downscaler:self.vmConfiguration.displayDownscalerValue];
@@ -121,8 +120,7 @@
                 self.placeholderImageView.hidden = YES;
                 self.mtkView.hidden = NO;
             } completion:nil];
-            self->_renderer.sourceScreen = self.vmDisplay;
-            self->_renderer.sourceCursor = self.vmInput;
+            self->_renderer.source = self.vmDisplay;
             [self displayResize:self.view.bounds.size];
             if (self.vmConfiguration.shareClipboardEnabled) {
                 [[UTMPasteboard generalPasteboard] requestPollingModeForObject:self];

+ 2 - 3
Platform/macOS/Display/VMDisplayMetalWindowController.swift

@@ -84,8 +84,7 @@ class VMDisplayMetalWindowController: VMDisplayWindowController, UTMSpiceIODeleg
     override func enterLive() {
         metalView.isHidden = false
         screenshotView.isHidden = true
-        renderer!.sourceScreen = vmDisplay
-        renderer!.sourceCursor = vmInput
+        renderer!.source = vmDisplay
         displaySizeObserver = observe(\.vmDisplay!.displaySize, options: [.initial, .new]) { (_, change) in
             guard let size = change.newValue else { return }
             self.displaySizeDidChange(size: size)
@@ -282,7 +281,7 @@ extension VMDisplayMetalWindowController: VMMetalViewInputDelegate {
         let point = CGPoint(x: newX, y: newY)
         logger.debug("move cursor: cocoa (\(absolutePoint.x), \(absolutePoint.y)), native (\(newX), \(newY))")
         vmInput?.sendMouseMotion(button, point: point)
-        vmInput?.forceCursorPosition(point) // required to show cursor on screen
+        vmDisplay?.forceCursorPosition(point) // required to show cursor on screen
     }
     
     func mouseMove(relativePoint: CGPoint, button: CSInputButton) {

+ 11 - 7
Renderer/UTMRenderSource.h

@@ -21,14 +21,18 @@ NS_ASSUME_NONNULL_BEGIN
 
 @protocol UTMRenderSource <NSObject>
 
-@property (nonatomic, readonly) BOOL visible;
-@property (nonatomic, assign) CGPoint viewportOrigin;
-@property (nonatomic, assign) CGFloat viewportScale;
+@property (nonatomic, readonly) BOOL cursorVisible;
+@property (nonatomic) CGPoint cursorOrigin;
+@property (nonatomic) CGPoint viewportOrigin;
+@property (nonatomic) CGFloat viewportScale;
 @property (nonatomic, readonly) dispatch_semaphore_t drawLock;
-@property (nonatomic, nullable, strong) id<MTLDevice> device;
-@property (nonatomic, nullable, readonly) id<MTLTexture> texture;
-@property (nonatomic, readonly) NSUInteger numVertices;
-@property (nonatomic, nullable, readonly) id<MTLBuffer> vertices;
+@property (nonatomic, nullable) id<MTLDevice> device;
+@property (nonatomic, nullable, readonly) id<MTLTexture> displayTexture;
+@property (nonatomic, nullable, readonly) id<MTLTexture> cursorTexture;
+@property (nonatomic, readonly) NSUInteger displayNumVertices;
+@property (nonatomic, readonly) NSUInteger cursorNumVertices;
+@property (nonatomic, nullable, readonly) id<MTLBuffer> displayVertices;
+@property (nonatomic, nullable, readonly) id<MTLBuffer> cursorVertices;
 
 @end
 

+ 1 - 2
Renderer/UTMRenderer.h

@@ -14,8 +14,7 @@ NS_ASSUME_NONNULL_BEGIN
 // Our platform independent renderer class
 @interface UTMRenderer : NSObject<MTKViewDelegate>
 
-@property (nonatomic, weak, nullable) id<UTMRenderSource> sourceScreen;
-@property (nonatomic, weak, nullable) id<UTMRenderSource> sourceCursor;
+@property (nonatomic, weak, nullable) id<UTMRenderSource> source;
 
 - (nonnull instancetype)initWithMetalKitView:(nonnull MTKView *)mtkView;
 - (void)changeUpscaler:(MTLSamplerMinMagFilter)upscaler downscaler:(MTLSamplerMinMagFilter)downscaler;

+ 73 - 97
Renderer/UTMRenderer.m

@@ -34,14 +34,9 @@ Implementation of renderer class which performs Metal setup and per frame render
     id<MTLSamplerState> _sampler;
 }
 
-- (void)setSourceScreen:(id<UTMRenderSource>)source {
+- (void)setSource:(id<UTMRenderSource>)source {
     source.device = _device;
-    _sourceScreen = source;
-}
-
-- (void)setSourceCursor:(id<UTMRenderSource>)source {
-    source.device = _device;
-    _sourceCursor = source;
+    _source = source;
 }
 
 /// Initialize with the MetalKit view from which we'll obtain our Metal device
@@ -154,7 +149,8 @@ static matrix_float4x4 matrix_scale_translate(CGFloat scale, CGPoint translate)
 /// Called whenever the view needs to render a frame
 - (void)drawInMTKView:(nonnull MTKView *)view
 {
-    if (view.hidden) {
+    id<UTMRenderSource> source = self.source;
+    if (view.hidden || !source) {
         return;
     }
 
@@ -167,96 +163,81 @@ static matrix_float4x4 matrix_scale_translate(CGFloat scale, CGPoint translate)
 
     if(renderPassDescriptor != nil)
     {
-        BOOL screenDrawn = NO;
-        __weak dispatch_semaphore_t screenLock = nil;
-        __weak dispatch_semaphore_t cursorLock = nil;
-
         // Create a render command encoder so we can render into something
         id<MTLRenderCommandEncoder> renderEncoder =
         [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
         renderEncoder.label = @"MyRenderEncoder";
         
-        if (self.sourceScreen) {
-            // Lock screen updates
-            bool hasAlpha = NO;
-            screenLock = self.sourceScreen.drawLock;
-            dispatch_semaphore_wait(screenLock, DISPATCH_TIME_FOREVER);
-            
-            if (self.sourceScreen.visible) {
-                // Render the screen first
-                
-                matrix_float4x4 transform = matrix_scale_translate(self.sourceScreen.viewportScale,
-                                                                   self.sourceScreen.viewportOrigin);
-
-                [renderEncoder setRenderPipelineState:_pipelineState];
-
-                [renderEncoder setVertexBuffer:self.sourceScreen.vertices
-                                        offset:0
-                                      atIndex:UTMVertexInputIndexVertices];
-
-                [renderEncoder setVertexBytes:&_viewportSize
-                                       length:sizeof(_viewportSize)
-                                      atIndex:UTMVertexInputIndexViewportSize];
-
-                [renderEncoder setVertexBytes:&transform
-                                       length:sizeof(transform)
-                                      atIndex:UTMVertexInputIndexTransform];
-
-                [renderEncoder setVertexBytes:&hasAlpha
-                                       length:sizeof(hasAlpha)
-                                      atIndex:UTMVertexInputIndexHasAlpha];
-
-                // Set the texture object.  The UTMTextureIndexBaseColor enum value corresponds
-                ///  to the 'colorMap' argument in our 'samplingShader' function because its
-                //   texture attribute qualifier also uses UTMTextureIndexBaseColor for its index
-                [renderEncoder setFragmentTexture:self.sourceScreen.texture
-                                          atIndex:UTMTextureIndexBaseColor];
-                
-                [renderEncoder setFragmentSamplerState:_sampler
-                                               atIndex:UTMSamplerIndexTexture];
-
-                // Draw the vertices of our triangles
-                [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
-                                  vertexStart:0
-                                  vertexCount:self.sourceScreen.numVertices];
-                
-                screenDrawn = YES;
-            }
-        }
+        // Lock screen updates
+        dispatch_semaphore_t drawLock = source.drawLock;
+        dispatch_semaphore_wait(drawLock, DISPATCH_TIME_FOREVER);
+        
+        // Render the screen first
+        
+        bool hasAlpha = NO;
+        matrix_float4x4 transform = matrix_scale_translate(source.viewportScale,
+                                                           source.viewportOrigin);
+
+        [renderEncoder setRenderPipelineState:_pipelineState];
 
-        if (screenDrawn && self.sourceCursor) {
-            // Lock cursor updates
+        [renderEncoder setVertexBuffer:source.displayVertices
+                                offset:0
+                              atIndex:UTMVertexInputIndexVertices];
+
+        [renderEncoder setVertexBytes:&_viewportSize
+                               length:sizeof(_viewportSize)
+                              atIndex:UTMVertexInputIndexViewportSize];
+
+        [renderEncoder setVertexBytes:&transform
+                               length:sizeof(transform)
+                              atIndex:UTMVertexInputIndexTransform];
+
+        [renderEncoder setVertexBytes:&hasAlpha
+                               length:sizeof(hasAlpha)
+                              atIndex:UTMVertexInputIndexHasAlpha];
+
+        // Set the texture object.  The UTMTextureIndexBaseColor enum value corresponds
+        ///  to the 'colorMap' argument in our 'samplingShader' function because its
+        //   texture attribute qualifier also uses UTMTextureIndexBaseColor for its index
+        [renderEncoder setFragmentTexture:source.displayTexture
+                                  atIndex:UTMTextureIndexBaseColor];
+        
+        [renderEncoder setFragmentSamplerState:_sampler
+                                       atIndex:UTMSamplerIndexTexture];
+
+        // Draw the vertices of our triangles
+        [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
+                          vertexStart:0
+                          vertexCount:source.displayNumVertices];
+        
+        // Draw cursor
+        if (source.cursorVisible) {
+            // Next render the cursor
             bool hasAlpha = YES;
-            cursorLock = self.sourceCursor.drawLock;
-            dispatch_semaphore_wait(cursorLock, DISPATCH_TIME_FOREVER);
-            
-            if (self.sourceCursor.visible) {
-                // Next render the cursor
-                matrix_float4x4 transform = matrix_scale_translate(self.sourceScreen.viewportScale,
-                                                                   CGPointMake(self.sourceScreen.viewportOrigin.x +
-                                                                               self.sourceCursor.viewportOrigin.x,
-                                                                               self.sourceScreen.viewportOrigin.y +
-                                                                               self.sourceCursor.viewportOrigin.y));
-                [renderEncoder setVertexBuffer:self.sourceCursor.vertices
-                                        offset:0
-                                      atIndex:UTMVertexInputIndexVertices];
-                [renderEncoder setVertexBytes:&_viewportSize
-                                       length:sizeof(_viewportSize)
-                                      atIndex:UTMVertexInputIndexViewportSize];
-                [renderEncoder setVertexBytes:&transform
-                                     length:sizeof(transform)
-                                    atIndex:UTMVertexInputIndexTransform];
-                [renderEncoder setVertexBytes:&hasAlpha
-                                     length:sizeof(hasAlpha)
-                                    atIndex:UTMVertexInputIndexHasAlpha];
-                [renderEncoder setFragmentTexture:self.sourceCursor.texture
-                                          atIndex:UTMTextureIndexBaseColor];
-                [renderEncoder setFragmentSamplerState:_sampler
-                                               atIndex:UTMSamplerIndexTexture];
-                [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
-                                  vertexStart:0
-                                  vertexCount:self.sourceCursor.numVertices];
-            }
+            matrix_float4x4 transform = matrix_scale_translate(source.viewportScale,
+                                                               CGPointMake(source.viewportOrigin.x +
+                                                                           source.cursorOrigin.x,
+                                                                           source.viewportOrigin.y +
+                                                                           source.cursorOrigin.y));
+            [renderEncoder setVertexBuffer:source.cursorVertices
+                                    offset:0
+                                  atIndex:UTMVertexInputIndexVertices];
+            [renderEncoder setVertexBytes:&_viewportSize
+                                   length:sizeof(_viewportSize)
+                                  atIndex:UTMVertexInputIndexViewportSize];
+            [renderEncoder setVertexBytes:&transform
+                                 length:sizeof(transform)
+                                atIndex:UTMVertexInputIndexTransform];
+            [renderEncoder setVertexBytes:&hasAlpha
+                                 length:sizeof(hasAlpha)
+                                atIndex:UTMVertexInputIndexHasAlpha];
+            [renderEncoder setFragmentTexture:source.cursorTexture
+                                      atIndex:UTMTextureIndexBaseColor];
+            [renderEncoder setFragmentSamplerState:_sampler
+                                           atIndex:UTMSamplerIndexTexture];
+            [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
+                              vertexStart:0
+                              vertexCount:source.cursorNumVertices];
         }
 
         [renderEncoder endEncoding];
@@ -268,12 +249,7 @@ static matrix_float4x4 matrix_scale_translate(CGFloat scale, CGPoint translate)
         [commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> commandBuffer) {
             // GPU work is complete
             // Signal the semaphore to start the CPU work
-            if (screenLock) {
-                dispatch_semaphore_signal(screenLock);
-            }
-            if (cursorLock) {
-                dispatch_semaphore_signal(cursorLock);
-            }
+            dispatch_semaphore_signal(drawLock);
         }];
     }