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