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