Created
August 11, 2025 22:23
-
-
Save nickav/4a774b0cdbd044946db468a81263737b to your computer and use it in GitHub Desktop.
Minimal Example of a Metal Triangle using Metal and QuartzCore (no MetalKit)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // 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