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