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