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