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

This commit is contained in:
Alex Szpakowski 2018-01-04 19:29:33 -04:00
parent 66baf7363e
commit 990ebba55a
1 changed files with 126 additions and 90 deletions

View File

@ -147,7 +147,6 @@ typedef struct METAL_PipelineCache
} METAL_PipelineCache; } METAL_PipelineCache;
@interface METAL_RenderData : NSObject @interface METAL_RenderData : NSObject
@property (nonatomic, assign) BOOL beginScene;
@property (nonatomic, retain) id<MTLDevice> mtldevice; @property (nonatomic, retain) id<MTLDevice> mtldevice;
@property (nonatomic, retain) id<MTLCommandQueue> mtlcmdqueue; @property (nonatomic, retain) id<MTLCommandQueue> mtlcmdqueue;
@property (nonatomic, retain) id<MTLCommandBuffer> mtlcmdbuffer; @property (nonatomic, retain) id<MTLCommandBuffer> mtlcmdbuffer;
@ -404,7 +403,6 @@ METAL_CreateRenderer(SDL_Window * window, Uint32 flags)
} }
data = [[METAL_RenderData alloc] init]; data = [[METAL_RenderData alloc] init];
data.beginScene = YES;
renderer->driverdata = (void*)CFBridgingRetain(data); renderer->driverdata = (void*)CFBridgingRetain(data);
renderer->window = window; renderer->window = window;
@ -562,21 +560,50 @@ METAL_CreateRenderer(SDL_Window * window, Uint32 flags)
} }
static void static void
METAL_ActivateRenderer(SDL_Renderer * renderer) METAL_ActivateRenderCommandEncoder(SDL_Renderer * renderer, MTLLoadAction load)
{ {
METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata; METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
if (data.beginScene) { /* Our SetRenderTarget just signals that the next render operation should
data.beginScene = NO; * set up a new render pass. This is where that work happens. */
data.mtlbackbuffer = [data.mtllayer nextDrawable]; if (data.mtlcmdencoder == nil) {
SDL_assert(data.mtlbackbuffer); id<MTLTexture> mtltexture = nil;
data.mtlpassdesc.colorAttachments[0].texture = data.mtlbackbuffer.texture;
data.mtlpassdesc.colorAttachments[0].loadAction = MTLLoadActionDontCare; 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.mtlcmdbuffer = [data.mtlcmdqueue commandBuffer];
data.mtlcmdencoder = [data.mtlcmdbuffer renderCommandEncoderWithDescriptor:data.mtlpassdesc]; 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_UpdateViewport(renderer);
METAL_UpdateClipRect(renderer); METAL_UpdateClipRect(renderer);
} }
@ -715,24 +742,21 @@ METAL_UnlockTexture(SDL_Renderer * renderer, SDL_Texture * texture)
static int static int
METAL_SetRenderTarget(SDL_Renderer * renderer, SDL_Texture * texture) METAL_SetRenderTarget(SDL_Renderer * renderer, SDL_Texture * texture)
{ @autoreleasepool { { @autoreleasepool {
METAL_ActivateRenderer(renderer);
METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata; METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
// commit the current command buffer, so that any work on a render target if (data.mtlcmdencoder) {
// will be available to the next one we're about to queue up. /* End encoding for the previous render target so we can set up a new
[data.mtlcmdencoder endEncoding]; * render pass for this one. */
[data.mtlcmdbuffer commit]; [data.mtlcmdencoder endEncoding];
[data.mtlcmdbuffer commit];
id<MTLTexture> mtltexture = texture ? ((__bridge METAL_TextureData *)texture->driverdata).mtltexture : data.mtlbackbuffer.texture; data.mtlcmdencoder = nil;
data.mtlpassdesc.colorAttachments[0].texture = mtltexture; data.mtlcmdbuffer = nil;
// !!! 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";
// The higher level will reset the viewport and scissor after this call returns.
/* 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; return 0;
}} }}
@ -772,41 +796,43 @@ METAL_SetOrthographicProjection(SDL_Renderer *renderer, int w, int h)
static int static int
METAL_UpdateViewport(SDL_Renderer * renderer) METAL_UpdateViewport(SDL_Renderer * renderer)
{ @autoreleasepool { { @autoreleasepool {
METAL_ActivateRenderer(renderer);
METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata; METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
MTLViewport viewport; if (data.mtlcmdencoder) {
viewport.originX = renderer->viewport.x; MTLViewport viewport;
viewport.originY = renderer->viewport.y; viewport.originX = renderer->viewport.x;
viewport.width = renderer->viewport.w; viewport.originY = renderer->viewport.y;
viewport.height = renderer->viewport.h; viewport.width = renderer->viewport.w;
viewport.znear = 0.0; viewport.height = renderer->viewport.h;
viewport.zfar = 1.0; viewport.znear = 0.0;
[data.mtlcmdencoder setViewport:viewport]; viewport.zfar = 1.0;
METAL_SetOrthographicProjection(renderer, renderer->viewport.w, renderer->viewport.h); [data.mtlcmdencoder setViewport:viewport];
METAL_SetOrthographicProjection(renderer, renderer->viewport.w, renderer->viewport.h);
}
return 0; return 0;
}} }}
static int static int
METAL_UpdateClipRect(SDL_Renderer * renderer) METAL_UpdateClipRect(SDL_Renderer * renderer)
{ @autoreleasepool { { @autoreleasepool {
METAL_ActivateRenderer(renderer);
METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata; METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
MTLScissorRect mtlrect; if (data.mtlcmdencoder) {
// !!! FIXME: should this care about the viewport? MTLScissorRect mtlrect;
if (renderer->clipping_enabled) { // !!! FIXME: should this care about the viewport?
const SDL_Rect *rect = &renderer->clip_rect; if (renderer->clipping_enabled) {
mtlrect.x = renderer->viewport.x + rect->x; const SDL_Rect *rect = &renderer->clip_rect;
mtlrect.y = renderer->viewport.x + rect->y; mtlrect.x = renderer->viewport.x + rect->x;
mtlrect.width = rect->w; mtlrect.y = renderer->viewport.x + rect->y;
mtlrect.height = rect->h; mtlrect.width = rect->w;
} else { mtlrect.height = rect->h;
mtlrect.x = renderer->viewport.x; } else {
mtlrect.y = renderer->viewport.y; mtlrect.x = renderer->viewport.x;
mtlrect.width = renderer->viewport.w; mtlrect.y = renderer->viewport.y;
mtlrect.height = renderer->viewport.h; mtlrect.width = renderer->viewport.w;
} mtlrect.height = renderer->viewport.h;
if (mtlrect.width > 0 && mtlrect.height > 0) { }
[data.mtlcmdencoder setScissorRect:mtlrect]; if (mtlrect.width > 0 && mtlrect.height > 0) {
[data.mtlcmdencoder setScissorRect:mtlrect];
}
} }
return 0; return 0;
}} }}
@ -814,38 +840,43 @@ METAL_UpdateClipRect(SDL_Renderer * renderer)
static int static int
METAL_RenderClear(SDL_Renderer * renderer) METAL_RenderClear(SDL_Renderer * renderer)
{ @autoreleasepool { { @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; METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
// !!! FIXME: render color should live in a dedicated uniform buffer. /* Since we set up the render command encoder lazily when a draw is
const float color[4] = { ((float)renderer->r) / 255.0f, ((float)renderer->g) / 255.0f, ((float)renderer->b) / 255.0f, ((float)renderer->a) / 255.0f }; * 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. MTLViewport viewport; // RenderClear ignores the viewport state, though, so reset that.
viewport.originX = viewport.originY = 0.0; viewport.originX = viewport.originY = 0.0;
viewport.width = data.mtlpassdesc.colorAttachments[0].texture.width; viewport.width = data.mtlpassdesc.colorAttachments[0].texture.width;
viewport.height = data.mtlpassdesc.colorAttachments[0].texture.height; viewport.height = data.mtlpassdesc.colorAttachments[0].texture.height;
viewport.znear = 0.0; viewport.znear = 0.0;
viewport.zfar = 1.0; viewport.zfar = 1.0;
// Draw a simple filled fullscreen triangle now. // Slow path for clearing: draw a filled fullscreen triangle.
METAL_SetOrthographicProjection(renderer, 1, 1); METAL_SetOrthographicProjection(renderer, 1, 1);
[data.mtlcmdencoder setViewport:viewport]; [data.mtlcmdencoder setViewport:viewport];
[data.mtlcmdencoder setRenderPipelineState:ChoosePipelineState(data, data.mtlpipelineprims, SDL_BLENDMODE_NONE)]; [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_CLEAR_VERTS atIndex:0];
[data.mtlcmdencoder setVertexBuffer:data.mtlbufconstants offset:CONSTANTS_OFFSET_IDENTITY atIndex:3]; [data.mtlcmdencoder setVertexBuffer:data.mtlbufconstants offset:CONSTANTS_OFFSET_IDENTITY atIndex:3];
[data.mtlcmdencoder setFragmentBytes:color length:sizeof(color) atIndex:0]; [data.mtlcmdencoder setFragmentBytes:color length:sizeof(color) atIndex:0];
[data.mtlcmdencoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3]; [data.mtlcmdencoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
// reset the viewport for the rest of our usual drawing work... // reset the viewport for the rest of our usual drawing work...
viewport.originX = renderer->viewport.x; viewport.originX = renderer->viewport.x;
viewport.originY = renderer->viewport.y; viewport.originY = renderer->viewport.y;
viewport.width = renderer->viewport.w; viewport.width = renderer->viewport.w;
viewport.height = renderer->viewport.h; viewport.height = renderer->viewport.h;
viewport.znear = 0.0; viewport.znear = 0.0;
viewport.zfar = 1.0; viewport.zfar = 1.0;
[data.mtlcmdencoder setViewport:viewport]; [data.mtlcmdencoder setViewport:viewport];
METAL_SetOrthographicProjection(renderer, renderer->viewport.w, renderer->viewport.h); METAL_SetOrthographicProjection(renderer, renderer->viewport.w, renderer->viewport.h);
}
return 0; return 0;
}} }}
@ -861,7 +892,7 @@ static int
DrawVerts(SDL_Renderer * renderer, const SDL_FPoint * points, int count, DrawVerts(SDL_Renderer * renderer, const SDL_FPoint * points, int count,
const MTLPrimitiveType primtype) const MTLPrimitiveType primtype)
{ @autoreleasepool { { @autoreleasepool {
METAL_ActivateRenderer(renderer); METAL_ActivateRenderCommandEncoder(renderer, MTLLoadActionLoad);
const size_t vertlen = (sizeof (float) * 2) * count; const size_t vertlen = (sizeof (float) * 2) * count;
METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata; 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 static int
METAL_RenderFillRects(SDL_Renderer * renderer, const SDL_FRect * rects, int count) METAL_RenderFillRects(SDL_Renderer * renderer, const SDL_FRect * rects, int count)
{ @autoreleasepool { { @autoreleasepool {
METAL_ActivateRenderer(renderer); METAL_ActivateRenderCommandEncoder(renderer, MTLLoadActionLoad);
METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata; METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
// !!! FIXME: render color should live in a dedicated uniform buffer. // !!! FIXME: render color should live in a dedicated uniform buffer.
@ -925,7 +956,7 @@ static int
METAL_RenderCopy(SDL_Renderer * renderer, SDL_Texture * texture, METAL_RenderCopy(SDL_Renderer * renderer, SDL_Texture * texture,
const SDL_Rect * srcrect, const SDL_FRect * dstrect) const SDL_Rect * srcrect, const SDL_FRect * dstrect)
{ @autoreleasepool { { @autoreleasepool {
METAL_ActivateRenderer(renderer); METAL_ActivateRenderCommandEncoder(renderer, MTLLoadActionLoad);
METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata; METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
METAL_TextureData *texturedata = (__bridge METAL_TextureData *)texture->driverdata; METAL_TextureData *texturedata = (__bridge METAL_TextureData *)texture->driverdata;
const float texw = (float) texturedata.mtltexture.width; 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 SDL_Rect * srcrect, const SDL_FRect * dstrect,
const double angle, const SDL_FPoint *center, const SDL_RendererFlip flip) const double angle, const SDL_FPoint *center, const SDL_RendererFlip flip)
{ @autoreleasepool { { @autoreleasepool {
METAL_ActivateRenderer(renderer); METAL_ActivateRenderCommandEncoder(renderer, MTLLoadActionLoad);
METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata; METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
METAL_TextureData *texturedata = (__bridge METAL_TextureData *)texture->driverdata; METAL_TextureData *texturedata = (__bridge METAL_TextureData *)texture->driverdata;
const float texw = (float) texturedata.mtltexture.width; const float texw = (float) texturedata.mtltexture.width;
@ -1052,7 +1083,8 @@ static int
METAL_RenderReadPixels(SDL_Renderer * renderer, const SDL_Rect * rect, METAL_RenderReadPixels(SDL_Renderer * renderer, const SDL_Rect * rect,
Uint32 pixel_format, void * pixels, int pitch) Uint32 pixel_format, void * pixels, int pitch)
{ @autoreleasepool { { @autoreleasepool {
METAL_ActivateRenderer(renderer); METAL_ActivateRenderCommandEncoder(renderer, MTLLoadActionLoad);
// !!! FIXME: this probably needs to commit the current command buffer, and probably waitUntilCompleted // !!! FIXME: this probably needs to commit the current command buffer, and probably waitUntilCompleted
METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata; METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
id<MTLTexture> mtltexture = data.mtlpassdesc.colorAttachments[0].texture; id<MTLTexture> mtltexture = data.mtlpassdesc.colorAttachments[0].texture;
@ -1076,16 +1108,20 @@ METAL_RenderReadPixels(SDL_Renderer * renderer, const SDL_Rect * rect,
static void static void
METAL_RenderPresent(SDL_Renderer * renderer) METAL_RenderPresent(SDL_Renderer * renderer)
{ @autoreleasepool { { @autoreleasepool {
METAL_ActivateRenderer(renderer);
METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata; METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
[data.mtlcmdencoder endEncoding]; if (data.mtlcmdencoder != nil) {
[data.mtlcmdbuffer presentDrawable:data.mtlbackbuffer]; [data.mtlcmdencoder endEncoding];
[data.mtlcmdbuffer commit]; }
if (data.mtlbackbuffer != nil) {
[data.mtlcmdbuffer presentDrawable:data.mtlbackbuffer];
}
if (data.mtlcmdbuffer != nil) {
[data.mtlcmdbuffer commit];
}
data.mtlcmdencoder = nil; data.mtlcmdencoder = nil;
data.mtlcmdbuffer = nil; data.mtlcmdbuffer = nil;
data.mtlbackbuffer = nil; data.mtlbackbuffer = nil;
data.beginScene = YES;
}} }}
static void static void
@ -1122,7 +1158,7 @@ METAL_GetMetalLayer(SDL_Renderer * renderer)
static void * static void *
METAL_GetMetalCommandEncoder(SDL_Renderer * renderer) METAL_GetMetalCommandEncoder(SDL_Renderer * renderer)
{ @autoreleasepool { { @autoreleasepool {
METAL_ActivateRenderer(renderer); METAL_ActivateRenderCommandEncoder(renderer, MTLLoadActionLoad);
METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata; METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
return (__bridge void*)data.mtlcmdencoder; return (__bridge void*)data.mtlcmdencoder;
}} }}