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