metal: Implement fast hardware clearing when possible, by deferring the start of a render pass until a clear or draw operation happens.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342
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;
}}