aboutsummaryrefslogtreecommitdiffstats
path: root/code/metal/4ed_metal_render.mm
blob: 8bdccd3d173f6293aae0b55e15f3b188ed75aeff (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
/* 4coder Metal render implementation */

#undef clamp
#undef function
#import <simd/simd.h>
#import <MetalKit/MetalKit.h>
#define function static

////////////////////////////////

struct Metal_Buffer{
    Node node;
    
    id<MTLBuffer> buffer;
    u32 size;
    u64 last_reuse_time;
};

////////////////////////////////

typedef id<MTLTexture> Metal_Texture;

// NOTE(yuval): This is a locator used to describe where a specific slot is located.
union Metal_Texture_Slot_Locator{
    u32 packed;
    
    struct{
        u16 bucket_index;
        u16 slot_index;
    };
};

// NOTE(yuval): This is the ACTUAL texture slot. Each slot contains the texture handle, the slot locator, and a pointer to the next slot in the free list (in case the slot if not occupied).
struct Metal_Texture_Slot{
    // NOTE(yuval): This is a pointer to the next texture in the free texture slots list
    Metal_Texture_Slot *next;
    
    Metal_Texture texture;
    Metal_Texture_Slot_Locator locator;
};

global_const u32 metal__texture_slots_per_bucket = 256;

// NOTE(yuval): This a bucket of ACTUAL texture slots.
struct Metal_Texture_Slot_Bucket{
    Metal_Texture_Slot_Bucket *next;
    Metal_Texture_Slot slots[metal__texture_slots_per_bucket];
};

// NOTE(yuval): This a struct contaning all texture slot buckets and a list of the currently free slots.
struct Metal_Texture_Slot_List{
    Metal_Texture_Slot_Bucket *first_bucket;
    Metal_Texture_Slot_Bucket *last_bucket;
    u16 bucket_count;
    
    Metal_Texture_Slot *first_free_slot;
    Metal_Texture_Slot *last_free_slot;
};

global_const u32 metal__invalid_texture_slot_locator = 0;

////////////////////////////////

@interface Metal_Renderer : NSObject<MTKViewDelegate>
- (nonnull instancetype)initWithMetalKitView:(nonnull MTKView*)mtkView target:(Render_Target*)target;

- (u32)get_texture_of_dim:(Vec3_i32)dim kind:(Texture_Kind)kind;
- (b32)fill_texture:(u32)texture kind:(Texture_Kind)kind pos:(Vec3_i32)p dim:(Vec3_i32)dim data:(void*)data;
- (void)bind_texture:(u32)handle encoder:(id<MTLRenderCommandEncoder>)render_encoder;
- (void)free_texture:(u32)handle;
- (Metal_Texture_Slot*)get_texture_slot_at_locator:(Metal_Texture_Slot_Locator)locator;
- (Metal_Texture_Slot*)get_texture_slot_at_handle:(u32)handle;

- (Metal_Buffer*)get_reusable_buffer_with_size:(NSUInteger)size;
- (void)add_reusable_buffer:(Metal_Buffer*)buffer;
@end

////////////////////////////////

global_const char *metal__shaders_source = R"(
#include <metal_stdlib>
#include <simd/simd.h>

using namespace metal;

////////////////////////////////

typedef struct{
float2 xy [[attribute(0)]];
float3 uvw [[attribute(1)]];
uint32_t color [[attribute(2)]];
float half_thickness [[attribute(3)]];
} Vertex;

// NOTE(yuval): Vertex shader outputs and fragment shader inputs
typedef struct{
// NOTE(yuval): Vertex shader output
float4 position [[position]];

// NOTE(yuval): Fragment shader inputs
float4 color;
float3 uvw;
float2 xy;
float2 adjusted_half_dim;
float half_thickness;
} Rasterizer_Data;

////////////////////////////////

vertex Rasterizer_Data
vertex_shader(Vertex in [[stage_in]],
constant float4x4 &proj [[buffer(1)]]){
Rasterizer_Data out;

// NOTE(yuval): Calculate position in NDC
out.position = proj * float4(in.xy, 0.0, 1.0);

// NOTE(yuval): Convert color to float4 format
out.color.b = ((float((in.color       ) & 0xFFu)) / 255.0);
out.color.g = ((float((in.color >>  8u) & 0xFFu)) / 255.0);
out.color.r = ((float((in.color >> 16u) & 0xFFu)) / 255.0);
out.color.a = ((float((in.color >> 24u) & 0xFFu)) / 255.0);

// NOTE(yuval): Pass uvw coordinates to the fragment shader
out.uvw = in.uvw;

// NOTE(yuval): Calculate adjusted half dim
float2 center = in.uvw.xy;
float2 half_dim = abs(in.xy - center);
out.adjusted_half_dim = (half_dim - in.uvw.zz + float2(0.5, 0.5));

// NOTE(yuval): Pass half_thickness to the fragment shader
out.half_thickness = in.half_thickness;

// NOTE(yuval): Pass xy to the fragment shader
out.xy = in.xy;

return(out);
}

////////////////////////////////

float
rectangle_sd(float2 p, float2 b){
float2 d = (abs(p) - b);
float result = (length(max(d, float2(0.0, 0.0))) + min(max(d.x, d.y), 0.0));

return(result);
}

fragment float4
fragment_shader(Rasterizer_Data in [[stage_in]],
texture2d_array<half> in_texture [[texture(0)]]){
float has_thickness = step(0.49, in.half_thickness);
float does_not_have_thickness = (1.0 - has_thickness);

constexpr sampler texture_sampler(coord::normalized, min_filter::linear, mag_filter::linear, mip_filter::linear);
half sample_value = in_texture.sample(texture_sampler, in.uvw.xy, in.uvw.z).r;
sample_value *= does_not_have_thickness;

float2 center = in.uvw.xy;
float roundness = in.uvw.z;
float sd = rectangle_sd(in.xy - center, in.adjusted_half_dim);
sd = sd - roundness;
sd = (abs(sd + in.half_thickness) - in.half_thickness);
float shape_value = (1.0 - smoothstep(-1.0, 0.0, sd));
shape_value *= has_thickness;

float4 out_color = float4(in.color.xyz, in.color.a * (sample_value + shape_value));
return(out_color);
}
)";

