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
 14int glfw_initialized = 0;
 15pthread_mutex_t glfw_mutex = PTHREAD_MUTEX_INITIALIZER;
 16
 17void on_receive_adapter(
 18    const WGPURequestAdapterStatus status,
 19    const WGPUAdapter adapter,
 20    char const* message,
 21    void* data
 22)
 23{
 24    if (status != WGPURequestAdapterStatus_Success)
 25    {
 26        fprintf(stderr, "error: requesting adapter failed: %s", message);
 27        exit(1);
 28    }
 29
 30    *((WGPUAdapter*)data) = adapter;
 31}
 32
 33void on_device_lost_error(const WGPUDeviceLostReason reason, char const* message, void* userdata)
 34{
 35    fprintf(stderr, "error: device lost error (%d): %s", reason, message);
 36    exit(1);
 37}
 38
 39void on_receive_device(
 40    const WGPURequestDeviceStatus status,
 41    const WGPUDevice device,
 42    char const* message,
 43    void* data
 44)
 45{
 46    if (status != WGPURequestDeviceStatus_Success)
 47    {
 48        fprintf(stderr, "error: requesting device failed: %s", message);
 49        exit(1);
 50    }
 51
 52    *((WGPUDevice*)data) = device;
 53}
 54
 55void on_error(const WGPUErrorType type, char const* message, void* data)
 56{
 57    if (type == WGPUErrorType_NoError)
 58    {
 59        return;
 60    }
 61    fprintf(stderr, "error (%d): %s", type, message);
 62}
 63
 64
 65void create_uniform_bind_group(AlbaWindow* window, const WGPUBindGroupLayout bind_group_layout)
 66{
 67    const float uniforms_default_value[2] = {1, 1};
 68    window->uniforms = create_buffer(
 69        window,
 70        sizeof(uniforms_default_value), uniforms_default_value,
 71        WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform
 72    );
 73
 74    WGPUBindGroupEntry bind_group_entries[1] = {0};
 75    bind_group_entries[0].binding = 0; // @group(0) @binding(0)
 76    bind_group_entries[0].buffer = window->uniforms;
 77    bind_group_entries[0].size = sizeof(uniforms_default_value);
 78
 79    WGPUBindGroupDescriptor binding_group_options = {0};
 80    binding_group_options.layout = bind_group_layout;
 81    binding_group_options.entryCount = 1;
 82    binding_group_options.entries = bind_group_entries;
 83
 84    window->uniforms_bind_group = wgpuDeviceCreateBindGroup(window->device, &binding_group_options);
 85}
 86
 87WGPUPipelineLayout configure_bind_group_layout(AlbaWindow* window)
 88{
 89    WGPUBindGroupLayout bind_groups[2];
 90
 91    // @group(0) - Uniforms (scale)
 92    // --------------------------------------------
 93    WGPUBindGroupLayoutEntry uniforms_bind_group_entries[1] = {0};
 94
 95    // @binding(0)
 96    uniforms_bind_group_entries[0].binding = 0;
 97    uniforms_bind_group_entries[0].visibility = WGPUShaderStage_Vertex;
 98    uniforms_bind_group_entries[0].buffer.type = WGPUBufferBindingType_Uniform;
 99
100    WGPUBindGroupLayoutDescriptor uniforms_bind_group_options = {0};
101    uniforms_bind_group_options.entryCount = 1;
102    uniforms_bind_group_options.entries = uniforms_bind_group_entries;
103    bind_groups[0] = wgpuDeviceCreateBindGroupLayout(window->device, &uniforms_bind_group_options);
104
105    // @group(1) - Texture & sampler
106    // --------------------------------------------
107    WGPUBindGroupLayoutEntry texture_bind_group_entries[2] = {0};
108
109    // @binding(0)
110    texture_bind_group_entries[0].binding = 0;
111    texture_bind_group_entries[0].visibility = WGPUShaderStage_Fragment;
112    texture_bind_group_entries[0].texture.sampleType = WGPUTextureSampleType_Float;
113    texture_bind_group_entries[0].texture.viewDimension = WGPUTextureViewDimension_2D;
114    // @binding(1)
115    texture_bind_group_entries[1].binding = 1;
116    texture_bind_group_entries[1].visibility = WGPUShaderStage_Fragment;
117    texture_bind_group_entries[1].sampler.type = WGPUSamplerBindingType_Filtering;
118
119    WGPUBindGroupLayoutDescriptor texture_bind_group_options = {0};
120    texture_bind_group_options.entryCount = 2;
121    texture_bind_group_options.entries = texture_bind_group_entries;
122    bind_groups[1] = wgpuDeviceCreateBindGroupLayout(window->device, &texture_bind_group_options);
123
124    // Pipeline layout
125    // --------------------------------------------
126    WGPUPipelineLayoutDescriptor pipeline_layout_options = {0};
127    pipeline_layout_options.bindGroupLayoutCount = 2;
128    pipeline_layout_options.bindGroupLayouts = bind_groups;
129    const WGPUPipelineLayout pipeline_layout = wgpuDeviceCreatePipelineLayout(
130        window->device, &pipeline_layout_options);
131
132    // Create bind group for uniforms
133    create_uniform_bind_group(window, bind_groups[0]);
134
135    wgpuBindGroupLayoutRelease(bind_groups[0]);
136    window->texture_bind_group_layout = bind_groups[1];
137
138    return pipeline_layout;
139}
140
141
142void configure_surface(AlbaWindow* window)
143{
144    int width, height;
145    float x_scale, y_scale;
146    glfwGetWindowSize(window->glfw_window, &width, &height);
147    glfwGetWindowContentScale(window->glfw_window, &x_scale, &y_scale);
148
149    if (width == 0 || height == 0) return;
150
151    window->size = (AlbaVector){width, height};
152    window->scale = (AlbaVector){width / (2 * x_scale), height / (2 * y_scale)};
153
154    // TODO: callback when scaling changes (window changes monitor)
155    // Update uniforms
156    wgpuQueueWriteBuffer(window->queue, window->uniforms, 0, &window->scale, sizeof(AlbaVector));
157
158    const WGPUTextureFormat format = wgpuSurfaceGetPreferredFormat(window->surface, window->adapter);
159
160    WGPUSurfaceConfiguration surface_options = {0};
161    surface_options.device = window->device;
162    surface_options.format = format;
163    surface_options.usage = WGPUTextureUsage_RenderAttachment;
164    surface_options.presentMode = WGPUPresentMode_Fifo;
165    surface_options.width = width;
166    surface_options.height = height;
167    wgpuSurfaceConfigure(window->surface, &surface_options);
168}
169
170void configure_pipeline(AlbaWindow* window)
171{
172    // Load shaders
173    // --------------------------------------------
174    WGPUShaderModuleWGSLDescriptor shader_options = {0};
175    shader_options.chain.sType = WGPUSType_ShaderModuleWGSLDescriptor;
176    shader_options.code = _binary_shaders_wgsl_start;
177
178    WGPUShaderModuleDescriptor shader_loader_options = {0};
179    shader_loader_options.nextInChain = (WGPUChainedStruct*)&shader_options;
180
181    window->shaders = wgpuDeviceCreateShaderModule(window->device, &shader_loader_options);
182
183    // Configure render pipeline
184    // --------------------------------------------
185    WGPURenderPipelineDescriptor pipleine_options = {0};
186    // Rendering settings
187    pipleine_options.primitive.topology = WGPUPrimitiveTopology_TriangleList;
188    pipleine_options.primitive.frontFace = WGPUFrontFace_CCW; // counter clockwise
189    pipleine_options.multisample.count = 4;
190    pipleine_options.multisample.mask = 0xFFFFFFFF;
191
192    // Vertex shader
193    pipleine_options.vertex.module = window->shaders;
194    pipleine_options.vertex.entryPoint = "vertex_shader";
195    pipleine_options.vertex.bufferCount = 2;
196    pipleine_options.vertex.buffers = (WGPUVertexBufferLayout[]){
197        // Position
198        {
199            .arrayStride = sizeof(AlbaVector),
200            .stepMode = WGPUVertexStepMode_Vertex,
201            .attributeCount = 1,
202            .attributes = &(WGPUVertexAttribute){
203                .format = WGPUVertexFormat_Float32x2,
204                .offset = 0,
205                .shaderLocation = 0,
206            },
207        },
208        // Attributes
209        {
210            .arrayStride = sizeof(AlbaAttribute),
211            .stepMode = WGPUVertexStepMode_Vertex,
212            .attributeCount = 2,
213            .attributes = (WGPUVertexAttribute[]){
214                // Color
215                {
216                    .format = WGPUVertexFormat_Float32x4,
217                    .offset = 0,
218                    .shaderLocation = 1
219                },
220                // UV coordinates
221                {
222                    .format = WGPUVertexFormat_Float32x2,
223                    .offset = offsetof(AlbaAttribute, uv),
224                    .shaderLocation = 2,
225                },
226            },
227        },
228    };
229
230    // Fragment shader
231    const WGPUBlendState blend_state = {
232        .color = {
233            .operation = WGPUBlendOperation_Add,
234            .srcFactor = WGPUBlendFactor_SrcAlpha,
235            .dstFactor = WGPUBlendFactor_OneMinusSrcAlpha,
236        },
237        .alpha = {
238            .operation = WGPUBlendOperation_Add,
239            .srcFactor = WGPUBlendFactor_Zero,
240            .dstFactor = WGPUBlendFactor_One,
241        }
242    };
243    WGPUColorTargetState color_state = {0};
244    color_state.format = WGPUTextureFormat_BGRA8UnormSrgb; // TODO: avoid hardcoding
245    color_state.blend = &blend_state;
246    color_state.writeMask = WGPUColorWriteMask_All;
247    WGPUFragmentState fragment_state = {0};
248    fragment_state.module = window->shaders;
249    fragment_state.entryPoint = "fragment_shader";
250    fragment_state.targetCount = 1;
251    fragment_state.targets = &color_state;
252    pipleine_options.fragment = &fragment_state;
253
254    // Configure bind groups layout
255    // --------------------------------------------
256    pipleine_options.layout = configure_bind_group_layout(window);
257    window->pipeline = wgpuDeviceCreateRenderPipeline(window->device, &pipleine_options);
258
259    wgpuPipelineLayoutRelease(pipleine_options.layout);
260}
261
262AlbaWindow* alba_create_window(const AlbaWindowOptions* options)
263{
264    pthread_mutex_lock(&glfw_mutex);
265    if (!glfw_initialized)
266    {
267        if (!glfwInit())
268        {
269            fprintf(stderr, "error: initializeing GLFW failed");
270            pthread_mutex_unlock(&glfw_mutex);
271            exit(1);
272        }
273        glfw_initialized = 1;
274    }
275    pthread_mutex_unlock(&glfw_mutex);
276
277    const AlbaWindowOptions default_options = {0};
278    if (options == NULL)
279    {
280        options = &default_options;
281    }
282
283    AlbaWindow* window = calloc(1, sizeof(AlbaWindow));
284    memset(window, 0, sizeof(AlbaWindow));
285
286    window->options = *options;
287
288    // GLFW window
289    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
290    window->glfw_window = glfwCreateWindow(
291        options->initial_width == 0 ? 800 : options->initial_width,
292        options->initial_height == 0 ? 600 : options->initial_height,
293        options->title == NULL ? "Alba" : options->title,
294        NULL,
295        NULL
296    );
297
298    // Instance & surface initialization
299    window->instance = wgpuCreateInstance(NULL);
300    window->surface = get_window_surface(window->instance, window);
301
302    // Adapter
303    WGPURequestAdapterOptions adapter_options = {0};
304    adapter_options.compatibleSurface = window->surface;
305    wgpuInstanceRequestAdapter(
306        window->instance,
307        &adapter_options,
308        on_receive_adapter,
309        &window->adapter
310    );
311
312    // Device
313    WGPUDeviceDescriptor device_options = {0};
314    device_options.deviceLostCallback = on_device_lost_error;
315    wgpuAdapterRequestDevice(
316        window->adapter,
317        &device_options,
318        on_receive_device,
319        &window->device
320    );
321    wgpuDeviceSetUncapturedErrorCallback(window->device, on_error, NULL);
322
323    window->queue = wgpuDeviceGetQueue(window->device);
324    window->command_encoder = wgpuDeviceCreateCommandEncoder(window->device, NULL);
325
326    configure_pipeline(window);
327    configure_surface(window);
328
329    window->draw_calls = alba_create_array(sizeof(DrawCall), 0);
330    window_create_draw_call(window);
331
332    return window;
333}
334
335uint32_t alba_window_should_close(const AlbaWindow* window)
336{
337    const int should_close = glfwWindowShouldClose(window->glfw_window);
338    if (!should_close)
339    {
340        glfwWaitEventsTimeout(0.01);
341    }
342    return should_close;
343}
344
345void alba_window_release(AlbaWindow* window)
346{
347    for (uint64_t i = 0; i < window->draw_calls.length; i++)
348    {
349        draw_call_release(window->draw_calls.data + i);
350    }
351    alba_array_release(&window->draw_calls);
352
353    wgpuBindGroupRelease(window->uniforms_bind_group);
354    release_buffer(window->uniforms);
355    wgpuRenderPipelineRelease(window->pipeline);
356    wgpuShaderModuleRelease(window->shaders);
357    wgpuCommandEncoderRelease(window->command_encoder);
358    wgpuQueueRelease(window->queue);
359    wgpuDeviceRelease(window->device);
360    wgpuAdapterRelease(window->adapter);
361    wgpuSurfaceRelease(window->surface);
362    wgpuInstanceRelease(window->instance);
363
364    glfwDestroyWindow(window->glfw_window);
365    free(window);
366}
367
368uint32_t frame_status_is_valid(const WGPUSurfaceTexture frame)
369{
370    switch (frame.status)
371    {
372    case WGPUSurfaceGetCurrentTextureStatus_Success:
373        break;
374    case WGPUSurfaceGetCurrentTextureStatus_Timeout:
375    case WGPUSurfaceGetCurrentTextureStatus_Outdated:
376    case WGPUSurfaceGetCurrentTextureStatus_Lost:
377        return 0;
378    case WGPUSurfaceGetCurrentTextureStatus_OutOfMemory:
379        fprintf(stderr, "error: frame allocation failed due to insufficient memory (%d)\n",
380                frame.status);
381    case WGPUSurfaceGetCurrentTextureStatus_DeviceLost:
382        fprintf(stderr, "error: device lost (%d)\n", frame.status);
383    case WGPUSurfaceGetCurrentTextureStatus_Force32:
384        fprintf(stderr, "error: force 32 error (%d)\n", frame.status);
385        exit(1);
386    }
387
388    return 1;
389}
390
391
392void copy_color(const AlbaColor src, WGPUColor* dst)
393{
394    dst->r = src.r;
395    dst->g = src.g;
396    dst->b = src.b;
397    dst->a = src.a;
398}
399
400void ensure_draw_call_updated(
401    const AlbaWindow* window,
402    DrawCall* draw_call
403)
404{
405    if (draw_call->text_dirty)
406    {
407        pthread_mutex_lock(&freetype_mutex);
408        atlas_generate_image(&atlas);
409        pthread_mutex_unlock(&freetype_mutex);
410        draw_call_set_texture(
411            draw_call,
412            create_texture(
413                window,
414                WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst,
415                atlas.size,
416                WGPUTextureFormat_BGRA8UnormSrgb, 1, // TODO: do not assume format
417                atlas.image
418            )
419        );
420        draw_call->texture_set = 1;
421        draw_call_generate_text(draw_call);
422        draw_call->buffers_dirty = 1;
423        draw_call->text_dirty = 0;
424    }
425
426    if (draw_call->buffers_dirty)
427    {
428        draw_call->vertices = update_buffer(
429            window, draw_call->vertices, WGPUBufferUsage_Vertex, draw_call->new_vertices);
430        draw_call->attributes = update_buffer(
431            window, draw_call->attributes, WGPUBufferUsage_Vertex, draw_call->new_attributes);
432        draw_call->indices = update_buffer(
433            window, draw_call->indices, WGPUBufferUsage_Index, draw_call->new_indices);
434
435        draw_call->buffers_dirty = 0;
436    }
437
438    if (draw_call->bind_group_dirty)
439    {
440        draw_call_update_bind_group(window, draw_call);
441        draw_call->bind_group_dirty = 0;
442    }
443}
444
445void draw_objects(
446    const AlbaWindow* window,
447    const WGPUCommandEncoder command_encoder,
448    const WGPURenderPassEncoder render_pass
449)
450{
451    wgpuRenderPassEncoderSetPipeline(render_pass, window->pipeline);
452    wgpuRenderPassEncoderSetBindGroup(render_pass, 0, window->uniforms_bind_group, 0, NULL);
453
454    for (uint64_t i = 0; i < window->draw_calls.length; i++)
455    {
456        DrawCall* draw_call = window->draw_calls.data + i;
457
458        ensure_draw_call_updated(window, draw_call);
459        wgpuRenderPassEncoderSetBindGroup(render_pass, 1, draw_call->bind_group, 0, NULL);
460
461        const uint32_t vertex_size = wgpuBufferGetSize(draw_call->vertices);
462        const uint32_t attributes_size = wgpuBufferGetSize(draw_call->attributes);
463        const uint32_t indices_size = wgpuBufferGetSize(draw_call->indices);
464        if (vertex_size > 0)
465        {
466            wgpuRenderPassEncoderSetVertexBuffer(
467                render_pass, 0, draw_call->vertices, 0, vertex_size);
468            wgpuRenderPassEncoderSetVertexBuffer(
469                render_pass, 1, draw_call->attributes, 0, attributes_size);
470            wgpuRenderPassEncoderSetIndexBuffer(
471                render_pass, draw_call->indices, WGPUIndexFormat_Uint32, 0, indices_size);
472            wgpuRenderPassEncoderDrawIndexed(
473                render_pass, indices_size / sizeof(uint32_t), 1, 0, 0, 0);
474        }
475    }
476
477    wgpuRenderPassEncoderEnd(render_pass);
478
479    // Encode render command and send to GPU
480    const WGPUCommandBuffer command = wgpuCommandEncoderFinish(command_encoder, NULL);
481    wgpuQueueSubmit(window->queue, 1, &command);
482    wgpuCommandBufferRelease(command);
483}
484
485void alba_window_render(AlbaWindow* window)
486{
487    // Setup textures for rendering
488    // --------------------------------------------
489    WGPUSurfaceTexture frame;
490    wgpuSurfaceGetCurrentTexture(window->surface, &frame);
491    if (!frame_status_is_valid(frame))
492    {
493        // Re-configure surface and skip frame
494        if (frame.texture != NULL)
495        {
496            wgpuTextureRelease(frame.texture);
497        }
498
499        configure_surface(window);
500        return;
501    }
502
503    const WGPUTextureView frame_view = wgpuTextureCreateView(frame.texture, NULL);
504    if (frame_view == NULL)
505    {
506        printf("warning: could not get frame view, dropping frame (%d)\n", frame.status);
507        return;
508    }
509
510    // Render of off-screen texture with anti-aliasing (MSAA 4x)
511    const WGPUTexture render_target = create_texture(
512        window,
513        WGPUTextureUsage_RenderAttachment,
514        window->size,
515        WGPUTextureFormat_BGRA8UnormSrgb, 4, // TODO: do not assume format
516        NULL
517    );
518    const WGPUTextureView render_target_view = wgpuTextureCreateView(render_target, NULL);
519    if (render_target_view == NULL)
520    {
521        printf("warning: could not get texture view, dropping frame\n");
522        return;
523    }
524
525    // Draw commands
526    // --------------------------------------------
527    const WGPUCommandEncoder command_encoder = wgpuDeviceCreateCommandEncoder(window->device, NULL);
528
529    // Configures rendering
530    WGPURenderPassColorAttachment render_pass_attachment_options = {0};
531    render_pass_attachment_options.view = render_target_view;
532    render_pass_attachment_options.loadOp = WGPULoadOp_Clear;
533    render_pass_attachment_options.storeOp = WGPUStoreOp_Store;
534    render_pass_attachment_options.resolveTarget = frame_view;
535    copy_color(window->options.clear_color, &render_pass_attachment_options.clearValue);
536
537    WGPURenderPassDescriptor render_pass_options = {0};
538    render_pass_options.colorAttachmentCount = 1;
539    render_pass_options.colorAttachments = &render_pass_attachment_options;
540
541    const WGPURenderPassEncoder render_pass = wgpuCommandEncoderBeginRenderPass(
542        command_encoder, &render_pass_options);
543
544    // Draw calls
545    draw_objects(window, command_encoder, render_pass);
546
547    // Update screen & cleanup
548    // --------------------------------------------
549    wgpuSurfacePresent(window->surface);
550
551    wgpuRenderPassEncoderRelease(render_pass);
552    wgpuCommandEncoderRelease(command_encoder);
553
554    wgpuTextureViewRelease(render_target_view);
555    release_texture(render_target);
556    wgpuTextureViewRelease(frame_view);
557    // release_texture(frame.texture);
558}
559
560
561void alba_window_get_size(const AlbaWindow* window, float* width, float* height)
562{
563    int raw_width, raw_height;
564    float x_scale, y_scale;
565    glfwGetWindowSize(window->glfw_window, &raw_width, &raw_height);
566    glfwGetWindowContentScale(window->glfw_window, &x_scale, &y_scale);
567
568    *width = raw_width / x_scale;
569    *height = raw_height / y_scale;
570}
571
572void alba_release()
573{
574    if (freetype != NULL)
575    {
576        // freetype errors are ignored here
577        FT_Done_FreeType(freetype);
578        // TODO: free faces
579        FT_Done_Face(roboto);
580        atlas_release(&atlas);
581    }
582    glfwTerminate();
583}