metal/learn_metal/08-compute macOS

Code

08-compute.odin ¶
481 linesSource

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		//                                         Texture
137		//   Positions           Normals         Coordinates
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, // front
170		 4,  5,  6,  6,  7,  4, // right
171		 8,  9, 10, 10, 11,  8, // back
172		12, 13, 14, 14, 15, 12, // left
173		16, 17, 18, 18, 19, 16, // top
174		20, 21, 22, 22, 23, 20, // bottom
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	// Build Depth Stencil State
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}

Declarations Used 54