////////////////////////////////

function Metal_Buffer*
metal__make_buffer(u32 size, id<MTLDevice> device){
    Metal_Buffer *result = (Metal_Buffer*)malloc(sizeof(Metal_Buffer));
    
    // NOTE(yuval): Create the vertex buffer
    MTLResourceOptions options = MTLCPUCacheModeWriteCombined|MTLResourceStorageModeManaged;
    result->buffer = [device newBufferWithLength:size options:options];
    result->size = size;
    
    // NOTE(yuval): Set the last_reuse_time to the current time
    result->last_reuse_time = system_now_time();
    
    return result;
}

////////////////////////////////

@implementation Metal_Renderer{
    Render_Target *_target;
    
    id<MTLDevice> _device;
    id<MTLRenderPipelineState> _pipeline_state;
    id<MTLCommandQueue> _command_queue;
    id<MTLCaptureScope> _capture_scope;
    
    Node _buffer_cache;
    u64 _last_buffer_cache_purge_time;
    
    Metal_Texture_Slot_List _texture_slots;
}

- (nonnull instancetype)initWithMetalKitView:(nonnull MTKView*)mtk_view target:(Render_Target*)target{
    self = [super init];
    if (self == nil){
        return(nil);
    }
    
    _target = target;
    
    NSError *error = nil;
    
    _device = mtk_view.device;
    
    // NOTE(yuval): Compile the shaders
    id<MTLFunction> vertex_function = nil;
    id<MTLFunction> fragment_function = nil;
    {
        NSString *shaders_source_str = [NSString stringWithUTF8String:metal__shaders_source];
        
        MTLCompileOptions *options = [[MTLCompileOptions alloc] init];
        options.fastMathEnabled = YES;
        
        id<MTLLibrary> shader_library = [_device newLibraryWithSource:shaders_source_str
                options:options error:&error];
        vertex_function = [shader_library newFunctionWithName:@"vertex_shader"];
        fragment_function = [shader_library newFunctionWithName:@"fragment_shader"];
        
        [options release];
    }
    
    Assert(error == nil);
    Assert((vertex_function != nil) && (fragment_function != nil));
    
    // NOTE(yuval): Configure the pipeline descriptor
    {
        MTLVertexDescriptor *vertexDescriptor = [MTLVertexDescriptor vertexDescriptor];
        vertexDescriptor.attributes[0].offset = OffsetOfMember(Render_Vertex, xy);
        vertexDescriptor.attributes[0].format = MTLVertexFormatFloat2; // position
        vertexDescriptor.attributes[0].bufferIndex = 0;
        vertexDescriptor.attributes[1].offset = OffsetOfMember(Render_Vertex, uvw);
        vertexDescriptor.attributes[1].format = MTLVertexFormatFloat3; // texCoords
        vertexDescriptor.attributes[1].bufferIndex = 0;
        vertexDescriptor.attributes[2].offset = OffsetOfMember(Render_Vertex, color);
        vertexDescriptor.attributes[2].format = MTLVertexFormatUInt; // color
        vertexDescriptor.attributes[2].bufferIndex = 0;
        vertexDescriptor.attributes[3].offset = OffsetOfMember(Render_Vertex, half_thickness);
        vertexDescriptor.attributes[3].format = MTLVertexFormatFloat; // position
        vertexDescriptor.attributes[3].bufferIndex = 0;
        vertexDescriptor.layouts[0].stepRate = 1;
        vertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex;
        vertexDescriptor.layouts[0].stride = sizeof(Render_Vertex);
        
        MTLRenderPipelineDescriptor *pipeline_state_descriptor = [[MTLRenderPipelineDescriptor alloc] init];
        pipeline_state_descriptor.label = @"4coder Metal Renderer Pipeline";
        pipeline_state_descriptor.vertexFunction = vertex_function;
        pipeline_state_descriptor.fragmentFunction = fragment_function;
        pipeline_state_descriptor.vertexDescriptor = vertexDescriptor;
        pipeline_state_descriptor.colorAttachments[0].pixelFormat = mtk_view.colorPixelFormat;
        pipeline_state_descriptor.colorAttachments[0].blendingEnabled = YES;
        pipeline_state_descriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd;
        pipeline_state_descriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd;
        pipeline_state_descriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha;
        pipeline_state_descriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
        pipeline_state_descriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne;
        pipeline_state_descriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
        
        _pipeline_state = [_device newRenderPipelineStateWithDescriptor:pipeline_state_descriptor
                error:&error];
    }
    
    Assert(error == nil);
    
    // NOTE(yuval): Create the command queue
    _command_queue = [_device newCommandQueue];
    
    // NOTE(yuval): Initialize buffer caching
    dll_init_sentinel(&_buffer_cache);
    _last_buffer_cache_purge_time = system_now_time();
    
    // NOTE(yuval): Initialize the texture slot list
    block_zero_struct(&_texture_slots);
    
    // NOTE(simon): Other platforms use 0 as invalid handle, so we allocate the first texture here (it should be 0),
    // and never use it so we can use 0 as the invalid handle.
    u32 reserved_texture_slot_do_not_use = [self get_texture_of_dim:V3i32(2, 2, 1) kind:TextureKind_Mono];
    Assert( reserved_texture_slot_do_not_use == 0 );
    
    // NOTE(yuval): Create the fallback texture
    _target->fallback_texture_id = [self get_texture_of_dim:V3i32(2, 2, 1)
            kind:TextureKind_Mono];
    u8 white_block[] = {0xFF, 0xFF, 0xFF, 0xFF};
    [self fill_texture:_target->fallback_texture_id
            kind:TextureKind_Mono
            pos:V3i32(0, 0, 0)
            dim:V3i32(2, 2, 1)
            data:white_block];
    
    // NOTE(yuval): Create a capture scope for gpu frame capture
    _capture_scope = [[MTLCaptureManager sharedCaptureManager]
            newCaptureScopeWithDevice:_device];
    _capture_scope.label = @"4coder Metal Capture Scope";
    
    return(self);
}

