Testing Metal API with SDL

Here is an experimental using Metal API with SDL and CMake. But I think better directly using MetalKit and Xcode if you want to get full of tools.

CMakeFiles.txt:

add_executable(mygame ${GameSource})
target_compile_definitions(mygame PUBLIC SDL MACOSX_DEPLOYMENT_TARGET=12.1)
target_compile_options(mygame PUBLIC -mmacosx-version-min=12.1)
target_link_libraries(mygame SDL2::SDL2 "-framework Metal -framework QuartzCore")
target_link_options(mygame PUBLIC -mmacosx-version-min=12.1)

add_custom_command(TARGET mygame PRE_BUILD
        COMMAND xcrun -sdk macosx metal "${CMAKE_SOURCE_DIR}/MyLibrary.metal" -o "${CMAKE_BINARY_DIR}/MyLibrary.metalib"
        WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
        DEPENDS "${CMAKE_SOURCE_DIR}/MyLibrary.metal" "${CMAKE_SOURCE_DIR}/MyLibraryShaderTypes.h"
        BYPRODUCTS "${CMAKE_BINARY_DIR}/MyLibrary.metalib"
)

Startup code on main.cpp:

int mainSDL();

int main(int argc, char *argv[])
{
  mainSDL();
}

Startup for Objective-C++ thru gui_osx.mm:

#include <cstdlib>
#include <iostream>

#include <SDL.h>
#include "shader/MyLibraryShaderTypes.h"

#import <Metal/Metal.h>
#import <QuartzCore/CAMetalLayer.h>


int mainSDL() {
  NSError *error;

  SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal");

  //Initialize all the systems of SDL
  if (SDL_Init(SDL_INIT_EVERYTHING) != 0) {
    std::cout << "SDL_Init Error: " << SDL_GetError() << std::endl;
    return EXIT_FAILURE;
  }

  //Create a window with a title, "Getting Started", in the centre
  //(or undefined x and y positions), with dimensions of 800 px width
  //and 600 px height and force it to be shown on screen
  SDL_Window* window = SDL_CreateWindow("Getting Started", SDL_WINDOWPOS_UNDEFINED,
      SDL_WINDOWPOS_UNDEFINED, 800, 600, SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE);
  if (!window) {
    std::cout << "SDL_CreateWindow Error: " << SDL_GetError() << std::endl;
    return EXIT_FAILURE;
  }

  //Create a renderer for the window created above, with the first display driver present
  //and with no additional settings
  SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_PRESENTVSYNC);
  Uint32 pixelFormatEnum = SDL_GetWindowPixelFormat(window);
  simd::uint2 viewportSize;

  MTLPixelFormat pixfmt;
  switch (pixelFormatEnum) {
    case SDL_PIXELFORMAT_ABGR8888:
      pixfmt = MTLPixelFormatRGBA8Unorm;
      break;
    case SDL_PIXELFORMAT_ARGB8888:
      pixfmt = MTLPixelFormatBGRA8Unorm;
      break;
    default:
      std::cout << "ERROR skip or not supported pixel format " << SDL_GetPixelFormatName(pixelFormatEnum) << std::endl;
      return EXIT_FAILURE;
  }

  const CAMetalLayer *swapchain = (__bridge CAMetalLayer *)SDL_RenderGetMetalLayer(renderer);
  const id<MTLDevice> gpu = swapchain.device;

  // Load all the shader files with a .metal file extension in the project.
  id<MTLLibrary> defaultLibrary = [gpu newLibraryWithFile:@"MyLibrary.metalib"
                                                    error:&error];

  NSCAssert(defaultLibrary, @"Failed to load library file: %@", error);

  id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
  id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];

  // Configure a pipeline descriptor that is used to create a pipeline state.
  MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];

  pipelineStateDescriptor.label = @"Simple Pipeline";
  pipelineStateDescriptor.vertexFunction = vertexFunction;
  pipelineStateDescriptor.fragmentFunction = fragmentFunction;
  pipelineStateDescriptor.colorAttachments[0].pixelFormat = pixfmt;

  id<MTLRenderPipelineState> pipelineState = [gpu newRenderPipelineStateWithDescriptor:pipelineStateDescriptor
                                                                                 error:&error];

  NSCAssert(pipelineState, @"Failed to create pipeline state: %@", error);

  MTLRenderPassDescriptor *pass = [MTLRenderPassDescriptor renderPassDescriptor];
  MTLRenderPassColorAttachmentDescriptor *colorattachment = pass.colorAttachments[0];

  const id<MTLCommandQueue> queue = [gpu newCommandQueue];

  MTLClearColor color = MTLClearColorMake(1.0, 0.3, 0.0, 1.0);
  /* Clear to a red-orange color when beginning the render pass. */
  colorattachment.clearColor  = color;
  colorattachment.loadAction  = MTLLoadActionClear;
  colorattachment.storeAction = MTLStoreActionStore;

  //Set the draw color of renderer to green
  //SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);

  //Clear the renderer with the draw color
  //SDL_RenderClear(renderer);

  //Update the renderer which will show the renderer cleared by the draw color which is green
  //SDL_RenderPresent(renderer);

  static const AAPLVertex triangleVertices[] =
  {
    // 2D positions,    RGBA colors
    { {  250,  -250 }, { 1, 0, 0, 1 } },
    { { -250,  -250 }, { 0, 1, 0, 1 } },
    { {    0,   250 }, { 0, 0, 1, 1 } },
  };


  SDL_Event e;
  bool quit = false;
  while (!quit){
      while (SDL_PollEvent(&e)){
          if (e.type == SDL_QUIT){
              quit = true;
          }
          if (e.type == SDL_KEYDOWN){
              quit = true;
          }
          if (e.type == SDL_MOUSEBUTTONDOWN){
              quit = true;
          }
      }

      int width, height;
      SDL_GetRendererOutputSize(renderer, &width, &height);
      viewportSize.x = width;
      viewportSize.y = height;

      @autoreleasepool {
        id<CAMetalDrawable> surface = [swapchain nextDrawable];

        id<MTLCommandBuffer> buffer = [queue commandBuffer];
        buffer.label = @"MyCommand";

        color.red = (color.red > 1.0) ? 0 : color.red + 0.01;
        colorattachment.clearColor = color;
        colorattachment.texture = surface.texture;

        id<MTLRenderCommandEncoder> encoder = [buffer renderCommandEncoderWithDescriptor:pass];
        encoder.label = @"MyRenderEncoder";

        // Set the region of the drawable to draw into.
        [encoder setViewport:(MTLViewport){0.0, 0.0, (double)viewportSize.x, (double)viewportSize.y, 0.0, 1.0 }];

        [encoder setRenderPipelineState:pipelineState];

        // Pass in the parameter data.
        [encoder setVertexBytes:triangleVertices
                        length:sizeof(triangleVertices)
                        atIndex:AAPLVertexInputIndexVertices];

        [encoder setVertexBytes:&viewportSize
                          length:sizeof(viewportSize)
                        atIndex:AAPLVertexInputIndexViewportSize];

        // Draw the triangle.
        [encoder drawPrimitives:MTLPrimitiveTypeTriangle
                    vertexStart:0
                    vertexCount:3];

        [encoder endEncoding];
        [buffer presentDrawable:surface];

        [buffer commit];
      }
  }

  //Destroy the renderer created above
  SDL_DestroyRenderer(renderer);

  //Destroy the window created above
  SDL_DestroyWindow(window);

  //Close all the systems of SDL initialized at the top
  SDL_Quit();

  return EXIT_SUCCESS;
}

