src/window.c
1#include "alba.h"
2#include "internal.h"
3
4#include <stdio.h>
5#include <stdlib.h>
6#include <string.h>
7#include <pthread.h>
8
9#include "GLFW/glfw3.h"
10#include "webgpu.h"
11
12extern char _binary_shaders_wgsl_start[];
13
14// forward-declare
15WGPUSurface get_window_surface(WGPUInstance instance, AlbaWindow* window);
16static FT_Library freetype;
17
18// global variables
19static int glfw_initialized = 0;
20pthread_mutex_t glfw_mutex = PTHREAD_MUTEX_INITIALIZER;
21
22
23void on_receive_adapter(
24 const WGPURequestAdapterStatus status,
25 const WGPUAdapter adapter,
26 char const* message,
27 void* data
28)
29{
30 if (status != WGPURequestAdapterStatus_Success)
31 {
32 fprintf(stderr, "error: requesting adapter failed: %s", message);
33 exit(1);
34 }
35
36 *((WGPUAdapter*)data) = adapter;
37}
38
39void on_device_lost_error(const WGPUDeviceLostReason reason, char const* message, void* userdata)
40{
41 fprintf(stderr, "error: device lost error (%d): %s", reason, message);
42 exit(1);
43}
44
45void on_receive_device(
46 const WGPURequestDeviceStatus status,
47 const WGPUDevice device,
48 char const* message,
49 void* data
50)
51{
52 if (status != WGPURequestDeviceStatus_Success)
53 {
54 fprintf(stderr, "error: requesting device failed: %s", message);
55 exit(1);
56 }
57
58 *((WGPUDevice*)data) = device;
59}
60
61void on_error(const WGPUErrorType type, char const* message, void* data)
62{
63 if (type == WGPUErrorType_NoError)
64 {
65 return;
66 }
67 fprintf(stderr, "error (%d): %s", type, message);
68}
69
70WGPUTexture create_texture(
71 const AlbaWindow* window,
72 const WGPUTextureUsage usage,
73 const AlbaVector size,
74 const WGPUTextureFormat format, const uint8_t samples,
75 const void* data
76)
77{
78 WGPUTextureDescriptor texture_options = {0};
79 texture_options.usage = usage;
80 texture_options.format = format;
81 texture_options.dimension = WGPUTextureDimension_2D;
82 texture_options.size.width = size.x;
83 texture_options.size.height = size.y;
84 texture_options.size.depthOrArrayLayers = 1;
85 texture_options.sampleCount = samples;
86 texture_options.mipLevelCount = 1;
87
88 const WGPUTexture texture = wgpuDeviceCreateTexture(window->device, &texture_options);
89
90 if (data != NULL)
91 {
92 const uint64_t bpp = format == WGPUTextureFormat_R8Unorm ? 1 : 4;
93
94 WGPUImageCopyTexture destination = {0};
95 destination.texture = texture;
96 destination.aspect = WGPUTextureAspect_All;
97
98 WGPUTextureDataLayout source = {0};
99 source.bytesPerRow = size.x * bpp;
100 source.rowsPerImage = size.y;
101
102 wgpuQueueWriteTexture(
103 window->queue,
104 &destination, data,
105 size.x * size.y * bpp,
106 &source,
107 &texture_options.size
108 );
109 }
110
111 return texture;
112}
113
114// TODO: reorganize code
115WGPUPipelineLayout configure_resources(const AlbaWindow* window, RenderPassData* data)
116{
117 WGPUBindGroupLayoutEntry bind_group_layout_entries[3] = {0};
118
119 // Uniform (scale)
120 // @binding(0)
121 bind_group_layout_entries[0].binding = 0;
122 bind_group_layout_entries[0].visibility = WGPUShaderStage_Vertex;
123 bind_group_layout_entries[0].buffer.type = WGPUBufferBindingType_Uniform;
124
125 // Texture
126 // @binding(1)
127 bind_group_layout_entries[1].binding = 1;
128 bind_group_layout_entries[1].visibility = WGPUShaderStage_Fragment;
129 bind_group_layout_entries[1].texture.sampleType = WGPUTextureSampleType_Float;
130 bind_group_layout_entries[1].texture.viewDimension = WGPUTextureViewDimension_2D;
131
132 // Sampler
133 // @binding(2)
134 bind_group_layout_entries[2].binding = 2;
135 bind_group_layout_entries[2].visibility = WGPUShaderStage_Fragment;
136 bind_group_layout_entries[2].sampler.type = WGPUSamplerBindingType_Filtering;
137
138 WGPUBindGroupLayoutDescriptor bind_group_layout_options = {0};
139 bind_group_layout_options.entryCount = 3;
140 bind_group_layout_options.entries = bind_group_layout_entries;
141 const WGPUBindGroupLayout bind_group_layout = wgpuDeviceCreateBindGroupLayout(
142 window->device, &bind_group_layout_options);
143
144 // --------------------------
145 WGPUBindGroupEntry bind_group_entries[3] = {0};
146
147 // Uniform (scale)
148 WGPUBufferDescriptor uniform_options = {0};
149 uniform_options.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform;
150 uniform_options.size = 2 * sizeof(float);
151 if (data->uniforms == NULL)
152 {
153 data->uniforms = wgpuDeviceCreateBuffer(window->device, &uniform_options);
154 }
155
156 // @binding(0)
157 bind_group_entries[0].binding = 0;
158 bind_group_entries[0].buffer = data->uniforms;
159 bind_group_entries[0].size = uniform_options.size;
160
161 // Texture
162 if (data->texture == NULL)
163 {
164 // 1x1 transparent texture
165 data->texture = create_texture(
166 window,
167 WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst,
168 (AlbaVector){1, 1},
169 WGPUTextureFormat_RGBA8UnormSrgb, 1,
170 &(AlbaColor)TRANSPARENT
171 );
172 }
173 // @binding(1)
174 bind_group_entries[1].binding = 1;
175 bind_group_entries[1].textureView = wgpuTextureCreateView(data->texture, NULL);
176
177 // Sampler
178 WGPUSamplerDescriptor sampler_options = {0};
179 sampler_options.addressModeU = WGPUAddressMode_ClampToEdge;
180 sampler_options.addressModeV = WGPUAddressMode_ClampToEdge;
181 sampler_options.addressModeW = WGPUAddressMode_ClampToEdge;
182 sampler_options.magFilter = WGPUFilterMode_Linear;
183 sampler_options.minFilter = WGPUFilterMode_Linear;
184 sampler_options.mipmapFilter = WGPUMipmapFilterMode_Linear;
185 sampler_options.lodMinClamp = 0;
186 sampler_options.lodMaxClamp = 1;
187 sampler_options.maxAnisotropy = 1;
188
189 // @binding(2)
190 bind_group_entries[2].binding = 2;
191 // TODO: free previous
192 bind_group_entries[2].sampler = wgpuDeviceCreateSampler(window->device, &sampler_options);
193
194 WGPUBindGroupDescriptor binding_group_options = {0};
195 binding_group_options.layout = bind_group_layout;
196 binding_group_options.entryCount = 3;
197 binding_group_options.entries = bind_group_entries;
198
199 // TODO: free previous
200 data->bind_group = wgpuDeviceCreateBindGroup(window->device, &binding_group_options);
201
202 // A pipeline can have multiple bind groups
203 WGPUPipelineLayoutDescriptor pipeline_layout_options = {0};
204 pipeline_layout_options.bindGroupLayoutCount = 1;
205 pipeline_layout_options.bindGroupLayouts = &bind_group_layout;
206 // TODO: free previous
207 const WGPUPipelineLayout pipeline_layout = wgpuDeviceCreatePipelineLayout(
208 window->device, &pipeline_layout_options);
209
210 wgpuBindGroupLayoutRelease(bind_group_layout);
211
212 return pipeline_layout;
213}
214
215void configure_surface(AlbaWindow* window)
216{
217 int width, height;
218 float x_scale, y_scale;
219 glfwGetWindowSize(window->glfw_window, &width, &height);
220 glfwGetWindowContentScale(window->glfw_window, &x_scale, &y_scale);
221
222 if (width == 0 || height == 0) return;
223
224 window->size = (AlbaVector){width, height};
225 window->scale = (AlbaVector){width / (2 * x_scale), height / (2 * y_scale)};
226
227 // Update uniforms
228 wgpuQueueWriteBuffer(window->queue, window->drawing.uniforms, 0, &window->scale, sizeof(AlbaVector));
229 wgpuQueueWriteBuffer(window->queue, window->text.uniforms, 0, &window->scale, sizeof(AlbaVector));
230
231 const WGPUTextureFormat format = wgpuSurfaceGetPreferredFormat(window->surface, window->adapter);
232
233 WGPUSurfaceConfiguration surface_options = {0};
234 surface_options.device = window->device;
235 surface_options.format = format;
236 surface_options.usage = WGPUTextureUsage_RenderAttachment;
237 surface_options.presentMode = WGPUPresentMode_Fifo;
238 surface_options.width = width;
239 surface_options.height = height;
240 wgpuSurfaceConfigure(window->surface, &surface_options);
241}
242
243
244void configure_pipeline(AlbaWindow* window)
245{
246 WGPUShaderModuleWGSLDescriptor shader_options = {0};
247 shader_options.chain.sType = WGPUSType_ShaderModuleWGSLDescriptor;
248 shader_options.code = _binary_shaders_wgsl_start;
249
250 WGPUShaderModuleDescriptor shader_loader_options = {0};
251 shader_loader_options.nextInChain = (WGPUChainedStruct*)&shader_options;
252
253 window->shaders = wgpuDeviceCreateShaderModule(window->device, &shader_loader_options);
254
255 // Configure render pipeline
256 WGPURenderPipelineDescriptor pipleine_options = {0};
257 // Rendering settings
258 pipleine_options.primitive.topology = WGPUPrimitiveTopology_TriangleList;
259 pipleine_options.primitive.frontFace = WGPUFrontFace_CCW; // counter clockwise
260 pipleine_options.multisample.count = 4;
261 pipleine_options.multisample.mask = 0xFFFFFFFF;
262
263 // Vertex shader
264 pipleine_options.vertex.module = window->shaders;
265 pipleine_options.vertex.entryPoint = "vertex_shader";
266 pipleine_options.vertex.bufferCount = 2;
267 pipleine_options.vertex.buffers = (WGPUVertexBufferLayout[]){
268 // Position
269 {
270 .arrayStride = sizeof(AlbaVector),
271 .stepMode = WGPUVertexStepMode_Vertex,
272 .attributeCount = 1,
273 .attributes = &(WGPUVertexAttribute){
274 .format = WGPUVertexFormat_Float32x2,
275 .offset = 0,
276 .shaderLocation = 0,
277 },
278 },
279 // Attributes
280 {
281 .arrayStride = sizeof(AlbaAttribute),
282 .stepMode = WGPUVertexStepMode_Vertex,
283 .attributeCount = 2,
284 .attributes = (WGPUVertexAttribute[]){
285 // Color
286 {
287 .format = WGPUVertexFormat_Float32x4,
288 .offset = 0,
289 .shaderLocation = 1
290 },
291 // UV coordinates
292 {
293 .format = WGPUVertexFormat_Float32x2,
294 .offset = offsetof(AlbaAttribute, uv),
295 .shaderLocation = 2,
296 },
297 },
298 },
299 };
300
301 // Fragment shader
302 const WGPUBlendState blend_state = {
303 .color = {
304 .operation = WGPUBlendOperation_Add,
305 .srcFactor = WGPUBlendFactor_SrcAlpha,
306 .dstFactor = WGPUBlendFactor_OneMinusSrcAlpha,
307 },
308 .alpha = {
309 .operation = WGPUBlendOperation_Add,
310 .srcFactor = WGPUBlendFactor_Zero,
311 .dstFactor = WGPUBlendFactor_One,
312 }
313 };
314 WGPUColorTargetState color_state = {0};
315 color_state.format = WGPUTextureFormat_BGRA8UnormSrgb; // TODO: avoid hardcoding
316 color_state.blend = &blend_state;
317 color_state.writeMask = WGPUColorWriteMask_All;
318 WGPUFragmentState fragment_state = {0};
319 fragment_state.module = window->shaders;
320 fragment_state.entryPoint = "fragment_shader";
321 fragment_state.targetCount = 1;
322 fragment_state.targets = &color_state;
323 pipleine_options.fragment = &fragment_state;
324
325 // Configure resources for drawing pass
326 pipleine_options.layout = configure_resources(window, &window->drawing);
327 window->drawing.pipeline = wgpuDeviceCreateRenderPipeline(window->device, &pipleine_options);
328
329 // Configure resources for text pass
330 pipleine_options.layout = configure_resources(window, &window->text);
331 window->text.pipeline = wgpuDeviceCreateRenderPipeline(window->device, &pipleine_options);
332
333 wgpuPipelineLayoutRelease(pipleine_options.layout);
334}
335
336void init_render_pass_data(RenderPassData* data)
337{
338 data->new_vertices = dynarray_new(sizeof(AlbaVector), 0);
339 data->new_attributes = dynarray_new(sizeof(AlbaAttribute), 0);
340 data->new_indices = dynarray_new(sizeof(uint32_t), 0);
341}
342
343AlbaWindow* create_window(const AlbaWindowOptions* options)
344{
345 pthread_mutex_lock(&glfw_mutex);
346 if (!glfw_initialized)
347 {
348 if (!glfwInit())
349 {
350 fprintf(stderr, "error: initializeing GLFW failed");
351 pthread_mutex_unlock(&glfw_mutex);
352 exit(1);
353 }
354 glfw_initialized = 1;
355 }
356 pthread_mutex_unlock(&glfw_mutex);
357
358 const AlbaWindowOptions default_options = {0};
359 if (options == NULL)
360 {
361 options = &default_options;
362 }
363
364 AlbaWindow* window = calloc(1, sizeof(AlbaWindow));
365 memset(window, 0, sizeof(AlbaWindow));
366 init_render_pass_data(&window->drawing);
367 init_render_pass_data(&window->text);
368 window->options = *options;
369
370
371 // GLFW window
372 glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
373 window->glfw_window = glfwCreateWindow(
374 options->initial_width == 0 ? 800 : options->initial_width,
375 options->initial_height == 0 ? 600 : options->initial_height,
376 options->title == NULL ? "Alba" : options->title,
377 NULL,
378 NULL
379 );
380
381 // Instance & surface initialization
382 window->instance = wgpuCreateInstance(NULL);
383 window->surface = get_window_surface(window->instance, window);
384
385 // Adapter
386 WGPURequestAdapterOptions adapter_options = {0};
387 adapter_options.compatibleSurface = window->surface;
388 wgpuInstanceRequestAdapter(
389 window->instance,
390 &adapter_options,
391 on_receive_adapter,
392 &window->adapter
393 );
394
395 // Device
396 WGPUDeviceDescriptor device_options = {0};
397 device_options.deviceLostCallback = on_device_lost_error;
398 wgpuAdapterRequestDevice(
399 window->adapter,
400 &device_options,
401 on_receive_device,
402 &window->device
403 );
404
405 // Errors
406 wgpuDeviceSetUncapturedErrorCallback(window->device, on_error, NULL);
407 // Queue
408 window->queue = wgpuDeviceGetQueue(window->device);
409
410 configure_pipeline(window);
411 configure_surface(window);
412
413 // This is necessary to initialize buffers
414 window->drawing.dirty = 1;
415 window->text.dirty = 1;
416
417 return window;
418}
419
420uint32_t window_should_close(const AlbaWindow* window)
421{
422 const int should_close = glfwWindowShouldClose(window->glfw_window);
423 if (!should_close)
424 {
425 glfwWaitEventsTimeout(0.01);
426 }
427 return should_close;
428}
429
430void window_release(AlbaWindow* window)
431{
432 // TODO: destroy render pass
433 wgpuBufferDestroy(window->drawing.uniforms);
434 wgpuBufferRelease(window->drawing.uniforms);
435
436 wgpuBufferDestroy(window->drawing.vertices);
437 wgpuBufferRelease(window->drawing.vertices);
438 dynarray_release(&window->drawing.new_vertices);
439
440 wgpuBufferDestroy(window->drawing.attributes);
441 wgpuBufferRelease(window->drawing.attributes);
442 dynarray_release(&window->drawing.new_attributes);
443
444 wgpuBufferDestroy(window->drawing.indices);
445 wgpuBufferRelease(window->drawing.indices);
446 dynarray_release(&window->drawing.new_indices);
447
448 wgpuBindGroupRelease(window->drawing.bind_group);
449 wgpuRenderPipelineRelease(window->drawing.pipeline);
450 wgpuBindGroupRelease(window->text.bind_group);
451 wgpuRenderPipelineRelease(window->text.pipeline);
452 wgpuShaderModuleRelease(window->shaders);
453
454 wgpuQueueRelease(window->queue);
455 wgpuDeviceRelease(window->device);
456 wgpuAdapterRelease(window->adapter);
457 wgpuSurfaceRelease(window->surface);
458 wgpuInstanceRelease(window->instance);
459
460 glfwDestroyWindow(window->glfw_window);
461 free(window);
462}
463
464uint32_t frame_status_is_valid(const AlbaWindow* window, const WGPUSurfaceTexture frame)
465{
466 switch (frame.status)
467 {
468 case WGPUSurfaceGetCurrentTextureStatus_Success:
469 break;
470 case WGPUSurfaceGetCurrentTextureStatus_Timeout:
471 case WGPUSurfaceGetCurrentTextureStatus_Outdated:
472 case WGPUSurfaceGetCurrentTextureStatus_Lost:
473 return 0;
474 case WGPUSurfaceGetCurrentTextureStatus_OutOfMemory:
475 fprintf(stderr, "error: frame allocation failed due to insufficient memory (%d)\n",
476 frame.status);
477 case WGPUSurfaceGetCurrentTextureStatus_DeviceLost:
478 fprintf(stderr, "error: device lost (%d)\n", frame.status);
479 case WGPUSurfaceGetCurrentTextureStatus_Force32:
480 fprintf(stderr, "error: force 32 error (%d)\n", frame.status);
481 exit(1);
482 }
483
484 return 1;
485}
486
487WGPUBuffer create_buffer(const AlbaWindow* window, const uint64_t size, const void* data,
488 const WGPUBufferUsageFlags flags)
489{
490 WGPUBufferDescriptor buffer_options = {0};
491 buffer_options.usage = WGPUBufferUsage_CopyDst | flags;
492 buffer_options.size = size;
493 const WGPUBuffer buffer = wgpuDeviceCreateBuffer(window->device, &buffer_options);
494 wgpuQueueWriteBuffer(window->queue, buffer, 0, data, size);
495 return buffer;
496}
497
498void clear_buffer(const WGPUBuffer buffer)
499{
500 if (buffer != NULL)
501 {
502 wgpuBufferDestroy(buffer);
503 wgpuBufferRelease(buffer);
504 }
505}
506
507WGPUBuffer update_buffer(const AlbaWindow* window, WGPUBuffer buffer, WGPUBufferUsage usage, DynArray data)
508{
509 // Deallocate old buffer if any
510 clear_buffer(buffer);
511 // Copy data to new buffer
512 buffer = create_buffer(window, data.length * data.element_size, data.data, usage);
513 // Clear local copy for next frame
514 dynarray_clear(&data);
515 return buffer;
516}
517
518void copy_color(const AlbaColor src, WGPUColor* dst)
519{
520 dst->r = src.r;
521 dst->g = src.g;
522 dst->b = src.b;
523 dst->a = src.a;
524}
525
526void render_pass(
527 AlbaWindow* window,
528 const WGPUTextureView frame,
529 const WGPUTextureView render_target,
530 const int clear,
531 RenderPassData* data
532)
533{
534 if (data->dirty)
535 {
536 // TODO: optimize (and only recreate the necessary pipeline)
537 configure_pipeline(window);
538 data->vertices = update_buffer(window, data->vertices, WGPUBufferUsage_Vertex, data->new_vertices);
539 data->attributes = update_buffer(window, data->attributes, WGPUBufferUsage_Vertex, data->new_attributes);
540 data->indices = update_buffer(window, data->indices, WGPUBufferUsage_Index, data->new_indices);
541 data->dirty = 0;
542 }
543
544 const WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(window->device, NULL);
545
546 // Configures rendering
547 WGPURenderPassColorAttachment render_pass_attachment_options = {0};
548 render_pass_attachment_options.view = render_target;
549 render_pass_attachment_options.loadOp = clear ? WGPULoadOp_Clear : WGPULoadOp_Load;
550 render_pass_attachment_options.storeOp = WGPUStoreOp_Store;
551 render_pass_attachment_options.resolveTarget = frame;
552 copy_color(window->options.clear_color, &render_pass_attachment_options.clearValue);
553
554 WGPURenderPassDescriptor render_pass_options = {0};
555 render_pass_options.colorAttachmentCount = 1;
556 render_pass_options.colorAttachments = &render_pass_attachment_options;
557
558 const WGPURenderPassEncoder render_pass = wgpuCommandEncoderBeginRenderPass(
559 encoder, &render_pass_options);
560
561 wgpuRenderPassEncoderSetPipeline(render_pass, data->pipeline);
562 wgpuRenderPassEncoderSetBindGroup(render_pass, 0, data->bind_group, 0, NULL);
563
564 const uint32_t vertex_size = wgpuBufferGetSize(data->vertices);
565 const uint32_t attributes_size = wgpuBufferGetSize(data->attributes);
566 const uint32_t indices_size = wgpuBufferGetSize(data->indices);
567 if (vertex_size > 0)
568 {
569 wgpuRenderPassEncoderSetVertexBuffer(render_pass, 0, data->vertices, 0, vertex_size);
570 wgpuRenderPassEncoderSetVertexBuffer(render_pass, 1, data->attributes, 0, attributes_size);
571 wgpuRenderPassEncoderSetIndexBuffer(render_pass, data->indices, WGPUIndexFormat_Uint32, 0,
572 indices_size);
573 wgpuRenderPassEncoderDrawIndexed(render_pass, indices_size / sizeof(uint32_t), 1, 0, 0, 0);
574 }
575
576 wgpuRenderPassEncoderEnd(render_pass);
577
578 // Encode render command and send to GPU
579 const WGPUCommandBuffer command = wgpuCommandEncoderFinish(encoder, NULL);
580 wgpuQueueSubmit(window->queue, 1, &command);
581
582 wgpuCommandBufferRelease(command);
583 wgpuRenderPassEncoderRelease(render_pass);
584 wgpuCommandEncoderRelease(encoder);
585}
586
587void window_render(AlbaWindow* window)
588{
589 WGPUSurfaceTexture frame;
590 wgpuSurfaceGetCurrentTexture(window->surface, &frame);
591 if (!frame_status_is_valid(window, frame))
592 {
593 // Re-configure surface and skip frame
594 if (frame.texture != NULL)
595 {
596 wgpuTextureRelease(frame.texture);
597 }
598
599 configure_surface(window);
600 return;
601 }
602
603 const WGPUTextureView frame_view = wgpuTextureCreateView(frame.texture, NULL);
604 if (frame_view == NULL)
605 {
606 printf("warning: could not get frame view, dropping frame (%d)\n", frame.status);
607 return;
608 }
609
610 // Render of off-screen texture with anti-aliasing (MSAA 4x)
611 const WGPUTexture render_target = create_texture(
612 window,
613 WGPUTextureUsage_RenderAttachment,
614 window->size,
615 WGPUTextureFormat_BGRA8UnormSrgb, 4, // TODO: do not assume format
616 NULL
617 );
618 const WGPUTextureView render_target_view = wgpuTextureCreateView(render_target, NULL);
619 if (render_target_view == NULL)
620 {
621 printf("warning: could not get texture view, dropping frame\n");
622 return;
623 }
624
625 // Draw calls
626 render_pass(window, frame_view, render_target_view, 1, &window->drawing);
627 render_pass(window, frame_view, render_target_view, 0, &window->text);
628
629 // Update screen
630 wgpuSurfacePresent(window->surface);
631
632 wgpuTextureViewRelease(render_target_view);
633 wgpuTextureViewRelease(frame_view);
634 wgpuTextureRelease(render_target);
635 wgpuTextureRelease(frame.texture);
636}
637
638
639void window_get_size(const AlbaWindow* window, float* width, float* height)
640{
641 int raw_width, raw_height;
642 float x_scale, y_scale;
643 glfwGetWindowSize(window->glfw_window, &raw_width, &raw_height);
644 glfwGetWindowContentScale(window->glfw_window, &x_scale, &y_scale);
645
646 *width = raw_width / x_scale;
647 *height = raw_height / y_scale;
648}
649
650void alba_release()
651{
652 if (freetype != NULL)
653 {
654 FT_Done_FreeType(freetype); // ignore error
655 // TODO: free faces
656 }
657 glfwTerminate();
658}