Last active
October 16, 2025 13:19
-
-
Save gingerBill/e1270f60a1739c266934599c2bee46f5 to your computer and use it in GitHub Desktop.
Metal in Odin Natively
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
| package objc_test | |
| import NS "vendor:darwin/Foundation" | |
| import MTL "vendor:darwin/Metal" | |
| import CA "vendor:darwin/QuartzCore" | |
| import SDL "vendor:sdl2" | |
| import "core:fmt" | |
| import "core:os" | |
| metal_main :: proc() -> (err: ^NS.Error) { | |
| SDL.SetHint(SDL.HINT_RENDER_DRIVER, "metal") | |
| SDL.setenv("METAL_DEVICE_WRAPPER_TYPE", "1", 0) | |
| SDL.Init({.VIDEO}) | |
| defer SDL.Quit() | |
| window := SDL.CreateWindow("Metal in Odin", | |
| SDL.WINDOWPOS_CENTERED, SDL.WINDOWPOS_CENTERED, | |
| 854, 480, | |
| {.ALLOW_HIGHDPI, .HIDDEN, .RESIZABLE}, | |
| ) | |
| defer SDL.DestroyWindow(window) | |
| window_system_info: SDL.SysWMinfo | |
| SDL.GetVersion(&window_system_info.version) | |
| SDL.GetWindowWMInfo(window, &window_system_info) | |
| assert(window_system_info.subsystem == .COCOA) | |
| native_window := (^NS.Window)(window_system_info.info.cocoa.window) | |
| device := MTL.CreateSystemDefaultDevice() | |
| fmt.println(device->name()->odinString()) | |
| swapchain := CA.MetalLayer.layer() | |
| swapchain->setDevice(device) | |
| swapchain->setPixelFormat(.BGRA8Unorm_sRGB) | |
| swapchain->setFramebufferOnly(true) | |
| swapchain->setFrame(native_window->frame()) | |
| native_window->contentView()->setLayer(swapchain) | |
| native_window->setOpaque(true) | |
| native_window->setBackgroundColor(nil) | |
| command_queue := device->newCommandQueue() | |
| compile_options := NS.new(MTL.CompileOptions) | |
| defer compile_options->release() | |
| program_source :: ` | |
| using namespace metal; | |
| struct ColoredVertex { | |
| float4 position [[position]]; | |
| float4 color; | |
| }; | |
| vertex ColoredVertex vertex_main(constant float4 *position [[buffer(0)]], | |
| constant float4 *color [[buffer(1)]], | |
| uint vid [[vertex_id]]) { | |
| ColoredVertex vert; | |
| vert.position = position[vid]; | |
| vert.color = color[vid]; | |
| return vert; | |
| } | |
| fragment float4 fragment_main(ColoredVertex vert [[stage_in]]) { | |
| return vert.color; | |
| } | |
| ` | |
| program_library := device->newLibraryWithSource(NS.AT(program_source), compile_options) or_return | |
| vertex_program := program_library->newFunctionWithName(NS.AT("vertex_main")) | |
| fragment_program := program_library->newFunctionWithName(NS.AT("fragment_main")) | |
| assert(vertex_program != nil) | |
| assert(fragment_program != nil) | |
| pipeline_state_descriptor := NS.new(MTL.RenderPipelineDescriptor) | |
| pipeline_state_descriptor->colorAttachments()->object(0)->setPixelFormat(.BGRA8Unorm_sRGB) | |
| pipeline_state_descriptor->setVertexFunction(vertex_program) | |
| pipeline_state_descriptor->setFragmentFunction(fragment_program) | |
| pipeline_state := device->newRenderPipelineState(pipeline_state_descriptor) or_return | |
| positions := [?][4]f32{ | |
| { 0.0, 0.5, 0, 1}, | |
| {-0.5, -0.5, 0, 1}, | |
| { 0.5, -0.5, 0, 1}, | |
| } | |
| colors := [?][4]f32{ | |
| {1, 0, 0, 1}, | |
| {0, 1, 0, 1}, | |
| {0, 0, 1, 1}, | |
| } | |
| position_buffer := device->newBufferWithSlice(positions[:], {}) | |
| color_buffer := device->newBufferWithSlice(colors[:], {}) | |
| SDL.ShowWindow(window) | |
| for quit := false; !quit; { | |
| for e: SDL.Event; SDL.PollEvent(&e) != 0; { | |
| #partial switch e.type { | |
| case .QUIT: | |
| quit = true | |
| case .KEYDOWN: | |
| if e.key.keysym.sym == .ESCAPE { | |
| quit = true | |
| } | |
| } | |
| } | |
| NS.scoped_autoreleasepool() | |
| drawable := swapchain->nextDrawable() | |
| assert(drawable != nil) | |
| pass := MTL.RenderPassDescriptor.renderPassDescriptor() | |
| color_attachment := pass->colorAttachments()->object(0) | |
| assert(color_attachment != nil) | |
| color_attachment->setClearColor(MTL.ClearColor{0.25, 0.5, 1.0, 1.0}) | |
| color_attachment->setLoadAction(.Clear) | |
| color_attachment->setStoreAction(.Store) | |
| color_attachment->setTexture(drawable->texture()) | |
| command_buffer := command_queue->commandBuffer() | |
| render_encoder := command_buffer->renderCommandEncoderWithDescriptor(pass) | |
| render_encoder->setRenderPipelineState(pipeline_state) | |
| render_encoder->setVertexBuffer(position_buffer, 0, 0) | |
| render_encoder->setVertexBuffer(color_buffer, 0, 1) | |
| render_encoder->drawPrimitivesWithInstanceCount(.Triangle, 0, 3, 1) | |
| render_encoder->endEncoding() | |
| command_buffer->presentDrawable(drawable) | |
| command_buffer->commit() | |
| } | |
| return nil | |
| } | |
| main :: proc() { | |
| err := metal_main() | |
| if err != nil { | |
| fmt.eprintln(err->localizedDescription()->odinString()) | |
| os.exit(1) | |
| } | |
| } |
Here's an updated gist, including the fixes from previous commenters. Additionally, I migrated the SDL2 API calls to SDL3 using this migration guide. Hopefully, it helps someone :)
package objc_test
import NS "core:sys/darwin/Foundation"
import MTL "vendor:darwin/Metal"
import CA "vendor:darwin/QuartzCore"
import SDL "vendor:sdl3"
import "core:fmt"
import "core:os"
metal_main :: proc() -> (err: ^NS.Error) {
env := SDL.GetEnvironment()
SDL.SetEnvironmentVariable(env, "METAL_DEVICE_WRAPPER_TYPE", "1", false)
if ok := SDL.InitSubSystem({.VIDEO}); !ok {
e: NS.Error
return NS.Error_init(&e)
}
defer SDL.Quit()
window := SDL.CreateWindow(
"Metal in Odin",
854,
480,
{.HIGH_PIXEL_DENSITY, .HIDDEN, .RESIZABLE, .METAL},
)
defer SDL.DestroyWindow(window)
native_window := (^NS.Window)(
SDL.GetPointerProperty(
SDL.GetWindowProperties(window),
SDL.PROP_WINDOW_COCOA_WINDOW_POINTER,
nil,
),
)
assert(native_window != nil)
device := MTL.CreateSystemDefaultDevice()
fmt.println(device->name()->odinString())
swapchain := CA.MetalLayer.layer()
swapchain->setDevice(device)
swapchain->setPixelFormat(.BGRA8Unorm_sRGB)
swapchain->setFramebufferOnly(true)
swapchain->setFrame(native_window->frame())
native_window->contentView()->setLayer(swapchain)
native_window->setOpaque(true)
native_window->setBackgroundColor(nil)
command_queue := device->newCommandQueue()
compile_options := NS.new(MTL.CompileOptions)
defer compile_options->release()
program_source :: `
using namespace metal;
struct ColoredVertex {
float4 position [[position]];
float4 color;
};
vertex ColoredVertex vertex_main(constant float4 *position [[buffer(0)]],
constant float4 *color [[buffer(1)]],
uint vid [[vertex_id]]) {
ColoredVertex vert;
vert.position = position[vid];
vert.color = color[vid];
return vert;
}
fragment float4 fragment_main(ColoredVertex vert [[stage_in]]) {
return vert.color;
}
`
program_library := device->newLibraryWithSource(
NS.AT(program_source),
compile_options,
) or_return
vertex_program := program_library->newFunctionWithName(NS.AT("vertex_main"))
fragment_program := program_library->newFunctionWithName(NS.AT("fragment_main"))
assert(vertex_program != nil)
assert(fragment_program != nil)
pipeline_state_descriptor := NS.new(MTL.RenderPipelineDescriptor)
pipeline_state_descriptor->colorAttachments()->object(0)->setPixelFormat(.BGRA8Unorm_sRGB)
pipeline_state_descriptor->setVertexFunction(vertex_program)
pipeline_state_descriptor->setFragmentFunction(fragment_program)
pipeline_state := device->newRenderPipelineState(pipeline_state_descriptor) or_return
positions := [?][4]f32{{0.0, 0.5, 0, 1}, {-0.5, -0.5, 0, 1}, {0.5, -0.5, 0, 1}}
colors := [?][4]f32{{1, 0, 0, 1}, {0, 1, 0, 1}, {0, 0, 1, 1}}
position_buffer := device->newBufferWithSlice(positions[:], {})
color_buffer := device->newBufferWithSlice(colors[:], {})
SDL.ShowWindow(window)
for quit := false; !quit; {
for e: SDL.Event; SDL.PollEvent(&e) != false; {
#partial switch e.type {
case .QUIT:
quit = true
case .KEY_DOWN:
if e.key.key == SDL.K_ESCAPE {
quit = true
}
}
}
NS.scoped_autoreleasepool()
drawable := swapchain->nextDrawable()
assert(drawable != nil)
pass := MTL.RenderPassDescriptor.renderPassDescriptor()
color_attachment := pass->colorAttachments()->object(0)
assert(color_attachment != nil)
color_attachment->setClearColor(MTL.ClearColor{0.25, 0.5, 1.0, 1.0})
color_attachment->setLoadAction(.Clear)
color_attachment->setStoreAction(.Store)
color_attachment->setTexture(drawable->texture())
command_buffer := command_queue->commandBuffer()
render_encoder := command_buffer->renderCommandEncoderWithDescriptor(pass)
render_encoder->setRenderPipelineState(pipeline_state)
render_encoder->setVertexBuffer(position_buffer, 0, 0)
render_encoder->setVertexBuffer(color_buffer, 0, 1)
render_encoder->drawPrimitivesWithInstanceCount(.Triangle, 0, 3, 1)
render_encoder->endEncoding()
command_buffer->presentDrawable(drawable)
command_buffer->commit()
}
return nil
}
main :: proc() {
err := metal_main()
if err != nil {
fmt.eprintln(err->localizedDescription()->odinString())
os.exit(1)
}
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@pixls
just change the zero to false and it works
https://odin-lang.org/docs/overview/