The Metal Shander MyLibrary.metal:

/*
See LICENSE folder for this sample’s licensing information.

Abstract:
Metal shaders used for this sample
*/

#include <metal_stdlib>

using namespace metal;

// Include header shared between this Metal shader code and C code executing Metal API commands.
#include "MyLibraryShaderTypes.h"

// Vertex shader outputs and fragment shader inputs
struct RasterizerData
{
    // The [[position]] attribute of this member indicates that this value
    // is the clip space position of the vertex when this structure is
    // returned from the vertex function.
    float4 position [[position]];

    // Since this member does not have a special attribute, the rasterizer
    // interpolates its value with the values of the other triangle vertices
    // and then passes the interpolated value to the fragment shader for each
    // fragment in the triangle.
    float4 color;
};

vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
             constant AAPLVertex *vertices [[buffer(AAPLVertexInputIndexVertices)]],
             constant vector_uint2 *viewportSizePointer [[buffer(AAPLVertexInputIndexViewportSize)]])
{
    RasterizerData out;

    // Index into the array of positions to get the current vertex.
    // The positions are specified in pixel dimensions (i.e. a value of 100
    // is 100 pixels from the origin).
    float2 pixelSpacePosition = vertices[vertexID].position.xy;

    // Get the viewport size and cast to float.
    vector_float2 viewportSize = vector_float2(*viewportSizePointer);


    // To convert from positions in pixel space to positions in clip-space,
    //  divide the pixel coordinates by half the size of the viewport.
    out.position = vector_float4(0.0, 0.0, 0.0, 1.0);
    out.position.xy = pixelSpacePosition / (viewportSize / 2.0);

    // Pass the input color directly to the rasterizer.
    out.color = vertices[vertexID].color;

    return out;
}

fragment float4 fragmentShader(RasterizerData in [[stage_in]])
{
    // Return the interpolated color.
    return in.color;
}


And included shared header MyLibraryShaderTypes.h:

/*
See LICENSE folder for this sample’s licensing information.

Abstract:
Header containing types and enum constants shared between Metal shaders and C/ObjC source
*/

#ifndef MyLibraryShaderTypes_h
#define MyLibraryShaderTypes_h

#include <simd/simd.h>

// Buffer index values shared between shader and C code to ensure Metal shader buffer inputs
// match Metal API buffer set calls.
typedef enum AAPLVertexInputIndex
{
    AAPLVertexInputIndexVertices     = 0,
    AAPLVertexInputIndexViewportSize = 1,
} AAPLVertexInputIndex;

//  This structure defines the layout of vertices sent to the vertex
//  shader. This header is shared between the .metal shader and C code, to guarantee that
//  the layout of the vertex array in the C code matches the layout that the .metal
//  vertex shader expects.
typedef struct
{
    vector_float2 position;
    vector_float4 color;
} AAPLVertex;

#endif /* MyLibraryShaderTypes_h */