1package main
2
3import NS "core:sys/darwin/Foundation"
4import MTL "vendor:darwin/Metal"
5import CA "vendor:darwin/QuartzCore"
6
7import SDL "vendor:sdl2"
8
9import "core:fmt"
10import "core:os"
11import "core:math"
12import glm "core:math/linalg/glsl"
13
14
15Vertex_Data :: struct {
16 position: glm.vec3,
17 normal: glm.vec3,
18 texcoord: glm.vec2,
19}
20
21Instance_Data :: struct #align(16) {
22 transform: glm.mat4,
23 color: glm.vec4,
24 normal_transform: glm.mat3,
25}
26
27INSTANCE_WIDTH :: 10
28INSTANCE_HEIGHT :: 10
29INSTANCE_DEPTH :: 10
30NUM_INSTANCES :: INSTANCE_WIDTH*INSTANCE_HEIGHT*INSTANCE_DEPTH
31
32TEXTURE_WIDTH :: 128
33TEXTURE_HEIGHT :: 128
34
35Camera_Data :: struct #align(16) {
36 perspective_transform: glm.mat4,
37 world_transform: glm.mat4,
38 world_normal_transform: glm.mat3,
39}
40
41build_shaders :: proc(device: ^MTL.Device) -> (library: ^MTL.Library, pso: ^MTL.RenderPipelineState, err: ^NS.Error) {
42 shader_src := `
43 #include <metal_stdlib>
44 using namespace metal;
45
46 struct v2f {
47 float4 position [[position]];
48 float3 normal;
49 half3 color;
50 float2 texcoord;
51 };
52
53 struct Vertex_Data {
54 packed_float3 position;
55 packed_float3 normal;
56 packed_float2 texcoord;
57 };
58
59 struct Instance_Data {
60 float4x4 transform;
61 float4 color;
62 float3x3 normal_transform;
63 };
64
65 struct Camera_Data {
66 float4x4 perspective_transform;
67 float4x4 world_transform;
68 float3x3 world_normal_transform;
69 };
70
71 v2f vertex vertex_main(device const Vertex_Data* vertex_data [[buffer(0)]],
72 device const Instance_Data* instance_data [[buffer(1)]],
73 device const Camera_Data& camera_data [[buffer(2)]],
74 uint vertex_id [[vertex_id]],
75 uint instance_id [[instance_id]]) {
76 v2f o;
77
78 const device Vertex_Data& vd = vertex_data[vertex_id];
79 const device Instance_Data& id = instance_data[instance_id];
80
81 float4 pos = float4(vd.position, 1.0);
82 pos = id.transform * pos;
83 pos = camera_data.perspective_transform * camera_data.world_transform * pos;
84 o.position = pos;
85
86 float3 normal = id.normal_transform * float3(vd.normal);
87 normal = camera_data.world_normal_transform * normal;
88 o.normal = normal;
89
90 o.texcoord = float2(vd.texcoord.xy);
91
92 o.color = half3(id.color.rgb);
93 return o;
94 }
95
96 half4 fragment fragment_main(v2f in [[stage_in]],
97 texture2d<half, access::sample> tex [[texture(0)]]) {
98 constexpr sampler s(address::repeat, filter::linear);
99 half3 texel = tex.sample(s, in.texcoord).rgb;
100
101 // assume light coming from front-top-right
102 float3 l = normalize(float3(1.0, 1.0, 0.8));
103 float3 n = normalize(in.normal);
104
105 float ndotl = saturate(dot(n, l));
106
107 half3 illum = in.color * texel * 0.1 + in.color * texel * ndotl;
108 return half4(illum, 1.0);
109 }
110 `
111 shader_src_str := NS.String.alloc()->initWithOdinString(shader_src)
112 defer shader_src_str->release()
113
114 library = device->newLibraryWithSource(shader_src_str, nil) or_return
115
116 vertex_function := library->newFunctionWithName(NS.AT("vertex_main"))
117 fragment_function := library->newFunctionWithName(NS.AT("fragment_main"))
118 defer vertex_function->release()
119 defer fragment_function->release()
120
121 desc := MTL.RenderPipelineDescriptor.alloc()->init()
122 defer desc->release()
123
124 desc->setVertexFunction(vertex_function)
125 desc->setFragmentFunction(fragment_function)
126 desc->colorAttachments()->object(0)->setPixelFormat(.BGRA8Unorm_sRGB)
127 desc->setDepthAttachmentPixelFormat(.Depth16Unorm)
128
129 pso = device->newRenderPipelineStateWithDescriptor(desc) or_return
130 return
131}
132
133build_buffers :: proc(device: ^MTL.Device) -> (vertex_buffer, index_buffer, instance_buffer: ^MTL.Buffer) {
134 s :: 0.5
135 positions := []Vertex_Data{
136
137
138 {{-s, -s, +s}, { 0, 0, 1}, {0, 1}},
139 {{+s, -s, +s}, { 0, 0, 1}, {1, 1}},
140 {{+s, +s, +s}, { 0, 0, 1}, {1, 0}},
141 {{-s, +s, +s}, { 0, 0, 1}, {0, 0}},
142
143 {{+s, -s, +s}, { 1, 0, 0}, {0, 1}},
144 {{+s, -s, -s}, { 1, 0, 0}, {1, 1}},
145 {{+s, +s, -s}, { 1, 0, 0}, {1, 0}},
146 {{+s, +s, +s}, { 1, 0, 0}, {0, 0}},
147
148 {{+s, -s, -s}, { 0, 0, -1}, {0, 1}},
149 {{-s, -s, -s}, { 0, 0, -1}, {1, 1}},
150 {{-s, +s, -s}, { 0, 0, -1}, {1, 0}},
151 {{+s, +s, -s}, { 0, 0, -1}, {0, 0}},
152
153 {{-s, -s, -s}, {-1, 0, 0}, {0, 1}},
154 {{-s, -s, +s}, {-1, 0, 0}, {1, 1}},
155 {{-s, +s, +s}, {-1, 0, 0}, {1, 0}},
156 {{-s, +s, -s}, {-1, 0, 0}, {0, 0}},
157
158 {{-s, +s, +s}, { 0, 1, 0}, {0, 1}},
159 {{+s, +s, +s}, { 0, 1, 0}, {1, 1}},
160 {{+s, +s, -s}, { 0, 1, 0}, {1, 0}},
161 {{-s, +s, -s}, { 0, 1, 0}, {0, 0}},
162
163 {{-s, -s, -s}, { 0, -1, 0}, {0, 1}},
164 {{+s, -s, -s}, { 0, -1, 0}, {1, 1}},
165 {{+s, -s, +s}, { 0, -1, 0}, {1, 0}},
166 {{-s, -s, +s}, { 0, -1, 0}, {0, 0}},
167 }
168 indices := []u16{
169 0, 1, 2, 2, 3, 0,
170 4, 5, 6, 6, 7, 4,
171 8, 9, 10, 10, 11, 8,
172 12, 13, 14, 14, 15, 12,
173 16, 17, 18, 18, 19, 16,
174 20, 21, 22, 22, 23, 20,
175 }
176
177 vertex_buffer = device->newBufferWithSlice(positions[:], {.StorageModeManaged})
178 index_buffer = device->newBufferWithSlice(indices[:], {.StorageModeManaged})
179 instance_buffer = device->newBuffer(NUM_INSTANCES*size_of(Instance_Data), {.StorageModeManaged})
180 return
181}
182
183build_texture :: proc(device: ^MTL.Device) -> ^MTL.Texture {
184 desc := MTL.TextureDescriptor.alloc()->init()
185 defer desc->release()
186
187 desc->setWidth(TEXTURE_WIDTH)
188 desc->setHeight(TEXTURE_HEIGHT)
189 desc->setPixelFormat(.RGBA8Unorm)
190 desc->setStorageMode(.Managed)
191 desc->setUsage({.ShaderRead, .ShaderWrite})
192
193 return device->newTextureWithDescriptor(desc)
194}
195
196build_compute_pipeline :: proc(device: ^MTL.Device) -> (pso: ^MTL.ComputePipelineState, err: ^NS.Error) {
197 kernel_src := `
198 #include <metal_stdlib>
199 using namespace metal;
200
201 kernel void mandelbrot_set(texture2d<half, access::write> tex [[texture(0)]],
202 uint2 index [[thread_position_in_grid]],
203 uint2 grid_size [[threads_per_grid]]) {
204 // Scale
205 float x0 = 2.0 * index.x / grid_size.x - 1.5;
206 float y0 = 2.0 * index.y / grid_size.y - 1.0;
207
208 // Implement Mandelbrot set
209 float x = 0.0;
210 float y = 0.0;
211 uint iteration = 0;
212 uint max_iteration = 1000;
213 float xtmp = 0.0;
214 while (x * x + y * y <= 4 && iteration < max_iteration) {
215 xtmp = x * x - y * y + x0;
216 y = 2 * x * y + y0;
217 x = xtmp;
218 iteration += 1;
219 }
220
221 // Convert iteration result to colors
222 half color = (0.5 + 0.5 * cos(3.0 + iteration * 0.15));
223 tex.write(half4(color, color, color, 1.0), index, 0);
224 }`
225
226 kernel_src_str := NS.String.alloc()->initWithOdinString(kernel_src)
227 defer kernel_src_str->release()
228
229 compute_library := device->newLibraryWithSource(kernel_src_str, nil) or_return
230 defer compute_library->release()
231
232 mandelbrot_set := compute_library->newFunctionWithName(NS.AT("mandelbrot_set"))
233 defer mandelbrot_set->release()
234
235 return device->newComputePipelineStateWithFunction(mandelbrot_set)
236}
237
238generate_mandelbrot_texture :: proc(command_queue: ^MTL.CommandQueue, compute_pso: ^MTL.ComputePipelineState, texture: ^MTL.Texture) {
239 command_buffer := command_queue->commandBuffer()
240 defer command_buffer->release()
241
242 compute_encoder := command_buffer->computeCommandEncoder()
243
244 compute_encoder->setComputePipelineState(compute_pso)
245 compute_encoder->setTexture(texture, 0)
246
247 grid_size := MTL.Size{TEXTURE_WIDTH, TEXTURE_HEIGHT, 1}
248 thread_group_size := MTL.Size{NS.Integer(compute_pso->maxTotalThreadsPerThreadgroup()), 1, 1}
249
250 compute_encoder->dispatchThreads(grid_size, thread_group_size)
251 compute_encoder->endEncoding()
252
253 command_buffer->commit()
254}
255
256metal_main :: proc() -> (err: ^NS.Error) {
257 SDL.SetHint(SDL.HINT_RENDER_DRIVER, "metal")
258 SDL.setenv("METAL_DEVICE_WRAPPER_TYPE", "1", 0)
259 SDL.Init({.VIDEO})
260 defer SDL.Quit()
261
262 window := SDL.CreateWindow("Metal in Odin - 08 Compute",
263 SDL.WINDOWPOS_CENTERED, SDL.WINDOWPOS_CENTERED,
264 1024, 1024,
265 {.ALLOW_HIGHDPI, .HIDDEN, .RESIZABLE},
266 )
267 defer SDL.DestroyWindow(window)
268
269 window_system_info: SDL.SysWMinfo
270 SDL.GetVersion(&window_system_info.version)
271 SDL.GetWindowWMInfo(window, &window_system_info)
272 assert(window_system_info.subsystem == .COCOA)
273
274 native_window := (^NS.Window)(window_system_info.info.cocoa.window)
275
276 device := MTL.CreateSystemDefaultDevice()
277 defer device->release()
278
279 fmt.println(device->name()->odinString())
280
281 swapchain := CA.MetalLayer.layer()
282 defer swapchain->release()
283
284 swapchain->setDevice(device)
285 swapchain->setPixelFormat(.BGRA8Unorm_sRGB)
286 swapchain->setFramebufferOnly(true)
287 swapchain->setFrame(native_window->frame())
288
289 native_window->contentView()->setLayer(swapchain)
290 native_window->setOpaque(true)
291 native_window->setBackgroundColor(nil)
292
293 library, pso := build_shaders(device) or_return
294 defer library->release()
295 defer pso->release()
296
297
298 depth_stencil_state: ^MTL.DepthStencilState
299 depth_desc := MTL.DepthStencilDescriptor.alloc()->init()
300 depth_desc->setDepthCompareFunction(.Less)
301 depth_desc->setDepthWriteEnabled(true)
302 depth_stencil_state = device->newDepthStencilState(depth_desc)
303 depth_desc->release()
304
305 vertex_buffer, index_buffer, instance_buffer := build_buffers(device)
306 defer vertex_buffer->release()
307 defer index_buffer->release()
308 defer instance_buffer->release()
309
310 camera_buffer := device->newBuffer(size_of(Camera_Data), {.StorageModeManaged})
311 defer camera_buffer->release()
312
313 depth_texture: ^MTL.Texture = nil
314 defer if depth_texture != nil { depth_texture->release() }
315
316 compute_pso := build_compute_pipeline(device) or_return
317 defer compute_pso->release()
318
319 command_queue := device->newCommandQueue()
320 defer command_queue->release()
321
322 texture := build_texture(device)
323 defer texture->release()
324
325 generate_mandelbrot_texture(command_queue, compute_pso, texture)
326
327 SDL.ShowWindow(window)
328 for quit := false; !quit; {
329 for e: SDL.Event; SDL.PollEvent(&e); {
330 #partial switch e.type {
331 case .QUIT:
332 quit = true
333 case .KEYDOWN:
334 if e.key.keysym.sym == .ESCAPE {
335 quit = true
336 }
337 }
338 }
339
340 w, h: i32
341 SDL.GetWindowSize(window, &w, &h)
342 aspect_ratio := f32(w)/max(f32(h), 1)
343
344
345 {
346 @static angle: f32
347 angle += 0.002
348
349 object_position := glm.vec3{0, 0, -10}
350 rt := glm.mat4Translate(object_position)
351 rr1 := glm.mat4Rotate({0, 1, 0}, -angle)
352 rr0 := glm.mat4Rotate({1, 0, 0}, angle*0.5)
353 rt_inv := glm.mat4Translate(-object_position)
354 full_obj_rot := rt * rr1 * rr0 * rt_inv
355
356
357 ix, iy, iz := 0, 0, 0
358
359 instance_data := instance_buffer->contentsAsSlice([]Instance_Data)[:NUM_INSTANCES]
360 for &instance, idx in instance_data {
361 if ix == INSTANCE_WIDTH {
362 ix = 0
363 iy += 1
364 }
365 if iy == INSTANCE_HEIGHT {
366 iy = 0
367 iz += 1
368 }
369 defer ix += 1
370
371 scl :: 0.2
372
373 scale := glm.mat4Scale({scl, scl, scl})
374 zrot := glm.mat4Rotate({0, 0, 1}, angle * math.sin(f32(ix)))
375 yrot := glm.mat4Rotate({0, 1, 0}, angle * math.cos(f32(iy)))
376
377 pos := glm.vec3{
378 (f32(ix) - INSTANCE_WIDTH * 0.5) * 2*scl + scl,
379 (f32(iy) - INSTANCE_HEIGHT* 0.5) * 2*scl + scl,
380 (f32(iz) - INSTANCE_DEPTH * 0.5) * 2*scl,
381 }
382
383 translate := glm.mat4Translate(object_position + pos)
384
385 instance.transform = full_obj_rot * translate * yrot * zrot * scale
386 instance.normal_transform = glm.mat3(instance.transform)
387
388 r := f32(idx) / NUM_INSTANCES
389 instance.color = {r, 1-r, math.sin(math.TAU * r), 1}
390
391 }
392 sz := NS.UInteger(len(instance_data)*size_of(instance_data[0]))
393 instance_buffer->didModifyRange(NS.Range_Make(0, sz))
394 }
395
396 {
397 camera_data := camera_buffer->contentsAsType(Camera_Data)
398 camera_data.perspective_transform = glm.mat4Perspective(glm.radians_f32(45), aspect_ratio, 0.03, 500)
399 camera_data.world_transform = 1
400 camera_data.world_normal_transform = glm.mat3(camera_data.world_transform)
401
402 camera_buffer->didModifyRange(NS.Range_Make(0, size_of(Camera_Data)))
403 }
404
405 if depth_texture == nil ||
406 depth_texture->width() != NS.UInteger(w) ||
407 depth_texture->height() != NS.UInteger(h) {
408 desc := MTL.TextureDescriptor.texture2DDescriptorWithPixelFormat(
409 pixelFormat = .Depth16Unorm,
410 width = NS.UInteger(w),
411 height = NS.UInteger(h),
412 mipmapped = false,
413 )
414 defer desc->release()
415
416 desc->setUsage({.RenderTarget})
417 desc->setStorageMode(.Private)
418
419 if depth_texture != nil {
420 depth_texture->release()
421 }
422
423 depth_texture = device->newTextureWithDescriptor(desc)
424 }
425
426
427 drawable := swapchain->nextDrawable()
428 assert(drawable != nil)
429 defer drawable->release()
430
431 pass := MTL.RenderPassDescriptor.renderPassDescriptor()
432 defer pass->release()
433
434 color_attachment := pass->colorAttachments()->object(0)
435 assert(color_attachment != nil)
436 color_attachment->setClearColor(MTL.ClearColor{0.1, 0.1, 0.1, 1.0})
437 color_attachment->setLoadAction(.Clear)
438 color_attachment->setStoreAction(.Store)
439 color_attachment->setTexture(drawable->texture())
440
441 depth_attachment := pass->depthAttachment()
442 depth_attachment->setTexture(depth_texture)
443 depth_attachment->setClearDepth(1.0)
444 depth_attachment->setLoadAction(.Clear)
445 depth_attachment->setStoreAction(.Store)
446
447 command_buffer := command_queue->commandBuffer()
448 defer command_buffer->release()
449
450 render_encoder := command_buffer->renderCommandEncoderWithDescriptor(pass)
451 defer render_encoder->release()
452
453 render_encoder->setRenderPipelineState(pso)
454 render_encoder->setDepthStencilState(depth_stencil_state)
455
456 render_encoder->setVertexBuffer(buffer=vertex_buffer, offset=0, index=0)
457 render_encoder->setVertexBuffer(buffer=instance_buffer, offset=0, index=1)
458 render_encoder->setVertexBuffer(buffer=camera_buffer, offset=0, index=2)
459
460 render_encoder->setFragmentTexture(texture, 0)
461
462 render_encoder->setCullMode(.Back)
463 render_encoder->setFrontFacingWinding(.CounterClockwise)
464 render_encoder->drawIndexedPrimitivesWithInstanceCount(.Triangle, 6*6, .UInt16, index_buffer, 0, NUM_INSTANCES)
465
466 render_encoder->endEncoding()
467
468 command_buffer->presentDrawable(drawable)
469 command_buffer->commit()
470 }
471
472 return nil
473}
474
475main :: proc() {
476 err := metal_main()
477 if err != nil {
478 fmt.eprintln(err->localizedDescription()->odinString())
479 os.exit(1)
480 }
481}