- (void)mtkView:(nonnull MTKView*)view drawableSizeWillChange:(CGSize)size{
    // NOTE(yuval): Nothing to do here because we use the render target's dimentions for rendering
}

- (void)drawInMTKView:(nonnull MTKView*)view{
#if FRED_INTERNAL
    [_capture_scope beginScope];
#endif
    
    // HACK(yuval): This is the best way I found to force valid width and height without drawing on the next draw cycle (1 frame delay).
    CGSize drawable_size = [view drawableSize];
    i32 width = (i32)Min(_target->width, drawable_size.width);
    i32 height = (i32)Min(_target->height, drawable_size.height);
    
    Font_Set *font_set = (Font_Set*)_target->font_set;
    
    // NOTE(yuval): Free any textures in the target's texture free list
    for (Render_Free_Texture *free_texture = _target->free_texture_first;
         free_texture;
         free_texture = free_texture->next){
        Metal_Texture_Slot *texture_slot = [self get_texture_slot_at_handle:free_texture->tex_id];
        if (texture_slot){
            sll_queue_push(_texture_slots.first_free_slot, _texture_slots.last_free_slot, texture_slot);
        }
    }
    _target->free_texture_first = 0;
    _target->free_texture_last = 0;
    
    // NOTE(yuval): Create the command buffer
    id<MTLCommandBuffer> command_buffer = [_command_queue commandBuffer];
    command_buffer.label = @"4coder Metal Render Command";
    
    // NOTE(yuval): Obtain the render pass descriptor from the renderer's view
    MTLRenderPassDescriptor *render_pass_descriptor = view.currentRenderPassDescriptor;
    if (render_pass_descriptor != nil){
        render_pass_descriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0f, 0.0f, 0.0f, 1.0f);
        
        // NOTE(yuval): Create the render command encoder
        id<MTLRenderCommandEncoder> render_encoder =
            [command_buffer renderCommandEncoderWithDescriptor:render_pass_descriptor];
        render_encoder.label = @"4coder Render Encoder";
        
        // NOTE(yuval): Set the region of the drawable to draw into
        [render_encoder setViewport:(MTLViewport){0.0, 0.0, (double)width, (double)height, 0.0, 1.0}];
        
        // NOTE(yuval): Set the render pipeline to use for drawing
        [render_encoder setRenderPipelineState:_pipeline_state];
        
        // NOTE(yuval): Calculate the projection matrix
        float left = 0, right = (float)width;
        float bottom = (float)height, top = 0;
        float near_depth = -1.0f, far_depth = 1.0f;
        float proj[16] = {
            2.0f / (right - left), 0.0f, 0.0f, 0.0f,
            0.0f, 2.0f / (top - bottom), 0.0f, 0.0f,
            0.0f, 0.0f, -1.0f / (far_depth - near_depth), 0.0f,
            -((right + left) / (right - left)), -((top + bottom) / (top - bottom)),
            -(near_depth / (far_depth - near_depth)), 1.0f
        };
        
        // NOTE(yuval): Calculate required vertex buffer size
        i32 all_vertex_count = 0;
        for (Render_Group *group = _target->group_first;
             group;
             group = group->next){
            all_vertex_count += group->vertex_list.vertex_count;
        }
        
        u32 vertex_buffer_size = (all_vertex_count * sizeof(Render_Vertex));
        
        // NOTE(yuval): Find & Get a vertex buffer matching the required size
        Metal_Buffer *buffer = [self get_reusable_buffer_with_size:vertex_buffer_size];
        
        // NOTE(yuval): Pass the vertex buffer to the vertex shader
        [render_encoder setVertexBuffer:buffer->buffer
                offset:0
                atIndex:0];
        
        // NOTE(yuval): Pass the projection matrix to the vertex shader
        [render_encoder setVertexBytes:&proj
                length:sizeof(proj)
                atIndex:1];
        
        u32 buffer_offset = 0;
        for (Render_Group *group = _target->group_first;
             group;
             group = group->next){
            // NOTE(yuval): Set scissor rect
            {
                Rect_i32 box = Ri32(group->clip_box);
                
                NSUInteger x0 = (NSUInteger)Min(Max(0, box.x0), width - 1);
                NSUInteger x1 = (NSUInteger)Min(Max(0, box.x1), width);
                NSUInteger y0 = (NSUInteger)Min(Max(0, box.y0), height - 1);
                NSUInteger y1 = (NSUInteger)Min(Max(0, box.y1), height);
                
                MTLScissorRect scissor_rect;
                scissor_rect.x = x0;
                scissor_rect.y = y0;
                scissor_rect.width = (x1 - x0);
                scissor_rect.height = (y1 - y0);
                
                [render_encoder setScissorRect:scissor_rect];
            }
            
            i32 vertex_count = group->vertex_list.vertex_count;
            if (vertex_count > 0){
                // NOTE(yuval): Bind a texture
                {
                    Face* face = font_set_face_from_id(font_set, group->face_id);
                    if (face != 0){
                        // NOTE(yuval): Bind face texture
                        [self bind_texture:face->texture
                                encoder:render_encoder];
                    } else{
                        // NOTE(yuval): Bind fallback texture
                        [self bind_texture:_target->fallback_texture_id
                                encoder:render_encoder];
                    }
                }
                
                // NOTE(yuval): Copy the vertex data to the vertex buffer
                {
                    
                    u8 *group_buffer_contents = (u8*)[buffer->buffer contents] + buffer_offset;
                    u8 *cursor = group_buffer_contents;
                    for (Render_Vertex_Array_Node *node = group->vertex_list.first;
                         node;
                         node = node->next){
                        i32 size = node->vertex_count * sizeof(*node->vertices);
                        memcpy(cursor, node->vertices, size);
                        cursor += size;
                    }
                    
                    NSUInteger data_size = (NSUInteger)(cursor - group_buffer_contents);
                    NSRange modify_range = NSMakeRange(buffer_offset, data_size);
                    [buffer->buffer didModifyRange:modify_range];
                }
                
                // NOTE(yuval): Set the vertex buffer offset to the beginning of the group's vertices
                [render_encoder setVertexBufferOffset:buffer_offset atIndex:0];
                
                // NOTE(yuval): Draw the vertices
                [render_encoder drawPrimitives:MTLPrimitiveTypeTriangle
                        vertexStart:0
                        vertexCount:vertex_count];
                
                buffer_offset += (vertex_count * sizeof(Render_Vertex));
            }
        }
        
        [render_encoder endEncoding];
        
        // NOTE(yuval): Schedule a present once the framebuffer is complete using the current drawable
        [command_buffer presentDrawable:view.currentDrawable];
        
        [command_buffer addCompletedHandler:^(id<MTLCommandBuffer>){
                dispatch_async(dispatch_get_main_queue(), ^{
                               [self add_reusable_buffer:buffer];
                               });
            }];
    }
    
    // NOTE(yuval): Finalize rendering here and push the command buffer to the GPU
    [command_buffer commit];
    
