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