metal: use a staging texture in SDL_UpdateTexture, to make sure it doesn't stomp texture data being drawn in a previous frame on the GPU.
authorAlex Szpakowski <slime73@gmail.com>
Sun, 25 Nov 2018 22:13:09 -0400
changeset 12431178b6d6b407f
parent 12430 d2514b03d3d6
child 12432 defbd51902c3
metal: use a staging texture in SDL_UpdateTexture, to make sure it doesn't stomp texture data being drawn in a previous frame on the GPU.
src/render/metal/SDL_render_metal.m
     1.1 --- a/src/render/metal/SDL_render_metal.m	Fri Nov 23 21:29:42 2018 -0800
     1.2 +++ b/src/render/metal/SDL_render_metal.m	Sun Nov 25 22:13:09 2018 -0400
     1.3 @@ -154,6 +154,7 @@
     1.4      @property (nonatomic, assign) BOOL yuv;
     1.5      @property (nonatomic, assign) BOOL nv12;
     1.6      @property (nonatomic, assign) size_t conversionBufferOffset;
     1.7 +    @property (nonatomic, assign) BOOL hasdata;
     1.8  @end
     1.9  
    1.10  @implementation METAL_TextureData
    1.11 @@ -609,54 +610,141 @@
    1.12      return 0;
    1.13  }}
    1.14  
    1.15 +static void
    1.16 +METAL_UploadTextureData(id<MTLTexture> texture, SDL_Rect rect, int slice,
    1.17 +                        const void * pixels, int pitch)
    1.18 +{
    1.19 +    [texture replaceRegion:MTLRegionMake2D(rect.x, rect.y, rect.w, rect.h)
    1.20 +               mipmapLevel:0
    1.21 +                     slice:slice
    1.22 +                 withBytes:pixels
    1.23 +               bytesPerRow:pitch
    1.24 +             bytesPerImage:0];
    1.25 +}
    1.26 +
    1.27 +static MTLStorageMode
    1.28 +METAL_GetStorageMode(id<MTLResource> resource)
    1.29 +{
    1.30 +    /* iOS 8 does not have this method. */
    1.31 +    if ([resource respondsToSelector:@selector(storageMode)]) {
    1.32 +        return resource.storageMode;
    1.33 +    }
    1.34 +    return MTLStorageModeShared;
    1.35 +}
    1.36 +
    1.37 +static int
    1.38 +METAL_UpdateTextureInternal(SDL_Renderer * renderer, METAL_TextureData *texturedata,
    1.39 +                            id<MTLTexture> texture, SDL_Rect rect, int slice,
    1.40 +                            const void * pixels, int pitch)
    1.41 +{
    1.42 +    METAL_RenderData *data = (__bridge METAL_RenderData *) renderer->driverdata;
    1.43 +    SDL_Rect stagingrect = {0, 0, rect.w, rect.h};
    1.44 +    MTLTextureDescriptor *desc;
    1.45 +
    1.46 +    /* If the texture is managed or shared and this is the first upload, we can
    1.47 +     * use replaceRegion to upload to it directly. Otherwise we upload the data
    1.48 +     * to a staging texture and copy that over. */
    1.49 +    if (!texturedata.hasdata && METAL_GetStorageMode(texture) != MTLStorageModePrivate) {
    1.50 +        METAL_UploadTextureData(texture, rect, slice, pixels, pitch);
    1.51 +        return 0;
    1.52 +    }
    1.53 +
    1.54 +    desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:texture.pixelFormat
    1.55 +                                                              width:rect.w
    1.56 +                                                             height:rect.h
    1.57 +                                                          mipmapped:NO];
    1.58 +
    1.59 +    if (desc == nil) {
    1.60 +        return SDL_OutOfMemory();
    1.61 +    }
    1.62 +
    1.63 +    /* TODO: We could have a pool of textures or a MTLHeap we allocate from,
    1.64 +     * and release a staging texture back to the pool in the command buffer's
    1.65 +     * completion handler. */
    1.66 +    id<MTLTexture> stagingtex = [data.mtldevice newTextureWithDescriptor:desc];
    1.67 +    if (stagingtex == nil) {
    1.68 +        return SDL_OutOfMemory();
    1.69 +    }
    1.70 +
    1.71 +#if !__has_feature(objc_arc)
    1.72 +    [stagingtex autorelease];
    1.73 +#endif
    1.74 +
    1.75 +    METAL_UploadTextureData(stagingtex, stagingrect, 0, pixels, pitch);
    1.76 +
    1.77 +    if (data.mtlcmdencoder != nil) {
    1.78 +        [data.mtlcmdencoder endEncoding];
    1.79 +        data.mtlcmdencoder = nil;
    1.80 +    }
    1.81 +
    1.82 +    if (data.mtlcmdbuffer == nil) {
    1.83 +        data.mtlcmdbuffer = [data.mtlcmdqueue commandBuffer];
    1.84 +    }
    1.85 +
    1.86 +    id<MTLBlitCommandEncoder> blitcmd = [data.mtlcmdbuffer blitCommandEncoder];
    1.87 +
    1.88 +    [blitcmd copyFromTexture:stagingtex
    1.89 +                 sourceSlice:0
    1.90 +                 sourceLevel:0
    1.91 +                sourceOrigin:MTLOriginMake(0, 0, 0)
    1.92 +                  sourceSize:MTLSizeMake(rect.w, rect.h, 1)
    1.93 +                   toTexture:texture
    1.94 +            destinationSlice:slice
    1.95 +            destinationLevel:0
    1.96 +           destinationOrigin:MTLOriginMake(rect.x, rect.y, 0)];
    1.97 +
    1.98 +    [blitcmd endEncoding];
    1.99 +
   1.100 +    /* TODO: This isn't very efficient for the YUV formats, which call
   1.101 +     * UpdateTextureInternal multiple times in a row. */
   1.102 +    [data.mtlcmdbuffer commit];
   1.103 +    data.mtlcmdbuffer = nil;
   1.104 +
   1.105 +    return 0;
   1.106 +}
   1.107 +
   1.108  static int
   1.109  METAL_UpdateTexture(SDL_Renderer * renderer, SDL_Texture * texture,
   1.110 -                 const SDL_Rect * rect, const void *pixels, int pitch)
   1.111 +                    const SDL_Rect * rect, const void *pixels, int pitch)
   1.112  { @autoreleasepool {
   1.113      METAL_TextureData *texturedata = (__bridge METAL_TextureData *)texture->driverdata;
   1.114  
   1.115 -    /* !!! FIXME: replaceRegion does not do any synchronization, so it might
   1.116 -     * !!! FIXME: stomp on a previous frame's data that's currently being read
   1.117 -     * !!! FIXME: by the GPU. */
   1.118 -    [texturedata.mtltexture replaceRegion:MTLRegionMake2D(rect->x, rect->y, rect->w, rect->h)
   1.119 -                              mipmapLevel:0
   1.120 -                                withBytes:pixels
   1.121 -                              bytesPerRow:pitch];
   1.122 +    if (METAL_UpdateTextureInternal(renderer, texturedata, texturedata.mtltexture, *rect, 0, pixels, pitch) < 0) {
   1.123 +        return -1;
   1.124 +    }
   1.125  
   1.126      if (texturedata.yuv) {
   1.127          int Uslice = texture->format == SDL_PIXELFORMAT_YV12 ? 1 : 0;
   1.128          int Vslice = texture->format == SDL_PIXELFORMAT_YV12 ? 0 : 1;
   1.129 +        int UVpitch = (pitch + 1) / 2;
   1.130 +        SDL_Rect UVrect = {rect->x / 2, rect->y / 2, (rect->w + 1) / 2, (rect->h + 1) / 2};
   1.131  
   1.132          /* Skip to the correct offset into the next texture */
   1.133          pixels = (const void*)((const Uint8*)pixels + rect->h * pitch);
   1.134 -        [texturedata.mtltexture_uv replaceRegion:MTLRegionMake2D(rect->x / 2, rect->y / 2, (rect->w + 1) / 2, (rect->h + 1) / 2)
   1.135 -                                     mipmapLevel:0
   1.136 -                                           slice:Uslice
   1.137 -                                       withBytes:pixels
   1.138 -                                     bytesPerRow:(pitch + 1) / 2
   1.139 -                                   bytesPerImage:0];
   1.140 +        if (METAL_UpdateTextureInternal(renderer, texturedata, texturedata.mtltexture_uv, UVrect, Uslice, pixels, UVpitch) < 0) {
   1.141 +            return -1;
   1.142 +        }
   1.143  
   1.144          /* Skip to the correct offset into the next texture */
   1.145 -        pixels = (const void*)((const Uint8*)pixels + ((rect->h + 1) / 2) * ((pitch + 1)/2));
   1.146 -        [texturedata.mtltexture_uv replaceRegion:MTLRegionMake2D(rect->x / 2, rect->y / 2, (rect->w + 1) / 2, (rect->h + 1) / 2)
   1.147 -                                     mipmapLevel:0
   1.148 -                                           slice:Vslice
   1.149 -                                       withBytes:pixels
   1.150 -                                     bytesPerRow:(pitch + 1) / 2
   1.151 -                                   bytesPerImage:0];
   1.152 +        pixels = (const void*)((const Uint8*)pixels + UVrect.h * UVpitch);
   1.153 +        if (METAL_UpdateTextureInternal(renderer, texturedata, texturedata.mtltexture_uv, UVrect, Vslice, pixels, UVpitch) < 0) {
   1.154 +            return -1;
   1.155 +        }
   1.156      }
   1.157  
   1.158      if (texturedata.nv12) {
   1.159 +        SDL_Rect UVrect = {rect->x / 2, rect->y / 2, (rect->w + 1) / 2, (rect->h + 1) / 2};
   1.160 +        int UVpitch = 2 * ((pitch + 1) / 2);
   1.161 +
   1.162          /* Skip to the correct offset into the next texture */
   1.163          pixels = (const void*)((const Uint8*)pixels + rect->h * pitch);
   1.164 -        [texturedata.mtltexture_uv replaceRegion:MTLRegionMake2D(rect->x / 2, rect->y / 2, (rect->w + 1) / 2, (rect->h + 1) / 2)
   1.165 -                                     mipmapLevel:0
   1.166 -                                           slice:0
   1.167 -                                       withBytes:pixels
   1.168 -                                     bytesPerRow:2 * ((pitch + 1) / 2)
   1.169 -                                   bytesPerImage:0];
   1.170 +        if (METAL_UpdateTextureInternal(renderer, texturedata, texturedata.mtltexture_uv, UVrect, 0, pixels, UVpitch) < 0) {
   1.171 +            return -1;
   1.172 +        }
   1.173      }
   1.174  
   1.175 +    texturedata.hasdata = YES;
   1.176 +
   1.177      return 0;
   1.178  }}
   1.179  
   1.180 @@ -670,30 +758,24 @@
   1.181      METAL_TextureData *texturedata = (__bridge METAL_TextureData *)texture->driverdata;
   1.182      const int Uslice = 0;
   1.183      const int Vslice = 1;
   1.184 +    SDL_Rect UVrect = {rect->x / 2, rect->y / 2, (rect->w + 1) / 2, (rect->h + 1) / 2};
   1.185  
   1.186      /* Bail out if we're supposed to update an empty rectangle */
   1.187      if (rect->w <= 0 || rect->h <= 0) {
   1.188          return 0;
   1.189      }
   1.190  
   1.191 -    [texturedata.mtltexture replaceRegion:MTLRegionMake2D(rect->x, rect->y, rect->w, rect->h)
   1.192 -                              mipmapLevel:0
   1.193 -                                withBytes:Yplane
   1.194 -                              bytesPerRow:Ypitch];
   1.195 +    if (METAL_UpdateTextureInternal(renderer, texturedata, texturedata.mtltexture, *rect, 0, Yplane, Ypitch) < 0) {
   1.196 +        return -1;
   1.197 +    }
   1.198 +    if (METAL_UpdateTextureInternal(renderer, texturedata, texturedata.mtltexture_uv, UVrect, Uslice, Uplane, Upitch)) {
   1.199 +        return -1;
   1.200 +    }
   1.201 +    if (METAL_UpdateTextureInternal(renderer, texturedata, texturedata.mtltexture_uv, UVrect, Vslice, Vplane, Vpitch)) {
   1.202 +        return -1;
   1.203 +    }
   1.204  
   1.205 -    [texturedata.mtltexture_uv replaceRegion:MTLRegionMake2D(rect->x / 2, rect->y / 2, (rect->w + 1) / 2, (rect->h + 1) / 2)
   1.206 -                                 mipmapLevel:0
   1.207 -                                       slice:Uslice
   1.208 -                                   withBytes:Uplane
   1.209 -                                 bytesPerRow:Upitch
   1.210 -                               bytesPerImage:0];
   1.211 -
   1.212 -    [texturedata.mtltexture_uv replaceRegion:MTLRegionMake2D(rect->x / 2, rect->y / 2, (rect->w + 1) / 2, (rect->h + 1) / 2)
   1.213 -                                 mipmapLevel:0
   1.214 -                                       slice:Vslice
   1.215 -                                   withBytes:Vplane
   1.216 -                                 bytesPerRow:Vpitch
   1.217 -                               bytesPerImage:0];
   1.218 +    texturedata.hasdata = YES;
   1.219  
   1.220      return 0;
   1.221  }}
   1.222 @@ -1217,7 +1299,7 @@
   1.223       * update the CPU-side copy of the texture data.
   1.224       * NOTE: Currently all of our textures are managed on macOS. We'll need some
   1.225       * extra copying for any private textures. */
   1.226 -    if (mtltexture.storageMode == MTLStorageModeManaged) {
   1.227 +    if (METAL_GetStorageMode(mtltexture) == MTLStorageModeManaged) {
   1.228          id<MTLBlitCommandEncoder> blit = [data.mtlcmdbuffer blitCommandEncoder];
   1.229          [blit synchronizeResource:mtltexture];
   1.230          [blit endEncoding];