SDL: Backported Metal sampler improvements from main

From 0897f4a7d14537139774f9ddc3afaf45209e80fc Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Thu, 8 May 2025 10:22:17 -0700
Subject: [PATCH] Backported Metal sampler improvements from main

Fixes https://github.com/libsdl-org/SDL/issues/12988
---
 src/render/metal/SDL_render_metal.m | 137 ++++++++++++++++------------
 1 file changed, 78 insertions(+), 59 deletions(-)

diff --git a/src/render/metal/SDL_render_metal.m b/src/render/metal/SDL_render_metal.m
index 3bba129a67414..e11fe1c2b3885 100644
--- a/src/render/metal/SDL_render_metal.m
+++ b/src/render/metal/SDL_render_metal.m
@@ -79,15 +79,10 @@
 static const size_t CONSTANTS_OFFSET_DECODE_BT2020_FULL = ALIGN_CONSTANTS(16, CONSTANTS_OFFSET_DECODE_BT2020_LIMITED + sizeof(float) * 4 * 4);
 static const size_t CONSTANTS_LENGTH = CONSTANTS_OFFSET_DECODE_BT2020_FULL + sizeof(float) * 4 * 4;
 
-// Sampler types
-typedef enum
-{
-    SDL_METAL_SAMPLER_NEAREST_CLAMP,
-    SDL_METAL_SAMPLER_NEAREST_WRAP,
-    SDL_METAL_SAMPLER_LINEAR_CLAMP,
-    SDL_METAL_SAMPLER_LINEAR_WRAP,
-    SDL_NUM_METAL_SAMPLERS
-} SDL_METAL_sampler_type;
+#define RENDER_SAMPLER_HASHKEY(scale_mode, address_u, address_v)    \
+    (((scale_mode == SDL_SCALEMODE_NEAREST) << 0) |                 \
+     ((address_u == SDL_TEXTURE_ADDRESS_WRAP) << 1) |               \
+     ((address_v == SDL_TEXTURE_ADDRESS_WRAP) << 2))
 
 typedef enum SDL_MetalVertexFunction
 {
@@ -139,7 +134,7 @@ @interface SDL3METAL_RenderData : NSObject
 @property(nonatomic, retain) id<MTLRenderCommandEncoder> mtlcmdencoder;
 @property(nonatomic, retain) id<MTLLibrary> mtllibrary;
 @property(nonatomic, retain) id<CAMetalDrawable> mtlbackbuffer;
-@property(nonatomic, retain) NSMutableArray<id<MTLSamplerState>> *mtlsamplers;
+@property(nonatomic, retain) NSMutableDictionary<NSNumber *, id<MTLSamplerState>> *mtlsamplers;
 @property(nonatomic, retain) id<MTLBuffer> mtlbufconstants;
 @property(nonatomic, retain) id<MTLBuffer> mtlbufquadindices;
 @property(nonatomic, assign) SDL_MetalView mtlview;
@@ -1295,6 +1290,9 @@ static bool METAL_QueueGeometry(SDL_Renderer *renderer, SDL_RenderCommand *cmd,
     __unsafe_unretained id<MTLBuffer> vertex_buffer;
     size_t constants_offset;
     SDL_Texture *texture;
+    SDL_ScaleMode texture_scale_mode;
+    SDL_TextureAddressMode texture_address_mode_u;
+    SDL_TextureAddressMode texture_address_mode_v;
     bool cliprect_dirty;
     bool cliprect_enabled;
     SDL_Rect cliprect;
@@ -1452,6 +1450,58 @@ static bool SetDrawState(SDL_Renderer *renderer, const SDL_RenderCommand *cmd, c
     return true;
 }
 
+static id<MTLSamplerState> GetSampler(SDL3METAL_RenderData *data, SDL_ScaleMode scale_mode, SDL_TextureAddressMode address_u, SDL_TextureAddressMode address_v)
+{
+    NSNumber *key = [NSNumber numberWithInteger:RENDER_SAMPLER_HASHKEY(scale_mode, address_u, address_v)];
+    id<MTLSamplerState> mtlsampler = data.mtlsamplers[key];
+    if (mtlsampler == nil) {
+        MTLSamplerDescriptor *samplerdesc;
+        samplerdesc = [[MTLSamplerDescriptor alloc] init];
+        switch (scale_mode) {
+        case SDL_SCALEMODE_NEAREST:
+            samplerdesc.minFilter = MTLSamplerMinMagFilterNearest;
+            samplerdesc.magFilter = MTLSamplerMinMagFilterNearest;
+            break;
+        case SDL_SCALEMODE_LINEAR:
+            samplerdesc.minFilter = MTLSamplerMinMagFilterLinear;
+            samplerdesc.magFilter = MTLSamplerMinMagFilterLinear;
+            break;
+        default:
+            SDL_SetError("Unknown scale mode: %d", scale_mode);
+            return nil;
+        }
+        switch (address_u) {
+        case SDL_TEXTURE_ADDRESS_CLAMP:
+            samplerdesc.sAddressMode = MTLSamplerAddressModeClampToEdge;
+            break;
+        case SDL_TEXTURE_ADDRESS_WRAP:
+            samplerdesc.sAddressMode = MTLSamplerAddressModeRepeat;
+            break;
+        default:
+            SDL_SetError("Unknown texture address mode: %d", address_u);
+            return nil;
+        }
+        switch (address_v) {
+        case SDL_TEXTURE_ADDRESS_CLAMP:
+            samplerdesc.tAddressMode = MTLSamplerAddressModeClampToEdge;
+            break;
+        case SDL_TEXTURE_ADDRESS_WRAP:
+            samplerdesc.tAddressMode = MTLSamplerAddressModeRepeat;
+            break;
+        default:
+            SDL_SetError("Unknown texture address mode: %d", address_v);
+            return nil;
+        }
+        mtlsampler = [data.mtldevice newSamplerStateWithDescriptor:samplerdesc];
+        if (mtlsampler == nil) {
+            SDL_SetError("Couldn't create sampler");
+            return nil;
+        }
+        data.mtlsamplers[key] = mtlsampler;
+    }
+    return mtlsampler;
+}
+
 static bool SetCopyState(SDL_Renderer *renderer, const SDL_RenderCommand *cmd, const size_t constants_offset,
                              id<MTLBuffer> mtlbufvertex, METAL_DrawStateCache *statecache)
 {
@@ -1467,33 +1517,6 @@ static bool SetCopyState(SDL_Renderer *renderer, const SDL_RenderCommand *cmd, c
     }
 
     if (texture != statecache->texture) {
-        id<MTLSamplerState> mtlsampler;
-
-        if (cmd->data.draw.texture_scale_mode == SDL_SCALEMODE_NEAREST) {
-            switch (cmd->data.draw.texture_address_mode) {
-            case SDL_TEXTURE_ADDRESS_CLAMP:
-                mtlsampler = data.mtlsamplers[SDL_METAL_SAMPLER_NEAREST_CLAMP];
-                break;
-            case SDL_TEXTURE_ADDRESS_WRAP:
-                mtlsampler = data.mtlsamplers[SDL_METAL_SAMPLER_NEAREST_WRAP];
-                break;
-            default:
-                return SDL_SetError("Unknown texture address mode: %d", cmd->data.draw.texture_address_mode);
-            }
-        } else {
-            switch (cmd->data.draw.texture_address_mode) {
-            case SDL_TEXTURE_ADDRESS_CLAMP:
-                mtlsampler = data.mtlsamplers[SDL_METAL_SAMPLER_LINEAR_CLAMP];
-                break;
-            case SDL_TEXTURE_ADDRESS_WRAP:
-                mtlsampler = data.mtlsamplers[SDL_METAL_SAMPLER_LINEAR_WRAP];
-                break;
-            default:
-                return SDL_SetError("Unknown texture address mode: %d", cmd->data.draw.texture_address_mode);
-            }
-        }
-        [data.mtlcmdencoder setFragmentSamplerState:mtlsampler atIndex:0];
-
         [data.mtlcmdencoder setFragmentTexture:texturedata.mtltexture atIndex:0];
 #ifdef SDL_HAVE_YUV
         if (texturedata.yuv || texturedata.nv12) {
@@ -1503,6 +1526,20 @@ static bool SetCopyState(SDL_Renderer *renderer, const SDL_RenderCommand *cmd, c
 #endif
         statecache->texture = texture;
     }
+
+    if (cmd->data.draw.texture_scale_mode != statecache->texture_scale_mode ||
+        cmd->data.draw.texture_address_mode != statecache->texture_address_mode_u ||
+        cmd->data.draw.texture_address_mode != statecache->texture_address_mode_v) {
+        id<MTLSamplerState> mtlsampler = GetSampler(data, cmd->data.draw.texture_scale_mode, cmd->data.draw.texture_address_mode, cmd->data.draw.texture_address_mode);
+        if (mtlsampler == nil) {
+            return false;
+        }
+        [data.mtlcmdencoder setFragmentSamplerState:mtlsampler atIndex:0];
+
+        statecache->texture_scale_mode = cmd->data.draw.texture_scale_mode;
+        statecache->texture_address_mode_u = cmd->data.draw.texture_address_mode;
+        statecache->texture_address_mode_v = cmd->data.draw.texture_address_mode;
+    }
     return true;
 }
 
@@ -1523,6 +1560,9 @@ static bool METAL_RunCommandQueue(SDL_Renderer *renderer, SDL_RenderCommand *cmd
         statecache.vertex_buffer = nil;
         statecache.constants_offset = CONSTANTS_OFFSET_INVALID;
         statecache.texture = NULL;
+        statecache.texture_scale_mode = SDL_SCALEMODE_INVALID;
+        statecache.texture_address_mode_u = SDL_TEXTURE_ADDRESS_INVALID;
+        statecache.texture_address_mode_v = SDL_TEXTURE_ADDRESS_INVALID;
         statecache.shader_constants_dirty = true;
         statecache.cliprect_dirty = true;
         statecache.viewport_dirty = true;
@@ -1883,7 +1923,6 @@ static bool METAL_CreateRenderer(SDL_Renderer *renderer, SDL_Window *window, SDL
         int maxtexsize, quadcount = UINT16_MAX / 4;
         UInt16 *indexdata;
         size_t indicessize = sizeof(UInt16) * quadcount * 6;
-        MTLSamplerDescriptor *samplerdesc;
         id<MTLCommandQueue> mtlcmdqueue;
         id<MTLLibrary> mtllibrary;
         id<MTLBuffer> mtlbufconstantstaging, mtlbufquadindicesstaging, mtlbufconstants, mtlbufquadindices;
@@ -2043,27 +2082,7 @@ in case we want to use it later (recreating the renderer)
         data.allpipelines = NULL;
         ChooseShaderPipelines(data, MTLPixelFormatBGRA8Unorm);
 
-        static struct
-        {
-            MTLSamplerMinMagFilter filter;
-            MTLSamplerAddressMode address;
-        } samplerParams[] = {
-            { MTLSamplerMinMagFilterNearest, MTLSamplerAddressModeClampToEdge },
-            { MTLSamplerMinMagFilterNearest, MTLSamplerAddressModeRepeat },
-            { MTLSamplerMinMagFilterLinear, MTLSamplerAddressModeClampToEdge },
-            { MTLSamplerMinMagFilterLinear, MTLSamplerAddressModeRepeat },
-        };
-        SDL_COMPILE_TIME_ASSERT(samplerParams_SIZE, SDL_arraysize(samplerParams) == SDL_NUM_METAL_SAMPLERS);
-
-        data.mtlsamplers = [[NSMutableArray<id<MTLSamplerState>> alloc] init];
-        samplerdesc = [[MTLSamplerDescriptor alloc] init];
-        for (int i = 0; i < SDL_arraysize(samplerParams); ++i) {
-            samplerdesc.minFilter = samplerParams[i].filter;
-            samplerdesc.magFilter = samplerParams[i].filter;
-            samplerdesc.sAddressMode = samplerParams[i].address;
-            samplerdesc.tAddressMode = samplerParams[i].address;
-            [data.mtlsamplers addObject:[data.mtldevice newSamplerStateWithDescriptor:samplerdesc]];
-        }
+        data.mtlsamplers = [[NSMutableDictionary<NSNumber *, id<MTLSamplerState>> alloc] init];
 
         mtlbufconstantstaging = [data.mtldevice newBufferWithLength:CONSTANTS_LENGTH options:MTLResourceStorageModeShared];