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* alba_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 alba_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 alba_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    alba_dynarray_release(&window->drawing.new_vertices);
439
440    wgpuBufferDestroy(window->drawing.attributes);
441    wgpuBufferRelease(window->drawing.attributes);
442    alba_dynarray_release(&window->drawing.new_attributes);
443
444    wgpuBufferDestroy(window->drawing.indices);
445    wgpuBufferRelease(window->drawing.indices);
446    alba_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, AlbaDynArray 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    alba_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 alba_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 alba_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        // freetype errors are ignored here
655        FT_Done_FreeType(freetype);
656        // TODO: free faces
657        FT_Done_Face(roboto);
658        atlas_release(&atlas);
659    }
660    glfwTerminate();
661}