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