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 */