├── .gitignore ├── Makefile ├── common.h ├── main.mm ├── readme.md ├── result.png └── shaders.metal /.gitignore: -------------------------------------------------------------------------------- 1 | main 2 | *.metallib 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: main shaders.metallib 2 | 3 | main: main.mm common.h 4 | clang++ -std=c++11 -framework Cocoa -framework Metal -framework MetalKit -fobjc-arc $< -o $@ 5 | 6 | %.air: %.metal common.h 7 | metal -O2 -std=osx-metal1.1 -o $@ $< 8 | 9 | %.metal-ar: %.air 10 | metal-ar r $@ $< 11 | 12 | %.metallib: %.metal-ar 13 | metallib -o $@ $< 14 | 15 | clean: 16 | rm -f main *.metallib *.metal-ar *.air 17 | -------------------------------------------------------------------------------- /common.h: -------------------------------------------------------------------------------- 1 | #ifndef COMMON_H 2 | #define COMMON_H 3 | 4 | #include 5 | 6 | enum VertexAttributes { 7 | VertexAttributePosition = 0, 8 | VertexAttributeColor = 1, 9 | }; 10 | 11 | enum BufferIndex { 12 | MeshVertexBuffer = 0, 13 | FrameUniformBuffer = 1, 14 | }; 15 | 16 | struct FrameUniforms { 17 | simd::float4x4 projectionViewModel; 18 | }; 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /main.mm: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import 5 | #import "common.h" 6 | 7 | @interface HelloMetalView : MTKView 8 | @end 9 | 10 | int main () { 11 | @autoreleasepool { 12 | // Application. 13 | [NSApplication sharedApplication]; 14 | [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; 15 | [NSApp activateIgnoringOtherApps:YES]; 16 | 17 | // Menu. 18 | NSMenu* bar = [NSMenu new]; 19 | NSMenuItem * barItem = [NSMenuItem new]; 20 | NSMenu* menu = [NSMenu new]; 21 | NSMenuItem* quit = [[NSMenuItem alloc] 22 | initWithTitle:@"Quit" 23 | action:@selector(terminate:) 24 | keyEquivalent:@"q"]; 25 | [bar addItem:barItem]; 26 | [barItem setSubmenu:menu]; 27 | [menu addItem:quit]; 28 | NSApp.mainMenu = bar; 29 | 30 | // Window. 31 | NSRect frame = NSMakeRect(0, 0, 256, 256); 32 | NSWindow* window = [[NSWindow alloc] 33 | initWithContentRect:frame styleMask:NSTitledWindowMask 34 | backing:NSBackingStoreBuffered defer:NO]; 35 | [window cascadeTopLeftFromPoint:NSMakePoint(20,20)]; 36 | window.title = [[NSProcessInfo processInfo] processName]; 37 | [window makeKeyAndOrderFront:nil]; 38 | 39 | // Custom MTKView. 40 | HelloMetalView* view = [[HelloMetalView alloc] initWithFrame:frame]; 41 | window.contentView = view; 42 | 43 | // Run. 44 | [NSApp run]; 45 | } 46 | return 0; 47 | } 48 | 49 | // Vertex structure on CPU memory. 50 | struct Vertex { 51 | float position[3]; 52 | unsigned char color[4]; 53 | }; 54 | 55 | // For pipeline executing. 56 | constexpr int uniformBufferCount = 3; 57 | 58 | // The main view. 59 | @implementation HelloMetalView { 60 | id _library; 61 | id _commandQueue; 62 | id _pipelineState; 63 | id _depthState; 64 | dispatch_semaphore_t _semaphore; 65 | id _uniformBuffers[uniformBufferCount]; 66 | id _vertexBuffer; 67 | int uniformBufferIndex; 68 | long frame; 69 | } 70 | 71 | - (id)initWithFrame:(CGRect)inFrame { 72 | id device = MTLCreateSystemDefaultDevice(); 73 | self = [super initWithFrame:inFrame device:device]; 74 | if (self) { 75 | [self setup]; 76 | } 77 | return self; 78 | } 79 | 80 | - (void)setup { 81 | // Set view settings. 82 | self.colorPixelFormat = MTLPixelFormatBGRA8Unorm; 83 | self.depthStencilPixelFormat = MTLPixelFormatDepth32Float_Stencil8; 84 | 85 | // Load shaders. 86 | NSError *error = nil; 87 | _library = [self.device newLibraryWithFile: @"shaders.metallib" error:&error]; 88 | if (!_library) { 89 | NSLog(@"Failed to load library. error %@", error); 90 | exit(0); 91 | } 92 | id vertFunc = [_library newFunctionWithName:@"vert"]; 93 | id fragFunc = [_library newFunctionWithName:@"frag"]; 94 | 95 | // Create depth state. 96 | MTLDepthStencilDescriptor *depthDesc = [MTLDepthStencilDescriptor new]; 97 | depthDesc.depthCompareFunction = MTLCompareFunctionLess; 98 | depthDesc.depthWriteEnabled = YES; 99 | _depthState = [self.device newDepthStencilStateWithDescriptor:depthDesc]; 100 | 101 | // Create vertex descriptor. 102 | MTLVertexDescriptor *vertDesc = [MTLVertexDescriptor new]; 103 | vertDesc.attributes[VertexAttributePosition].format = MTLVertexFormatFloat3; 104 | vertDesc.attributes[VertexAttributePosition].offset = 0; 105 | vertDesc.attributes[VertexAttributePosition].bufferIndex = MeshVertexBuffer; 106 | vertDesc.attributes[VertexAttributeColor].format = MTLVertexFormatUChar4; 107 | vertDesc.attributes[VertexAttributeColor].offset = sizeof(Vertex::position); 108 | vertDesc.attributes[VertexAttributeColor].bufferIndex = MeshVertexBuffer; 109 | vertDesc.layouts[MeshVertexBuffer].stride = sizeof(Vertex); 110 | vertDesc.layouts[MeshVertexBuffer].stepRate = 1; 111 | vertDesc.layouts[MeshVertexBuffer].stepFunction = MTLVertexStepFunctionPerVertex; 112 | 113 | // Create pipeline state. 114 | MTLRenderPipelineDescriptor *pipelineDesc = [MTLRenderPipelineDescriptor new]; 115 | pipelineDesc.sampleCount = self.sampleCount; 116 | pipelineDesc.vertexFunction = vertFunc; 117 | pipelineDesc.fragmentFunction = fragFunc; 118 | pipelineDesc.vertexDescriptor = vertDesc; 119 | pipelineDesc.colorAttachments[0].pixelFormat = self.colorPixelFormat; 120 | pipelineDesc.depthAttachmentPixelFormat = self.depthStencilPixelFormat; 121 | pipelineDesc.stencilAttachmentPixelFormat = self.depthStencilPixelFormat; 122 | _pipelineState = [self.device newRenderPipelineStateWithDescriptor:pipelineDesc error:&error]; 123 | if (!_pipelineState) { 124 | NSLog(@"Failed to create pipeline state, error %@", error); 125 | exit(0); 126 | } 127 | 128 | // Create vertices. 129 | Vertex verts[] = { 130 | Vertex{{-0.5, -0.5, 0}, {255, 0, 0, 255}}, 131 | Vertex{{0, 0.5, 0}, {0, 255, 0, 255}}, 132 | Vertex{{0.5, -0.5, 0}, {0, 0, 255, 255}} 133 | }; 134 | _vertexBuffer = [self.device newBufferWithBytes:verts 135 | length:sizeof(verts) 136 | options:MTLResourceStorageModePrivate]; 137 | 138 | // Create uniform buffers. 139 | for (int i = 0; i < uniformBufferCount; i++) { 140 | _uniformBuffers[i] = [self.device newBufferWithLength:sizeof(FrameUniforms) 141 | options:MTLResourceCPUCacheModeWriteCombined]; 142 | } 143 | frame = 0; 144 | 145 | // Create semaphore for each uniform buffer. 146 | _semaphore = dispatch_semaphore_create(uniformBufferCount); 147 | uniformBufferIndex = 0; 148 | 149 | // Create command queue 150 | _commandQueue = [self.device newCommandQueue]; 151 | 152 | } 153 | 154 | - (void)drawRect:(CGRect)rect { 155 | // Wait for an available uniform buffer. 156 | dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER); 157 | 158 | // Animation. 159 | frame++; 160 | float rad = frame * 0.01f; 161 | float sin = std::sin(rad), cos = std::cos(rad); 162 | simd::float4x4 rot(simd::float4{cos, -sin, 0, 0}, 163 | simd::float4{sin, cos, 0, 0}, 164 | simd::float4{0, -0, 1, 0}, 165 | simd::float4{0, 0, 0, 1}); 166 | 167 | // Update the current uniform buffer. 168 | uniformBufferIndex = (uniformBufferIndex + 1) % uniformBufferCount; 169 | FrameUniforms *uniforms = (FrameUniforms *)[_uniformBuffers[uniformBufferIndex] contents]; 170 | uniforms->projectionViewModel = rot; 171 | 172 | // Create a command buffer. 173 | id commandBuffer = [_commandQueue commandBuffer]; 174 | 175 | // Encode render command. 176 | id encoder = 177 | [commandBuffer renderCommandEncoderWithDescriptor:self.currentRenderPassDescriptor]; 178 | [encoder setViewport:{0, 0, self.drawableSize.width, self.drawableSize.height, 0, 1}]; 179 | [encoder setDepthStencilState:_depthState]; 180 | [encoder setRenderPipelineState:_pipelineState]; 181 | [encoder setVertexBuffer:_uniformBuffers[uniformBufferIndex] 182 | offset:0 atIndex:FrameUniformBuffer]; 183 | [encoder setVertexBuffer:_vertexBuffer offset:0 atIndex:MeshVertexBuffer]; 184 | [encoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3]; 185 | [encoder endEncoding]; 186 | 187 | // Set callback for semaphore. 188 | __block dispatch_semaphore_t semaphore = _semaphore; 189 | [commandBuffer addCompletedHandler:^(id buffer) { 190 | dispatch_semaphore_signal(semaphore); 191 | }]; 192 | [commandBuffer presentDrawable:self.currentDrawable]; 193 | [commandBuffer commit]; 194 | 195 | // Draw children. 196 | [super drawRect:rect]; 197 | } 198 | 199 | @end 200 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Metal Programming without Xcode 2 | A simple example of Metal written in Objective-C++ which can be compiled in command-line. 3 | ![result.png](result.png) 4 | ## How to pre-compile Metal shaders 5 | ### Export PATH for Metal compiler 6 | export PATH="$PATH:/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/usr/bin 7 | 8 | or 9 | 10 | echo 'export PATH="$PATH:/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/usr/bin"' >> ~/.bashrc 11 | 12 | ### Compile 13 | Just run the following command: 14 | 15 | make shaders.metallib 16 | 17 | or, if you want to do it manually: 18 | #### Compile .metal file into .air 19 | metal -std=osx-metal1.1 -o shaders.air shaders.metal 20 | 21 | #### Archive .air into .metal-ar 22 | metal-ar r shaders.metal-ar shaders.air 23 | 24 | #### Make .metallib from metal-ar 25 | metallib -o shaders.metallib shaders.metal-ar 26 | -------------------------------------------------------------------------------- /result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n-yoda/metal-without-xcode/887456f80af1ea738005d94857fd8ac1e21dfbc1/result.png -------------------------------------------------------------------------------- /shaders.metal: -------------------------------------------------------------------------------- 1 | #include 2 | #include "common.h" 3 | 4 | using namespace metal; 5 | 6 | struct VertexInput { 7 | float3 position [[attribute(VertexAttributePosition)]]; 8 | half4 color [[attribute(VertexAttributeColor)]]; 9 | }; 10 | 11 | struct ShaderInOut { 12 | float4 position [[position]]; 13 | half4 color; 14 | }; 15 | 16 | vertex ShaderInOut vert(VertexInput in [[stage_in]], 17 | constant FrameUniforms& uniforms [[buffer(FrameUniformBuffer)]]) { 18 | ShaderInOut out; 19 | float4 pos4 = float4(in.position, 1.0); 20 | out.position = uniforms.projectionViewModel * pos4; 21 | out.color = in.color / 255.0; 22 | return out; 23 | } 24 | 25 | fragment half4 frag(ShaderInOut in [[stage_in]]) { 26 | return in.color; 27 | } 28 | --------------------------------------------------------------------------------