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