#if FRED_INTERNAL
    [_capture_scope endScope];
#endif
}

- (u32)get_texture_of_dim:(Vec3_i32)dim kind:(Texture_Kind)kind{
    u32 handle = metal__invalid_texture_slot_locator;
    
    // NOTE(yuval): Check for a free texture slot and allocate another slot bucket if no free slot has been found
    if (!_texture_slots.first_free_slot){
        // NOTE(yuval): Assert that the next bucket's index can fit in a u16
        Assert(_texture_slots.bucket_count < ((u16)-1));
        
        Metal_Texture_Slot_Bucket *bucket = (Metal_Texture_Slot_Bucket*)system_memory_allocate(sizeof(Metal_Texture_Slot_Bucket),  file_name_line_number_lit_u8);
        
        for (u16 slot_index = 0;
             slot_index < ArrayCount(bucket->slots);
             ++slot_index){
            Metal_Texture_Slot *slot = &bucket->slots[slot_index];
            block_zero_struct(slot);
            slot->locator.bucket_index = _texture_slots.bucket_count;
            slot->locator.slot_index = slot_index;
            
            sll_queue_push(_texture_slots.first_free_slot, _texture_slots.last_free_slot, slot);
        }
        
        sll_queue_push(_texture_slots.first_bucket, _texture_slots.last_bucket, bucket);
        _texture_slots.bucket_count += 1;
    }
    
    // NOTE(yuval): Get the first free texture slot and remove it from the free list (a slot is guarenteed to exist because we assert that above).
    if (_texture_slots.first_free_slot){
        Metal_Texture_Slot *texture_slot = _texture_slots.first_free_slot;
        sll_queue_pop(_texture_slots.first_free_slot, _texture_slots.last_free_slot);
        texture_slot->next = 0;
        
        // NOTE(yuval): Create a texture descriptor.
        MTLTextureDescriptor *texture_descriptor = [[MTLTextureDescriptor alloc] init];
        texture_descriptor.textureType = MTLTextureType2DArray;
        texture_descriptor.pixelFormat = MTLPixelFormatR8Unorm;
        texture_descriptor.width = dim.x;
        texture_descriptor.height = dim.y;
        texture_descriptor.arrayLength = dim.z;

        // NOTE(yuval): Create the texture from the device using the descriptor and add it to the textures array.
        Metal_Texture texture = [_device newTextureWithDescriptor:texture_descriptor];
        texture_slot->texture = texture;
        
        handle = texture_slot->locator.packed;
    }
    
    return handle;
}

