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