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, texture_animation_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 texture_animation_buffer = device->newBuffer(size_of(u32), {.StorageModeManaged})
181 return
182}
183
184build_texture :: proc(device: ^MTL.Device) -> ^MTL.Texture {
185 desc := MTL.TextureDescriptor.alloc()->init()
186 defer desc->release()
187
188 desc->setWidth(TEXTURE_WIDTH)
189 desc->setHeight(TEXTURE_HEIGHT)
190 desc->setPixelFormat(.RGBA8Unorm)
191 desc->setStorageMode(.Managed)
192 desc->setUsage({.ShaderRead, .ShaderWrite})
193
194 return device->newTextureWithDescriptor(desc)
195}
196
197build_compute_pipeline :: proc(device: ^MTL.Device) -> (pso: ^MTL.ComputePipelineState, err: ^NS.Error) {
198 kernel_src := `
199 #include <metal_stdlib>
200 using namespace metal;
201
202 kernel void mandelbrot_set(texture2d<half, access::write> tex [[texture(0)]],
203 uint2 index [[thread_position_in_grid]],
204 uint2 grid_size [[threads_per_grid]],
205 device const uint* frame [[buffer(0)]]) {
206 constexpr float ANIMATION_FREQUENCY = 0.01;
207 constexpr float ANIMATION_SPEED = 4;
208 constexpr float ANIMATION_SCALE_LOW = 0.62;
209 constexpr float ANIMATION_SCALE = 0.38;
210
211 constexpr float2 MANDELBROT_PIXEL_OFFSET = {-0.2, -0.35};
212 constexpr float2 MANDELBROT_ORIGIN = {-1.2, -0.32};
213 constexpr float2 MANDELBROT_SCALE = {2.2, 2.0};
214
215 // Map time to zoom value in [ANIMATION_SCALE_LOW, 1]
216 float zoom = ANIMATION_SCALE_LOW + ANIMATION_SCALE * cos(ANIMATION_FREQUENCY * *frame);
217 // Speed up zooming
218 zoom = pow(zoom, ANIMATION_SPEED);
219
220 //Scale
221 float x0 = zoom * MANDELBROT_SCALE.x * ((float)index.x / grid_size.x + MANDELBROT_PIXEL_OFFSET.x) + MANDELBROT_ORIGIN.x;
222 float y0 = zoom * MANDELBROT_SCALE.y * ((float)index.y / grid_size.y + MANDELBROT_PIXEL_OFFSET.y) + MANDELBROT_ORIGIN.y;
223
224 // Implement Mandelbrot set
225 float x = 0.0;
226 float y = 0.0;
227 uint iteration = 0;
228 uint max_iteration = 1000;
229 float xtmp = 0.0;
230 while (x * x + y * y <= 4 && iteration < max_iteration) {
231 xtmp = x * x - y * y + x0;
232 y = 2 * x * y + y0;
233 x = xtmp;
234 iteration += 1;
235 }
236
237 // Convert iteration result to colors
238 half color = (0.5 + 0.5 * cos(3.0 + iteration * 0.15));
239 tex.write(half4(color, color, color, 1.0), index, 0);
240 }`
241
242 kernel_src_str := NS.String.alloc()->initWithOdinString(kernel_src)
243 defer kernel_src_str->release()
244
245 compute_library := device->newLibraryWithSource(kernel_src_str, nil) or_return
246 defer compute_library->release()
247
248 mandelbrot_set := compute_library->newFunctionWithName(NS.AT("mandelbrot_set"))
249 defer mandelbrot_set->release()
250
251 return device->newComputePipelineStateWithFunction(mandelbrot_set)
252}
253
254generate_mandelbrot_texture :: proc(
255 command_buffer: ^MTL.CommandBuffer,
256 compute_pso: ^MTL.ComputePipelineState,
257 texture_animation_buffer: ^MTL.Buffer,
258 texture: ^MTL.Texture) {
259
260 @static animation_index: u32
261 ptr := texture_animation_buffer->contentsAsType(u32)
262 ptr^ = animation_index
263 animation_index = (animation_index + 1) % 5000
264
265
266 compute_encoder := command_buffer->computeCommandEncoder()
267
268 compute_encoder->setComputePipelineState(compute_pso)
269 compute_encoder->setTexture(texture, 0)
270 compute_encoder->setBuffer(texture_animation_buffer, 0, 0)
271
272 grid_size := MTL.Size{TEXTURE_WIDTH, TEXTURE_HEIGHT, 1}
273 thread_group_size := MTL.Size{NS.Integer(compute_pso->maxTotalThreadsPerThreadgroup()), 1, 1}
274
275 compute_encoder->dispatchThreads(grid_size, thread_group_size)
276 compute_encoder->endEncoding()
277}
278
279metal_main :: proc() -> (err: ^NS.Error) {
280 SDL.SetHint(SDL.HINT_RENDER_DRIVER, "metal")
281 SDL.setenv("METAL_DEVICE_WRAPPER_TYPE", "1", 0)
282 SDL.Init({.VIDEO})
283 defer SDL.Quit()
284
285 window := SDL.CreateWindow("Metal in Odin - 09 Compute to Render",
286 SDL.WINDOWPOS_CENTERED, SDL.WINDOWPOS_CENTERED,
287 1024, 1024,
288 {.ALLOW_HIGHDPI, .HIDDEN, .RESIZABLE},
289 )
290 defer SDL.DestroyWindow(window)
291
292 window_system_info: SDL.SysWMinfo
293 SDL.GetVersion(&window_system_info.version)
294 SDL.GetWindowWMInfo(window, &window_system_info)
295 assert(window_system_info.subsystem == .COCOA)
296
297 native_window := (^NS.Window)(window_system_info.info.cocoa.window)
298
299 device := MTL.CreateSystemDefaultDevice()
300 defer device->release()
301
302 fmt.println(device->name()->odinString())
303
304 swapchain := CA.MetalLayer.layer()
305 defer swapchain->release()
306
307 swapchain->setDevice(device)
308 swapchain->setPixelFormat(.BGRA8Unorm_sRGB)
309 swapchain->setFramebufferOnly(true)
310 swapchain->setFrame(native_window->frame())
311
312 native_window->contentView()->setLayer(swapchain)
313 native_window->setOpaque(true)
314 native_window->setBackgroundColor(nil)
315
316 library, pso := build_shaders(device) or_return
317 defer library->release()
318 defer pso->release()
319
320
321 depth_stencil_state: ^MTL.DepthStencilState
322 depth_desc := MTL.DepthStencilDescriptor.alloc()->init()
323 depth_desc->setDepthCompareFunction(.Less)
324 depth_desc->setDepthWriteEnabled(true)
325 depth_stencil_state = device->newDepthStencilState(depth_desc)
326 depth_desc->release()
327
328 vertex_buffer, index_buffer, instance_buffer, texture_animation_buffer := build_buffers(device)
329 defer vertex_buffer->release()
330 defer index_buffer->release()
331 defer instance_buffer->release()
332 defer texture_animation_buffer->release()
333
334 camera_buffer := device->newBuffer(size_of(Camera_Data), {.StorageModeManaged})
335 defer camera_buffer->release()
336
337 depth_texture: ^MTL.Texture = nil
338 defer if depth_texture != nil { depth_texture->release() }
339
340 compute_pso := build_compute_pipeline(device) or_return
341 defer compute_pso->release()
342
343 command_queue := device->newCommandQueue()
344 defer command_queue->release()
345
346 texture := build_texture(device)
347 defer texture->release()
348
349 SDL.ShowWindow(window)
350 for quit := false; !quit; {
351 for e: SDL.Event; SDL.PollEvent(&e); {
352 #partial switch e.type {
353 case .QUIT:
354 quit = true
355 case .KEYDOWN:
356 if e.key.keysym.sym == .ESCAPE {
357 quit = true
358 }
359 }
360 }
361
362 w, h: i32
363 SDL.GetWindowSize(window, &w, &h)
364 aspect_ratio := f32(w)/max(f32(h), 1)
365
366
367 {
368 @static angle: f32
369 angle += 0.002
370
371 object_position := glm.vec3{0, 0, -10}
372 rt := glm.mat4Translate(object_position)
373 rr1 := glm.mat4Rotate({0, 1, 0}, -angle)
374 rr0 := glm.mat4Rotate({1, 0, 0}, angle*0.5)
375 rt_inv := glm.mat4Translate(-object_position)
376 full_obj_rot := rt * rr1 * rr0 * rt_inv
377
378
379 ix, iy, iz := 0, 0, 0
380
381 instance_data := instance_buffer->contentsAsSlice([]Instance_Data)[:NUM_INSTANCES]
382 for &instance, idx in instance_data {
383 if ix == INSTANCE_WIDTH {
384 ix = 0
385 iy += 1
386 }
387 if iy == INSTANCE_HEIGHT {
388 iy = 0
389 iz += 1
390 }
391 defer ix += 1
392
393 scl :: 0.2
394
395 scale := glm.mat4Scale({scl, scl, scl})
396 zrot := glm.mat4Rotate({0, 0, 1}, angle * math.sin(f32(ix)))
397 yrot := glm.mat4Rotate({0, 1, 0}, angle * math.cos(f32(iy)))
398
399 pos := glm.vec3{
400 (f32(ix) - INSTANCE_WIDTH * 0.5) * 2*scl + scl,
401 (f32(iy) - INSTANCE_HEIGHT* 0.5) * 2*scl + scl,
402 (f32(iz) - INSTANCE_DEPTH * 0.5) * 2*scl,
403 }
404
405 translate := glm.mat4Translate(object_position + pos)
406
407 instance.transform = full_obj_rot * translate * yrot * zrot * scale
408 instance.normal_transform = glm.mat3(instance.transform)
409
410 r := f32(idx) / NUM_INSTANCES
411 instance.color = {r, 1-r, math.sin(math.TAU * r), 1}
412
413 }
414 sz := NS.UInteger(len(instance_data)*size_of(instance_data[0]))
415 instance_buffer->didModifyRange(NS.Range_Make(0, sz))
416 }
417
418 {
419 camera_data := camera_buffer->contentsAsType(Camera_Data)
420 camera_data.perspective_transform = glm.mat4Perspective(glm.radians_f32(45), aspect_ratio, 0.03, 500)
421 camera_data.world_transform = 1
422 camera_data.world_normal_transform = glm.mat3(camera_data.world_transform)
423
424 camera_buffer->didModifyRange(NS.Range_Make(0, size_of(Camera_Data)))
425 }
426
427 if depth_texture == nil ||
428 depth_texture->width() != NS.UInteger(w) ||
429 depth_texture->height() != NS.UInteger(h) {
430 desc := MTL.TextureDescriptor.texture2DDescriptorWithPixelFormat(
431 pixelFormat = .Depth16Unorm,
432 width = NS.UInteger(w),
433 height = NS.UInteger(h),
434 mipmapped = false,
435 )
436 defer desc->release()
437
438 desc->setUsage({.RenderTarget})
439 desc->setStorageMode(.Private)
440
441 if depth_texture != nil {
442 depth_texture->release()
443 }
444
445 depth_texture = device->newTextureWithDescriptor(desc)
446 }
447
448
449 drawable := swapchain->nextDrawable()
450 assert(drawable != nil)
451 defer drawable->release()
452
453 pass := MTL.RenderPassDescriptor.renderPassDescriptor()
454 defer pass->release()
455
456 color_attachment := pass->colorAttachments()->object(0)
457 assert(color_attachment != nil)
458 color_attachment->setClearColor(MTL.ClearColor{0.1, 0.1, 0.1, 1.0})
459 color_attachment->setLoadAction(.Clear)
460 color_attachment->setStoreAction(.Store)
461 color_attachment->setTexture(drawable->texture())
462
463 depth_attachment := pass->depthAttachment()
464 depth_attachment->setTexture(depth_texture)
465 depth_attachment->setClearDepth(1.0)
466 depth_attachment->setLoadAction(.Clear)
467 depth_attachment->setStoreAction(.Store)
468
469 command_buffer := command_queue->commandBuffer()
470 defer command_buffer->release()
471
472 generate_mandelbrot_texture(command_buffer, compute_pso, texture_animation_buffer, texture)
473
474 render_encoder := command_buffer->renderCommandEncoderWithDescriptor(pass)
475 defer render_encoder->release()
476
477 render_encoder->setRenderPipelineState(pso)
478 render_encoder->setDepthStencilState(depth_stencil_state)
479
480 render_encoder->setVertexBuffer(buffer=vertex_buffer, offset=0, index=0)
481 render_encoder->setVertexBuffer(buffer=instance_buffer, offset=0, index=1)
482 render_encoder->setVertexBuffer(buffer=camera_buffer, offset=0, index=2)
483
484 render_encoder->setFragmentTexture(texture, 0)
485
486 render_encoder->setCullMode(.Back)
487 render_encoder->setFrontFacingWinding(.CounterClockwise)
488 render_encoder->drawIndexedPrimitivesWithInstanceCount(.Triangle, 6*6, .UInt16, index_buffer, 0, NUM_INSTANCES)
489
490 render_encoder->endEncoding()
491
492 command_buffer->presentDrawable(drawable)
493 command_buffer->commit()
494 }
495
496 return nil
497}
498
499main :: proc() {
500 err := metal_main()
501 if err != nil {
502 fmt.eprintln(err->localizedDescription()->odinString())
503 os.exit(1)
504 }
505}