Skip to content

Instantly share code, notes, and snippets.

@nickav
Created August 11, 2025 22:23
Show Gist options
  • Select an option

  • Save nickav/4a774b0cdbd044946db468a81263737b to your computer and use it in GitHub Desktop.

Select an option

Save nickav/4a774b0cdbd044946db468a81263737b to your computer and use it in GitHub Desktop.
Minimal Example of a Metal Triangle using Metal and QuartzCore (no MetalKit)
//
// Build with:
// > clang -fobjc-arc triangle.m -o triangle -framework Metal -framework AppKit -framework QuartzCore
//
#import <Cocoa/Cocoa.h>
#import <Metal/Metal.h>
#import <QuartzCore/QuartzCore.h>
typedef struct {
float position[2];
float color[3];
} Vertex;
@interface MetalView : NSView
@property (nonatomic, strong) id<MTLDevice> device;
@property (nonatomic, strong) id<MTLCommandQueue> commandQueue;
@property (nonatomic, strong) id<MTLRenderPipelineState> pipelineState;
@property (nonatomic, strong) id<MTLBuffer> vertexBuffer;
@property (nonatomic, strong) CAMetalLayer *metalLayer;
@property (nonatomic, strong) NSTimer *renderTimer;
@end
@implementation MetalView
- (instancetype)initWithFrame:(NSRect)frameRect {
if (self = [super initWithFrame:frameRect]) {
self.device = MTLCreateSystemDefaultDevice();
self.commandQueue = [self.device newCommandQueue];
[self setupMetalLayer];
[self setupPipeline];
[self setupVertexBuffer];
[self startRenderLoop];
}
return self;
}
- (void)setupMetalLayer {
self.metalLayer = [CAMetalLayer layer];
self.metalLayer.device = self.device;
self.metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm;
self.metalLayer.framebufferOnly = YES;
self.layer = self.metalLayer;
self.wantsLayer = YES;
}
- (void)setupPipeline {
NSString *shaderSource = @"#include <metal_stdlib>\n"
"using namespace metal;\n"
"struct Vertex {\n"
" float2 position [[attribute(0)]];\n"
" float3 color [[attribute(1)]];\n"
"};\n"
"struct VertexOut {\n"
" float4 position [[position]];\n"
" float3 color;\n"
"};\n"
"vertex VertexOut vertex_main(Vertex in [[stage_in]]) {\n"
" VertexOut out;\n"
" out.position = float4(in.position, 0.0, 1.0);\n"
" out.color = in.color;\n"
" return out;\n"
"}\n"
"fragment float4 fragment_main(VertexOut in [[stage_in]]) {\n"
" return float4(in.color, 1.0);\n"
"}";
id<MTLLibrary> library = [self.device newLibraryWithSource:shaderSource options:nil error:nil];
id<MTLFunction> vertexFunction = [library newFunctionWithName:@"vertex_main"];
id<MTLFunction> fragmentFunction = [library newFunctionWithName:@"fragment_main"];
MTLRenderPipelineDescriptor *pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
pipelineDescriptor.vertexFunction = vertexFunction;
pipelineDescriptor.fragmentFunction = fragmentFunction;
pipelineDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;
MTLVertexDescriptor *vertexDescriptor = [[MTLVertexDescriptor alloc] init];
vertexDescriptor.attributes[0].format = MTLVertexFormatFloat2;
vertexDescriptor.attributes[0].offset = 0;
vertexDescriptor.attributes[0].bufferIndex = 0;
vertexDescriptor.attributes[1].format = MTLVertexFormatFloat3;
vertexDescriptor.attributes[1].offset = sizeof(float) * 2;
vertexDescriptor.attributes[1].bufferIndex = 0;
vertexDescriptor.layouts[0].stride = sizeof(Vertex);
vertexDescriptor.layouts[0].stepRate = 1;
vertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex;
pipelineDescriptor.vertexDescriptor = vertexDescriptor;
self.pipelineState = [self.device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:nil];
}
- (void)setupVertexBuffer {
Vertex vertices[] = {
{{0.0f, 0.5f}, {1.0f, 0.0f, 0.0f}},
{{-0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}},
{{0.5f, -0.5f}, {0.0f, 0.0f, 1.0f}}
};
self.vertexBuffer = [self.device newBufferWithBytes:vertices
length:sizeof(vertices)
options:MTLResourceStorageModeShared];
}
- (void)startRenderLoop {
self.renderTimer = [NSTimer scheduledTimerWithTimeInterval:1.0/60.0
target:self
selector:@selector(render)
userInfo:nil
repeats:YES];
}
- (void)render {
@autoreleasepool {
id<CAMetalDrawable> drawable = [self.metalLayer nextDrawable];
if (!drawable) return;
MTLRenderPassDescriptor *renderPassDescriptor = [[MTLRenderPassDescriptor alloc] init];
renderPassDescriptor.colorAttachments[0].texture = drawable.texture;
renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 1.0);
renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;
id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
[renderEncoder setRenderPipelineState:self.pipelineState];
[renderEncoder setVertexBuffer:self.vertexBuffer offset:0 atIndex:0];
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
[renderEncoder endEncoding];
[commandBuffer presentDrawable:drawable];
[commandBuffer commit];
}
}
- (void)setFrameSize:(NSSize)newSize {
[super setFrameSize:newSize];
self.metalLayer.drawableSize = CGSizeMake(newSize.width, newSize.height);
}
- (void)dealloc {
[self.renderTimer invalidate];
}
@end
@interface AppDelegate : NSObject <NSApplicationDelegate>
@property (nonatomic, strong) NSWindow *window;
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
self.window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable
backing:NSBackingStoreBuffered
defer:NO];
[self.window center];
[self.window setTitle:@"Metal Triangle"];
MetalView *metalView = [[MetalView alloc] initWithFrame:self.window.contentView.bounds];
metalView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
[self.window.contentView addSubview:metalView];
[self.window makeKeyAndOrderFront:nil];
[NSApp activateIgnoringOtherApps:YES];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSApplication *app = [NSApplication sharedApplication];
AppDelegate *delegate = [[AppDelegate alloc] init];
app.delegate = delegate;
[app run];
}
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment