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
 71WGPUPipelineLayout configure_uniforms(AlbaWindow* window)
 72{
 73    WGPUBufferDescriptor uniform_options = {0};
 74    uniform_options.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform;
 75    uniform_options.size = 2 * sizeof(float);
 76    window->drawing.uniforms = wgpuDeviceCreateBuffer(window->device, &uniform_options);
 77
 78    // @binding(0)
 79    WGPUBindGroupLayoutEntry bind_group_layout_entry = {0};
 80    bind_group_layout_entry.binding = 0;
 81    bind_group_layout_entry.visibility = WGPUShaderStage_Vertex;
 82    bind_group_layout_entry.buffer.type = WGPUBufferBindingType_Uniform;
 83
 84    WGPUBindGroupLayoutDescriptor bind_group_layout_options = {0};
 85    bind_group_layout_options.entryCount = 1;
 86    bind_group_layout_options.entries = &bind_group_layout_entry;
 87    const WGPUBindGroupLayout bind_group_layout = wgpuDeviceCreateBindGroupLayout(
 88        window->device, &bind_group_layout_options);
 89
 90    // @group(0)
 91    WGPUBindGroupEntry bind_group_entry = {0};
 92    bind_group_entry.binding = 0;
 93    bind_group_entry.buffer = window->drawing.uniforms;
 94    bind_group_entry.size = uniform_options.size;
 95
 96    WGPUBindGroupDescriptor binding_group_options = {0};
 97    binding_group_options.layout = bind_group_layout;
 98    binding_group_options.entryCount = 1;
 99    binding_group_options.entries = &bind_group_entry;
100
101    window->bind_group = wgpuDeviceCreateBindGroup(window->device, &binding_group_options);
102
103    // A pipeline can have multiple bind groups
104    WGPUPipelineLayoutDescriptor pipeline_layout_options = {0};
105    pipeline_layout_options.bindGroupLayoutCount = 1;
106    pipeline_layout_options.bindGroupLayouts = &bind_group_layout;
107    const WGPUPipelineLayout pipeline_layout = wgpuDeviceCreatePipelineLayout(
108        window->device, &pipeline_layout_options);
109
110    wgpuBindGroupLayoutRelease(bind_group_layout);
111
112    return pipeline_layout;
113}
114
115void configure_surface(AlbaWindow* window)
116{
117    int width, height;
118    float x_scale, y_scale;
119    glfwGetWindowSize(window->glfw_window, &width, &height);
120    glfwGetWindowContentScale(window->glfw_window, &x_scale, &y_scale);
121
122    if (width == 0 || height == 0) return;
123
124    window->width = width;
125    window->height = height;
126
127    // Update uniforms
128    const float scale[2] = {width / (2. * x_scale), height / (2. * y_scale)};
129    wgpuQueueWriteBuffer(window->queue, window->drawing.uniforms, 0, &scale, 2 * sizeof(uint32_t));
130
131    const WGPUTextureFormat format = wgpuSurfaceGetPreferredFormat(window->surface, window->adapter);
132
133    WGPUSurfaceConfiguration surface_options = {0};
134    surface_options.device = window->device;
135    surface_options.format = format;
136    surface_options.usage = WGPUTextureUsage_RenderAttachment;
137    surface_options.presentMode = WGPUPresentMode_Fifo;
138    surface_options.width = width;
139    surface_options.height = height;
140    wgpuSurfaceConfigure(window->surface, &surface_options);
141}
142
143
144void configure_pipeline(AlbaWindow* window)
145{
146    WGPUShaderModuleWGSLDescriptor shader_options = {0};
147    shader_options.chain.sType = WGPUSType_ShaderModuleWGSLDescriptor;
148    shader_options.code = _binary_shaders_wgsl_start;
149
150    WGPUShaderModuleDescriptor shader_loader_options = {0};
151    shader_loader_options.nextInChain = (WGPUChainedStruct*)&shader_options;
152
153    window->shaders = wgpuDeviceCreateShaderModule(window->device, &shader_loader_options);
154
155    // Configure render pipeline
156    WGPURenderPipelineDescriptor pipleine_options = {0};
157    // Rendering settings
158    pipleine_options.primitive.topology = WGPUPrimitiveTopology_TriangleList;
159    pipleine_options.primitive.frontFace = WGPUFrontFace_CCW; // counter clockwise
160    pipleine_options.multisample.count = 4;
161    pipleine_options.multisample.mask = 0xFFFFFFFF;
162
163    // Vertex shader
164    pipleine_options.vertex.module = window->shaders;
165    pipleine_options.vertex.entryPoint = "vertex_shader";
166    pipleine_options.vertex.bufferCount = 2;
167    pipleine_options.vertex.buffers = (WGPUVertexBufferLayout []){
168        // Position
169        {
170            .arrayStride = 2 * sizeof(float),
171            .stepMode = WGPUVertexStepMode_Vertex,
172            .attributeCount = 1,
173            .attributes = &(WGPUVertexAttribute){
174                .format = WGPUVertexFormat_Float32x2,
175                .offset = 0,
176                .shaderLocation = 0,
177            },
178        },
179        // Color
180        {
181            .arrayStride = 4 * sizeof(float),
182            .stepMode = WGPUVertexStepMode_Vertex,
183            .attributeCount = 1,
184            .attributes = &(WGPUVertexAttribute){
185                .format = WGPUVertexFormat_Float32x4,
186                .offset = 0,
187                .shaderLocation = 1,
188            },
189        },
190    };
191
192    // Fragment shader
193    const WGPUBlendState blend_state = {
194        .color = {
195            .operation = WGPUBlendOperation_Add,
196            .srcFactor = WGPUBlendFactor_SrcAlpha,
197            .dstFactor = WGPUBlendFactor_OneMinusSrcAlpha,
198        },
199        .alpha = {
200            .operation = WGPUBlendOperation_Add,
201            .srcFactor = WGPUBlendFactor_Zero,
202            .dstFactor = WGPUBlendFactor_One,
203        }
204    };
205    WGPUColorTargetState color_state = {0};
206    color_state.format = WGPUTextureFormat_BGRA8UnormSrgb; // TODO: avoid hardcoding
207    color_state.blend = &blend_state;
208    color_state.writeMask = WGPUColorWriteMask_All;
209    WGPUFragmentState fragment_state = {0};
210    fragment_state.module = window->shaders;
211    fragment_state.entryPoint = "fragment_shader";
212    fragment_state.targetCount = 1;
213    fragment_state.targets = &color_state;
214    pipleine_options.fragment = &fragment_state;
215
216    // Uniform buffer
217    pipleine_options.layout = configure_uniforms(window);
218
219    window->pipeline = wgpuDeviceCreateRenderPipeline(window->device, &pipleine_options);
220
221    wgpuPipelineLayoutRelease(pipleine_options.layout);
222}
223
224void init_render_pass_data(RenderPassData* data)
225{
226    data->new_vertices = dynarray_new(sizeof(float), 0);
227    data->new_attributes = dynarray_new(sizeof(float), 0);
228    data->new_indices = dynarray_new(sizeof(uint32_t), 0);
229}
230
231AlbaWindow* create_window(const AlbaWindowOptions* options)
232{
233    pthread_mutex_lock(&glfw_mutex);
234    if (!glfw_initialized)
235    {
236        if (!glfwInit())
237        {
238            printf("fatal error: initializeing GLFW failed");
239            pthread_mutex_unlock(&glfw_mutex);
240            exit(1);
241        }
242        glfw_initialized = 1;
243    }
244    pthread_mutex_unlock(&glfw_mutex);
245
246    const AlbaWindowOptions default_options = {0};
247    if (options == NULL)
248    {
249        options = &default_options;
250    }
251
252    AlbaWindow* window = calloc(1, sizeof(AlbaWindow));
253    memset(window, 0, sizeof(AlbaWindow));
254    init_render_pass_data(&window->drawing);
255    init_render_pass_data(&window->text);
256    window->options = *options;
257
258
259    // GLFW window
260    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
261    window->glfw_window = glfwCreateWindow(
262        options->initial_width == 0 ? 800 : options->initial_width,
263        options->initial_height == 0 ? 600 : options->initial_height,
264        options->title == NULL ? "Alba" : options->title,
265        NULL,
266        NULL
267    );
268
269    // Instance & surface initialization
270    window->instance = wgpuCreateInstance(NULL);
271    window->surface = get_window_surface(window->instance, window);
272
273    // Adapter
274    WGPURequestAdapterOptions adapter_options = {0};
275    adapter_options.compatibleSurface = window->surface;
276    wgpuInstanceRequestAdapter(
277        window->instance,
278        &adapter_options,
279        on_receive_adapter,
280        &window->adapter
281    );
282
283    // Device
284    WGPUDeviceDescriptor device_options = {0};
285    device_options.deviceLostCallback = on_device_lost_error;
286    wgpuAdapterRequestDevice(
287        window->adapter,
288        &device_options,
289        on_receive_device,
290        &window->device
291    );
292
293    // Errors
294    wgpuDeviceSetUncapturedErrorCallback(window->device, on_error, NULL);
295    // Queue
296    window->queue = wgpuDeviceGetQueue(window->device);
297
298    configure_pipeline(window);
299    configure_surface(window);
300
301    // This is necessary to initialize buffers
302    window->drawing.dirty = 1;
303
304    return window;
305}
306
307uint32_t window_should_close(const AlbaWindow* window)
308{
309    const int should_close = glfwWindowShouldClose(window->glfw_window);
310    if (!should_close)
311    {
312        glfwWaitEventsTimeout(0.01);
313    }
314    return should_close;
315}
316
317void window_release(AlbaWindow* window)
318{
319    wgpuBufferDestroy(window->drawing.uniforms);
320    wgpuBufferRelease(window->drawing.uniforms);
321
322    wgpuBufferDestroy(window->drawing.vertices);
323    wgpuBufferRelease(window->drawing.vertices);
324    dynarray_release(&window->drawing.new_vertices);
325
326    wgpuBufferDestroy(window->drawing.attributes);
327    wgpuBufferRelease(window->drawing.attributes);
328    dynarray_release(&window->drawing.new_attributes);
329
330    wgpuBufferDestroy(window->drawing.indices);
331    wgpuBufferRelease(window->drawing.indices);
332    dynarray_release(&window->drawing.new_indices);
333
334    wgpuBindGroupRelease(window->bind_group);
335    wgpuShaderModuleRelease(window->shaders);
336    wgpuRenderPipelineRelease(window->pipeline);
337    wgpuQueueRelease(window->queue);
338    wgpuDeviceRelease(window->device);
339    wgpuAdapterRelease(window->adapter);
340    wgpuSurfaceRelease(window->surface);
341    wgpuInstanceRelease(window->instance);
342
343    glfwDestroyWindow(window->glfw_window);
344    free(window);
345}
346
347uint32_t frame_status_is_valid(const AlbaWindow* window, const WGPUSurfaceTexture frame)
348{
349    switch (frame.status)
350    {
351    case WGPUSurfaceGetCurrentTextureStatus_Success:
352        break;
353    case WGPUSurfaceGetCurrentTextureStatus_Timeout:
354    case WGPUSurfaceGetCurrentTextureStatus_Outdated:
355    case WGPUSurfaceGetCurrentTextureStatus_Lost:
356        return 0;
357    case WGPUSurfaceGetCurrentTextureStatus_OutOfMemory:
358        printf("fatal error: frame allocation failed due to insufficient memory (%d)\n",
359               frame.status);
360    case WGPUSurfaceGetCurrentTextureStatus_DeviceLost:
361        printf("fatal error: device lost (%d)\n", frame.status);
362    case WGPUSurfaceGetCurrentTextureStatus_Force32:
363        printf("fatal error: force 32 error (%d)\n", frame.status);
364        exit(1);
365    }
366
367    return 1;
368}
369
370WGPUBuffer create_buffer(const AlbaWindow* window, const uint64_t size, const void* data,
371                         const WGPUBufferUsageFlags flags)
372{
373    WGPUBufferDescriptor buffer_options = {0};
374    buffer_options.usage = WGPUBufferUsage_CopyDst | flags;
375    buffer_options.size = size;
376    const WGPUBuffer buffer = wgpuDeviceCreateBuffer(window->device, &buffer_options);
377    wgpuQueueWriteBuffer(window->queue, buffer, 0, data, size);
378    return buffer;
379}
380
381void clear_buffer(const WGPUBuffer buffer)
382{
383    if (buffer != NULL)
384    {
385        wgpuBufferDestroy(buffer);
386        wgpuBufferRelease(buffer);
387    }
388}
389
390WGPUBuffer update_buffer(const AlbaWindow* window, WGPUBuffer buffer, DynArray data)
391{
392    // Deallocate old buffer if any
393    clear_buffer(buffer);
394    // Copy data to new buffer
395    buffer = create_buffer(window, data.length * data.element_size, data.data, WGPUBufferUsage_Vertex);
396    // Clear local copy for next frame
397    dynarray_clear(&data);
398    return buffer;
399}
400
401WGPUBuffer uint32_update_buffer(const AlbaWindow* window, WGPUBuffer buffer, DynArray data)
402{
403    // Deallocate old buffer if any
404    clear_buffer(buffer);
405    // Copy data to new buffer
406    buffer = create_buffer(window, data.length * sizeof(uint32_t), data.data, WGPUBufferUsage_Index);
407    // Clear local copy for next frame
408    dynarray_clear(&data);
409    return buffer;
410}
411
412void copy_color(const AlbaColor src, WGPUColor* dst)
413{
414    dst->r = src.r;
415    dst->g = src.g;
416    dst->b = src.b;
417    dst->a = src.a;
418}
419
420void render_pass(
421    const AlbaWindow* window,
422    const WGPUTextureView frame,
423    const WGPUTextureView render_target,
424    const RenderPassData* data
425)
426{
427    const WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(window->device, NULL);
428
429    // Configures rendering
430    WGPURenderPassColorAttachment render_pass_attachment_options = {0};
431    render_pass_attachment_options.view = render_target;
432    render_pass_attachment_options.loadOp = WGPULoadOp_Clear;
433    render_pass_attachment_options.storeOp = WGPUStoreOp_Store;
434    render_pass_attachment_options.resolveTarget = frame;
435    copy_color(window->options.clear_color, &render_pass_attachment_options.clearValue);
436
437    WGPURenderPassDescriptor render_pass_options = {0};
438    render_pass_options.colorAttachmentCount = 1;
439    render_pass_options.colorAttachments = &render_pass_attachment_options;
440
441    const WGPURenderPassEncoder render_pass = wgpuCommandEncoderBeginRenderPass(
442        encoder, &render_pass_options);
443
444    wgpuRenderPassEncoderSetPipeline(render_pass, window->pipeline);
445    wgpuRenderPassEncoderSetBindGroup(render_pass, 0, window->bind_group, 0, NULL);
446
447    const uint32_t vertex_size = wgpuBufferGetSize(data->vertices);
448    const uint32_t attributes_size = wgpuBufferGetSize(data->attributes);
449    const uint32_t indices_size = wgpuBufferGetSize(data->indices);
450    if (vertex_size > 0)
451    {
452        wgpuRenderPassEncoderSetVertexBuffer(render_pass, 0, data->vertices, 0, vertex_size);
453        wgpuRenderPassEncoderSetVertexBuffer(render_pass, 1, data->attributes, 0, attributes_size);
454        wgpuRenderPassEncoderSetIndexBuffer(render_pass, data->indices, WGPUIndexFormat_Uint32, 0,
455                                            indices_size);
456        wgpuRenderPassEncoderDrawIndexed(render_pass, indices_size / sizeof(uint32_t), 1, 0, 0, 0);
457    }
458
459    wgpuRenderPassEncoderEnd(render_pass);
460
461    // Encode render command and send to GPU
462    const WGPUCommandBuffer command = wgpuCommandEncoderFinish(encoder, NULL);
463    wgpuQueueSubmit(window->queue, 1, &command);
464
465    wgpuCommandBufferRelease(command);
466    wgpuRenderPassEncoderRelease(render_pass);
467    wgpuCommandEncoderRelease(encoder);
468}
469
470void window_render(AlbaWindow* window)
471{
472    if (window->drawing.dirty)
473    {
474        window->drawing.vertices = update_buffer(window, window->drawing.vertices, window->drawing.new_vertices);
475        window->drawing.attributes = update_buffer(window, window->drawing.attributes,
476                                                   window->drawing.new_attributes);
477        window->drawing.indices = uint32_update_buffer(window, window->drawing.indices, window->drawing.new_indices);
478        window->drawing.dirty = 0;
479    }
480
481
482    WGPUSurfaceTexture frame;
483    wgpuSurfaceGetCurrentTexture(window->surface, &frame);
484    if (!frame_status_is_valid(window, frame))
485    {
486        // Re-configure surface and skip frame
487        if (frame.texture != NULL)
488        {
489            wgpuTextureRelease(frame.texture);
490        }
491
492        configure_surface(window);
493        return;
494    }
495
496    WGPUTextureDescriptor texture_options = {0};
497    texture_options.usage = WGPUTextureUsage_RenderAttachment;
498    texture_options.format = WGPUTextureFormat_BGRA8UnormSrgb; // TODO: avoid hardcoding
499    texture_options.dimension = WGPUTextureDimension_2D;
500    texture_options.size.width = window->width;
501    texture_options.size.height = window->height;
502    texture_options.size.depthOrArrayLayers = 1;
503    texture_options.sampleCount = 4;
504    texture_options.mipLevelCount = 1;
505    const WGPUTexture render_target = wgpuDeviceCreateTexture(window->device, &texture_options);
506
507    const WGPUTextureView frame_view = wgpuTextureCreateView(frame.texture, NULL);
508    if (frame_view == NULL)
509    {
510        printf("warning: could not get frame view, dropping frame (%d)\n", frame.status);
511        return;
512    }
513
514    const WGPUTextureView render_target_view = wgpuTextureCreateView(render_target, NULL);
515    if (render_target_view == NULL)
516    {
517        printf("warning: could not get texture view, dropping frame\n");
518        return;
519    }
520
521    // Draw calls
522    render_pass(window, frame_view, render_target_view, &window->drawing);
523    // render_pass(window, frame_view, render_target_view, &window->text);
524
525    // Update screen
526    wgpuSurfacePresent(window->surface);
527
528    wgpuTextureViewRelease(render_target_view);
529    wgpuTextureViewRelease(frame_view);
530    wgpuTextureRelease(render_target);
531    wgpuTextureRelease(frame.texture);
532}
533
534
535void window_get_size(const AlbaWindow* window, float* width, float* height)
536{
537    int raw_width, raw_height;
538    float x_scale, y_scale;
539    glfwGetWindowSize(window->glfw_window, &raw_width, &raw_height);
540    glfwGetWindowContentScale(window->glfw_window, &x_scale, &y_scale);
541
542    *width = raw_width / x_scale;
543    *height = raw_height / y_scale;
544}
545
546void alba_release()
547{
548    if (freetype != NULL)
549    {
550        FT_Done_FreeType(freetype); // ignore error
551        // TODO: free faces
552    }
553    glfwTerminate();
554}