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