- (b32)fill_texture:(u32)handle kind:(Texture_Kind)kind pos:(Vec3_i32)p dim:(Vec3_i32)dim data:(void*)data{
    b32 result = false;
    
    if (data){
        Metal_Texture_Slot *texture_slot = [self get_texture_slot_at_handle:handle];
        if (texture_slot){
            Metal_Texture texture = texture_slot->texture;
            
            if (texture != 0){
                // https://developer.apple.com/documentation/metal/mtlregion
                // for 2d texture origin.z is 0, and depth is 1
                MTLRegion replace_region = {
                    {(NSUInteger)p.x, (NSUInteger)p.y, 0},
                    {(NSUInteger)dim.x, (NSUInteger)dim.y, 1}
                };
                
                // NOTE(yuval): Fill the texture with data
                [texture replaceRegion:replace_region
                        mipmapLevel:0
                        slice:p.z
                        withBytes:data
                        bytesPerRow:dim.x
                        bytesPerImage:0];

                result = true;
            }
        }
    }
    
    return(result);
}

- (void)bind_texture:(u32)handle encoder:(id<MTLRenderCommandEncoder>)render_encoder{
    Metal_Texture_Slot *texture_slot = [self get_texture_slot_at_handle:handle];
    if (texture_slot){
        Metal_Texture texture = texture_slot->texture;
        if (texture != 0){
            [render_encoder setFragmentTexture:texture
                    atIndex:0];
        }
    }
}

