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