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