- (void)free_texture:(u32)handle{
    Metal_Texture_Slot *texture_slot = [self get_texture_slot_at_handle:handle];
    if (texture_slot){
        sll_queue_push(_texture_slots.first_free_slot, _texture_slots.last_free_slot, texture_slot);
    }
}


- (Metal_Texture_Slot*)get_texture_slot_at_locator:(Metal_Texture_Slot_Locator)locator{
    Metal_Texture_Slot *result = 0;
    
    if (locator.packed != metal__invalid_texture_slot_locator){
        Metal_Texture_Slot_Bucket *bucket = _texture_slots.first_bucket;
        for (u16 bucket_index = 0;
             (bucket_index < locator.bucket_index) && bucket;
             ++bucket_index, bucket = bucket->next);
        
        if (bucket && (locator.slot_index < metal__texture_slots_per_bucket)){
            result = &bucket->slots[locator.slot_index];
        }
    }
    
    return(result);
}

- (Metal_Texture_Slot*)get_texture_slot_at_handle:(u32)handle{
    Metal_Texture_Slot_Locator locator;
    locator.packed = handle;
    
    Metal_Texture_Slot *result = [self get_texture_slot_at_locator:locator];
    return(result);
}

- (Metal_Buffer*)get_reusable_buffer_with_size:(NSUInteger)size{
    // NOTE(yuval): This routine is a modified version of Dear ImGui's MetalContext::dequeueReusableBufferOfLength in imgui_impl_metal.mm
    
    u64 now = system_now_time();
    
    // NOTE(yuval): Purge old buffers that haven't been useful for a while
    if ((now - _last_buffer_cache_purge_time) > 1000000){
        Node prev_buffer_cache = _buffer_cache;
        dll_init_sentinel(&_buffer_cache);
        
        for (Node *node = prev_buffer_cache.next;
             node != &_buffer_cache;
             node = node->next){
            Metal_Buffer *candidate = CastFromMember(Metal_Buffer, node, node);
            if (candidate->last_reuse_time > _last_buffer_cache_purge_time){
                dll_insert(&_buffer_cache, node);
            }
        }
        
        _last_buffer_cache_purge_time = now;
    }
    
    // NOTE(yuval): See if we have a buffer we can reuse
    Metal_Buffer *best_candidate = 0;
    for (Node *node = _buffer_cache.next;
         node != &_buffer_cache;
         node = node->next){
        Metal_Buffer *candidate = CastFromMember(Metal_Buffer, node, node);
        if ((candidate->size >= size) && ((!best_candidate) || (best_candidate->last_reuse_time > candidate->last_reuse_time))){
            best_candidate = candidate;
        }
    }
    
    Metal_Buffer *result;
    if (best_candidate){
        // NOTE(yuval): A best candidate has been found! Remove it from the buffer list and set its last reuse time.
        dll_remove(&best_candidate->node);
        best_candidate->last_reuse_time = now;
        result = best_candidate;
    } else{
        // NOTE(yuval): No luck; make a new buffer.
        result = metal__make_buffer(size, _device);
    }
    
    return(result);
}

- (void)add_reusable_buffer:(Metal_Buffer*)buffer{
    // NOTE(yuval): This routine is a modified version of Dear ImGui's MetalContext::enqueueReusableBuffer in imgui_impl_metal.mm
    
    dll_insert(&_buffer_cache, &buffer->node);
}
@end