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