Commit 990ebba55a1fad88f6b03fe58d518d1ce33a5c2d

Alex Szpakowski 2018-01-04T19:29:33

metal: Implement fast hardware clearing when possible, by deferring the start of a render pass until a clear or draw operation happens.

diff --git a/src/render/metal/SDL_render_metal.m b/src/render/metal/SDL_render_metal.m
index 61d6bde..335ee57 100644
--- a/src/render/metal/SDL_render_metal.m
+++ b/src/render/metal/SDL_render_metal.m
@@ -147,7 +147,6 @@ typedef struct METAL_PipelineCache
 } METAL_PipelineCache;
 
 @interface METAL_RenderData : NSObject
-    @property (nonatomic, assign) BOOL beginScene;
     @property (nonatomic, retain) id<MTLDevice> mtldevice;
     @property (nonatomic, retain) id<MTLCommandQueue> mtlcmdqueue;
     @property (nonatomic, retain) id<MTLCommandBuffer> mtlcmdbuffer;
@@ -404,7 +403,6 @@ METAL_CreateRenderer(SDL_Window * window, Uint32 flags)
     }
 
     data = [[METAL_RenderData alloc] init];
-    data.beginScene = YES;
 
     renderer->driverdata = (void*)CFBridgingRetain(data);
     renderer->window = window;
@@ -562,21 +560,50 @@ METAL_CreateRenderer(SDL_Window * window, Uint32 flags)
 }
 
 static void
