Skip to content

Instantly share code, notes, and snippets.

@gingerBill
Last active October 16, 2025 13:19
Show Gist options
  • Save gingerBill/e1270f60a1739c266934599c2bee46f5 to your computer and use it in GitHub Desktop.
Save gingerBill/e1270f60a1739c266934599c2bee46f5 to your computer and use it in GitHub Desktop.
Metal in Odin Natively
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)
}
}
@DonnieTD
Copy link

DonnieTD commented Oct 5, 2024

@pixls

just change the zero to false and it works

https://odin-lang.org/docs/overview/

@Yakvi
Copy link

Yakvi commented Oct 16, 2025

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