metal/learn_metal/09-compute-to-render macOS

Code

09-compute-to-render.odin ¶
505 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, texture_animation_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	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	// Build Depth Stencil State
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}

Declarations Used 54