From 990ebba55a1fad88f6b03fe58d518d1ce33a5c2d Mon Sep 17 00:00:00 2001 From: Alex Szpakowski Date: Thu, 4 Jan 2018 19:29:33 -0400 Subject: [PATCH] metal: Implement fast hardware clearing when possible, by deferring the start of a render pass until a clear or draw operation happens. --- src/render/metal/SDL_render_metal.m | 222 ++++++++++++++++------------ 1 file changed, 129 insertions(+), 93 deletions(-) diff --git a/src/render/metal/SDL_render_metal.m b/src/render/metal/SDL_render_metal.m index 61d6bde4b80c6..335ee57118c11 100644 --- a/src/render/metal/SDL_render_metal.m +++ b/src/render/metal/SDL_render_metal.m @@ -147,7 +147,6 @@ static int METAL_RenderReadPixels(SDL_Renderer * renderer, const SDL_Rect * rect } METAL_PipelineCache; @interface METAL_RenderData : NSObject - @property (nonatomic, assign) BOOL beginScene; @property (nonatomic, retain) id mtldevice; @property (nonatomic, retain) id mtlcmdqueue; @property (nonatomic, retain) id mtlcmdbuffer; @@ -404,7 +403,6 @@ - (void)dealloc } data = [[METAL_RenderData alloc] init]; - data.beginScene = YES; renderer->driverdata = (void*)CFBridgingRetain(data); renderer->window = window; @@ -562,21 +560,50 @@ - (void)dealloc } 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 = 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 @@ - (void)dealloc 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 = 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 @@ - (void)dealloc 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 @@ - (void)dealloc 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 @@ - (void)dealloc 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 @@ - (void)dealloc 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 @@ - (void)dealloc 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 @@ - (void)dealloc 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 @@ - (void)dealloc 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 = data.mtlpassdesc.colorAttachments[0].texture; @@ -1076,16 +1108,20 @@ - (void)dealloc 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 @@ - (void)dealloc 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; }}