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