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