-METAL_ActivateRenderer(SDL_Renderer * renderer)
+METAL_ActivateRenderCommandEncoder(SDL_Renderer * renderer, MTLLoadAction load)
 {
     METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
 
-    if (data.beginScene) {
-        data.beginScene = NO;
-        data.mtlbackbuffer = [data.mtllayer nextDrawable];
-        SDL_assert(data.mtlbackbuffer);
-        data.mtlpassdesc.colorAttachments[0].texture = data.mtlbackbuffer.texture;
-        data.mtlpassdesc.colorAttachments[0].loadAction = MTLLoadActionDontCare;
+    /* Our SetRenderTarget just signals that the next render operation should
+     * set up a new render pass. This is where that work happens. */
+    if (data.mtlcmdencoder == nil) {
+        id<MTLTexture> mtltexture = nil;
+
+        if (renderer->target != NULL) {
+            METAL_TextureData *texdata = (__bridge METAL_TextureData *)renderer->target->driverdata;
+            mtltexture = texdata.mtltexture;
+        } else {
+            if (data.mtlbackbuffer == nil) {
+                /* The backbuffer's contents aren't guaranteed to persist after
+                 * presenting, so we can leave it undefined when loading it. */
+                data.mtlbackbuffer = [data.mtllayer nextDrawable];
+                if (load == MTLLoadActionLoad) {
+                    load = MTLLoadActionDontCare;
+                }
+            }
+            mtltexture = data.mtlbackbuffer.texture;
+        }
+
+        SDL_assert(mtltexture);
+
+        if (load == MTLLoadActionClear) {
+            MTLClearColor color = MTLClearColorMake(renderer->r/255.0, renderer->g/255.0, renderer->b/255.0, renderer->a/255.0);
+            data.mtlpassdesc.colorAttachments[0].clearColor = color;
+        }
+
+        data.mtlpassdesc.colorAttachments[0].loadAction = load;
+        data.mtlpassdesc.colorAttachments[0].texture = mtltexture;
+
         data.mtlcmdbuffer = [data.mtlcmdqueue commandBuffer];
         data.mtlcmdencoder = [data.mtlcmdbuffer renderCommandEncoderWithDescriptor:data.mtlpassdesc];
-        data.mtlcmdencoder.label = @"SDL metal renderer start of frame";
 
-        // Set up our current renderer state for the next frame...
+        if (data.mtlbackbuffer != nil && mtltexture == data.mtlbackbuffer.texture) {
+            data.mtlcmdencoder.label = @"SDL metal renderer backbuffer";
+        } else {
+            data.mtlcmdencoder.label = @"SDL metal renderer render target";
+        }
+
+        /* Make sure the viewport and clip rect are set on the new render pass. */
         METAL_UpdateViewport(renderer);
         METAL_UpdateClipRect(renderer);
     }
@@ -715,24 +742,21 @@ METAL_UnlockTexture(SDL_Renderer * renderer, SDL_Texture * texture)
 static int
 METAL_SetRenderTarget(SDL_Renderer * renderer, SDL_Texture * texture)
 { @autoreleasepool {
-    METAL_ActivateRenderer(renderer);
     METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
 
-    // commit the current command buffer, so that any work on a render target
-    //  will be available to the next one we're about to queue up.
-    [data.mtlcmdencoder endEncoding];
-    [data.mtlcmdbuffer commit];
-
-    id<MTLTexture> mtltexture = texture ? ((__bridge METAL_TextureData *)texture->driverdata).mtltexture : data.mtlbackbuffer.texture;
-    data.mtlpassdesc.colorAttachments[0].texture = mtltexture;
-    // !!! FIXME: this can be MTLLoadActionDontCare for textures (not the backbuffer) if SDL doesn't guarantee the texture contents should survive.
-    data.mtlpassdesc.colorAttachments[0].loadAction = MTLLoadActionLoad;
-    data.mtlcmdbuffer = [data.mtlcmdqueue commandBuffer];
-    data.mtlcmdencoder = [data.mtlcmdbuffer renderCommandEncoderWithDescriptor:data.mtlpassdesc];
-    data.mtlcmdencoder.label = texture ? @"SDL metal renderer render texture" : @"SDL metal renderer backbuffer";
+    if (data.mtlcmdencoder) {
+        /* End encoding for the previous render target so we can set up a new
+         * render pass for this one. */
+        [data.mtlcmdencoder endEncoding];
+        [data.mtlcmdbuffer commit];
 
-    // The higher level will reset the viewport and scissor after this call returns.
+        data.mtlcmdencoder = nil;
+        data.mtlcmdbuffer = nil;
+    }
 
+    /* We don't begin a new render pass right away - we delay it until an actual
+     * draw or clear happens. That way we can use hardware clears when possible,
+     * which are only available when beginning a new render pass. */
     return 0;
 }}
 
@@ -772,41 +796,43 @@ METAL_SetOrthographicProjection(SDL_Renderer *renderer, int w, int h)
 static int
 METAL_UpdateViewport(SDL_Renderer * renderer)
 { @autoreleasepool {
-    METAL_ActivateRenderer(renderer);
     METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
-    MTLViewport viewport;
-    viewport.originX = renderer->viewport.x;
-    viewport.originY = renderer->viewport.y;
-    viewport.width = renderer->viewport.w;
-    viewport.height = renderer->viewport.h;
-    viewport.znear = 0.0;
-    viewport.zfar = 1.0;
-    [data.mtlcmdencoder setViewport:viewport];
-    METAL_SetOrthographicProjection(renderer, renderer->viewport.w, renderer->viewport.h);
+    if (data.mtlcmdencoder) {
+        MTLViewport viewport;
+        viewport.originX = renderer->viewport.x;
+        viewport.originY = renderer->viewport.y;
+        viewport.width = renderer->viewport.w;
+        viewport.height = renderer->viewport.h;
+        viewport.znear = 0.0;
+        viewport.zfar = 1.0;
+        [data.mtlcmdencoder setViewport:viewport];
+        METAL_SetOrthographicProjection(renderer, renderer->viewport.w, renderer->viewport.h);
+    }
     return 0;
 }}
 
 static int
 METAL_UpdateClipRect(SDL_Renderer * renderer)
 { @autoreleasepool {
-    METAL_ActivateRenderer(renderer);
     METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
-    MTLScissorRect mtlrect;
-    // !!! FIXME: should this care about the viewport?
-    if (renderer->clipping_enabled) {
-        const SDL_Rect *rect = &renderer->clip_rect;
-        mtlrect.x = renderer->viewport.x + rect->x;
-        mtlrect.y = renderer->viewport.x + rect->y;
-        mtlrect.width = rect->w;
-        mtlrect.height = rect->h;
-    } else {
-        mtlrect.x = renderer->viewport.x;
-        mtlrect.y = renderer->viewport.y;
-        mtlrect.width = renderer->viewport.w;
-        mtlrect.height = renderer->viewport.h;
-    }
-    if (mtlrect.width > 0 && mtlrect.height > 0) {
-        [data.mtlcmdencoder setScissorRect:mtlrect];
+    if (data.mtlcmdencoder) {
+        MTLScissorRect mtlrect;
+        // !!! FIXME: should this care about the viewport?
+        if (renderer->clipping_enabled) {
+            const SDL_Rect *rect = &renderer->clip_rect;
+            mtlrect.x = renderer->viewport.x + rect->x;
+            mtlrect.y = renderer->viewport.x + rect->y;
+            mtlrect.width = rect->w;
+            mtlrect.height = rect->h;
+        } else {
+            mtlrect.x = renderer->viewport.x;
+            mtlrect.y = renderer->viewport.y;
+            mtlrect.width = renderer->viewport.w;
+            mtlrect.height = renderer->viewport.h;
+        }
+        if (mtlrect.width > 0 && mtlrect.height > 0) {
+            [data.mtlcmdencoder setScissorRect:mtlrect];
+        }
     }
     return 0;
 }}
@@ -814,38 +840,43 @@ METAL_UpdateClipRect(SDL_Renderer * renderer)
 static int
 METAL_RenderClear(SDL_Renderer * renderer)
 { @autoreleasepool {
-    // We could dump the command buffer and force a clear on a new one, but this will respect the scissor state.
-    METAL_ActivateRenderer(renderer);
     METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
 
-    // !!! FIXME: render color should live in a dedicated uniform buffer.
-    const float color[4] = { ((float)renderer->r) / 255.0f, ((float)renderer->g) / 255.0f, ((float)renderer->b) / 255.0f, ((float)renderer->a) / 255.0f };
-
-    MTLViewport viewport;  // RenderClear ignores the viewport state, though, so reset that.
-    viewport.originX = viewport.originY = 0.0;
-    viewport.width = data.mtlpassdesc.colorAttachments[0].texture.width;
-    viewport.height = data.mtlpassdesc.colorAttachments[0].texture.height;
-    viewport.znear = 0.0;
-    viewport.zfar = 1.0;
-
-    // Draw a simple filled fullscreen triangle now.
-    METAL_SetOrthographicProjection(renderer, 1, 1);
-    [data.mtlcmdencoder setViewport:viewport];
-    [data.mtlcmdencoder setRenderPipelineState:ChoosePipelineState(data, data.mtlpipelineprims, SDL_BLENDMODE_NONE)];
-    [data.mtlcmdencoder setVertexBuffer:data.mtlbufconstants offset:CONSTANTS_OFFSET_CLEAR_VERTS atIndex:0];
-    [data.mtlcmdencoder setVertexBuffer:data.mtlbufconstants offset:CONSTANTS_OFFSET_IDENTITY atIndex:3];
-    [data.mtlcmdencoder setFragmentBytes:color length:sizeof(color) atIndex:0];
-    [data.mtlcmdencoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
-
-    // reset the viewport for the rest of our usual drawing work...
-    viewport.originX = renderer->viewport.x;
-    viewport.originY = renderer->viewport.y;
-    viewport.width = renderer->viewport.w;
-    viewport.height = renderer->viewport.h;
-    viewport.znear = 0.0;
-    viewport.zfar = 1.0;
-    [data.mtlcmdencoder setViewport:viewport];
-    METAL_SetOrthographicProjection(renderer, renderer->viewport.w, renderer->viewport.h);
+    /* Since we set up the render command encoder lazily when a draw is
+     * requested, we can do the fast path hardware clear if no draws have
+     * happened since the last SetRenderTarget. */
+    if (data.mtlcmdencoder == nil) {
+        METAL_ActivateRenderCommandEncoder(renderer, MTLLoadActionClear);
+    } else {
+        // !!! FIXME: render color should live in a dedicated uniform buffer.
+        const float color[4] = { ((float)renderer->r) / 255.0f, ((float)renderer->g) / 255.0f, ((float)renderer->b) / 255.0f, ((float)renderer->a) / 255.0f };
+
+        MTLViewport viewport;  // RenderClear ignores the viewport state, though, so reset that.
+        viewport.originX = viewport.originY = 0.0;
+        viewport.width = data.mtlpassdesc.colorAttachments[0].texture.width;
+        viewport.height = data.mtlpassdesc.colorAttachments[0].texture.height;
+        viewport.znear = 0.0;
+        viewport.zfar = 1.0;
+
+        // Slow path for clearing: draw a filled fullscreen triangle.
+        METAL_SetOrthographicProjection(renderer, 1, 1);
+        [data.mtlcmdencoder setViewport:viewport];
+        [data.mtlcmdencoder setRenderPipelineState:ChoosePipelineState(data, data.mtlpipelineprims, SDL_BLENDMODE_NONE)];
+        [data.mtlcmdencoder setVertexBuffer:data.mtlbufconstants offset:CONSTANTS_OFFSET_CLEAR_VERTS atIndex:0];
+        [data.mtlcmdencoder setVertexBuffer:data.mtlbufconstants offset:CONSTANTS_OFFSET_IDENTITY atIndex:3];
+        [data.mtlcmdencoder setFragmentBytes:color length:sizeof(color) atIndex:0];
+        [data.mtlcmdencoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
+
+        // reset the viewport for the rest of our usual drawing work...
+        viewport.originX = renderer->viewport.x;
+        viewport.originY = renderer->viewport.y;
+        viewport.width = renderer->viewport.w;
+        viewport.height = renderer->viewport.h;
+        viewport.znear = 0.0;
+        viewport.zfar = 1.0;
+        [data.mtlcmdencoder setViewport:viewport];
+        METAL_SetOrthographicProjection(renderer, renderer->viewport.w, renderer->viewport.h);
+    }
 
     return 0;
 }}
@@ -861,7 +892,7 @@ static int
 DrawVerts(SDL_Renderer * renderer, const SDL_FPoint * points, int count,
           const MTLPrimitiveType primtype)
 { @autoreleasepool {
-    METAL_ActivateRenderer(renderer);
+    METAL_ActivateRenderCommandEncoder(renderer, MTLLoadActionLoad);
 
     const size_t vertlen = (sizeof (float) * 2) * count;
     METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
@@ -894,7 +925,7 @@ METAL_RenderDrawLines(SDL_Renderer * renderer, const SDL_FPoint * points, int co
 static int
 METAL_RenderFillRects(SDL_Renderer * renderer, const SDL_FRect * rects, int count)
 { @autoreleasepool {
-    METAL_ActivateRenderer(renderer);
+    METAL_ActivateRenderCommandEncoder(renderer, MTLLoadActionLoad);
     METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
 
     // !!! FIXME: render color should live in a dedicated uniform buffer.
@@ -925,7 +956,7 @@ static int
 METAL_RenderCopy(SDL_Renderer * renderer, SDL_Texture * texture,
               const SDL_Rect * srcrect, const SDL_FRect * dstrect)
 { @autoreleasepool {
-    METAL_ActivateRenderer(renderer);
+    METAL_ActivateRenderCommandEncoder(renderer, MTLLoadActionLoad);
     METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
     METAL_TextureData *texturedata = (__bridge METAL_TextureData *)texture->driverdata;
     const float texw = (float) texturedata.mtltexture.width;
@@ -970,7 +1001,7 @@ METAL_RenderCopyEx(SDL_Renderer * renderer, SDL_Texture * texture,
               const SDL_Rect * srcrect, const SDL_FRect * dstrect,
               const double angle, const SDL_FPoint *center, const SDL_RendererFlip flip)
 { @autoreleasepool {
-    METAL_ActivateRenderer(renderer);
+    METAL_ActivateRenderCommandEncoder(renderer, MTLLoadActionLoad);
     METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
     METAL_TextureData *texturedata = (__bridge METAL_TextureData *)texture->driverdata;
     const float texw = (float) texturedata.mtltexture.width;
@@ -1052,7 +1083,8 @@ static int
 METAL_RenderReadPixels(SDL_Renderer * renderer, const SDL_Rect * rect,
                     Uint32 pixel_format, void * pixels, int pitch)
 { @autoreleasepool {
-    METAL_ActivateRenderer(renderer);
+    METAL_ActivateRenderCommandEncoder(renderer, MTLLoadActionLoad);
+
     // !!! FIXME: this probably needs to commit the current command buffer, and probably waitUntilCompleted
     METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
     id<MTLTexture> mtltexture = data.mtlpassdesc.colorAttachments[0].texture;
@@ -1076,16 +1108,20 @@ METAL_RenderReadPixels(SDL_Renderer * renderer, const SDL_Rect * rect,
 static void
 METAL_RenderPresent(SDL_Renderer * renderer)
 { @autoreleasepool {
-    METAL_ActivateRenderer(renderer);
     METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
 
-    [data.mtlcmdencoder endEncoding];
-    [data.mtlcmdbuffer presentDrawable:data.mtlbackbuffer];
-    [data.mtlcmdbuffer commit];
+    if (data.mtlcmdencoder != nil) {
+        [data.mtlcmdencoder endEncoding];
+    }
+    if (data.mtlbackbuffer != nil) {
+        [data.mtlcmdbuffer presentDrawable:data.mtlbackbuffer];
+    }
+    if (data.mtlcmdbuffer != nil) {
+        [data.mtlcmdbuffer commit];
+    }
     data.mtlcmdencoder = nil;
     data.mtlcmdbuffer = nil;
     data.mtlbackbuffer = nil;
-    data.beginScene = YES;
 }}
 
 static void
@@ -1122,7 +1158,7 @@ METAL_GetMetalLayer(SDL_Renderer * renderer)
 static void *
 METAL_GetMetalCommandEncoder(SDL_Renderer * renderer)
 { @autoreleasepool {
-    METAL_ActivateRenderer(renderer);
+    METAL_ActivateRenderCommandEncoder(renderer, MTLLoadActionLoad);
     METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
     return (__bridge void*)data.mtlcmdencoder;
 }}