src/window.c
1#include "alba.h"
2
3#include <stdio.h>
4#include <stdlib.h>
5#include <string.h>
6
7#include "GLFW/glfw3.h"
8#include "webgpu.h"
9
10extern char _binary_shaders_wgsl_start[];
11
12// forward-declare
13WGPUSurface get_window_surface(WGPUInstance instance, AlbaWindow* window);
14
15void on_receive_adapter(
16 const WGPURequestAdapterStatus status,
17 const WGPUAdapter adapter,
18 char const* message,
19 void* data
20)
21{
22 if (status != WGPURequestAdapterStatus_Success)
23 {
24 printf("fatal error: requesting adapter failed: %s", message);
25 exit(1);
26 }
27
28 *((WGPUAdapter*)data) = adapter;
29}
30
31void on_device_lost_error(const WGPUDeviceLostReason reason, char const* message, void* userdata)
32{
33 printf("fatal device lost error (%d): %s", reason, message);
34 exit(1);
35}
36
37void on_receive_device(
38 const WGPURequestDeviceStatus status,
39 const WGPUDevice device,
40 char const* message,
41 void* data
42)
43{
44 if (status != WGPURequestDeviceStatus_Success)
45 {
46 printf("fatal error: requesting device failed: %s", message);
47 exit(1);
48 }
49
50 *((WGPUDevice*)data) = device;
51}
52
53void on_error(const WGPUErrorType type, char const* message, void* data)
54{
55 if (type == WGPUErrorType_NoError)
56 {
57 return;
58 }
59 printf("error (%d): %s", type, message);
60}
61
62WGPUPipelineLayout configure_uniforms(AlbaWindow* window)
63{
64 WGPUBufferDescriptor uniform_options = {0};
65 uniform_options.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform;
66 uniform_options.size = 2 * sizeof(float);
67 window->uniforms = wgpuDeviceCreateBuffer(window->device, &uniform_options);
68
69 // @binding(0)
70 WGPUBindGroupLayoutEntry bind_group_layout_entry = {0};
71 bind_group_layout_entry.binding = 0;
72 bind_group_layout_entry.visibility = WGPUShaderStage_Vertex;
73 bind_group_layout_entry.buffer.type = WGPUBufferBindingType_Uniform;
74
75 WGPUBindGroupLayoutDescriptor bind_group_layout_options = {0};
76 bind_group_layout_options.entryCount = 1;
77 bind_group_layout_options.entries = &bind_group_layout_entry;
78 const WGPUBindGroupLayout bind_group_layout = wgpuDeviceCreateBindGroupLayout(
79 window->device, &bind_group_layout_options);
80
81 // @group(0)
82 WGPUBindGroupEntry bind_group_entry = {0};
83 bind_group_entry.binding = 0;
84 bind_group_entry.buffer = window->uniforms;
85 bind_group_entry.size = uniform_options.size;
86
87 WGPUBindGroupDescriptor binding_group_options = {0};
88 binding_group_options.layout = bind_group_layout;
89 binding_group_options.entryCount = 1;
90 binding_group_options.entries = &bind_group_entry;
91
92 window->bind_group = wgpuDeviceCreateBindGroup(window->device, &binding_group_options);
93
94 // A pipeline can have multiple bind groups
95 WGPUPipelineLayoutDescriptor pipeline_layout_options = {0};
96 pipeline_layout_options.bindGroupLayoutCount = 1;
97 pipeline_layout_options.bindGroupLayouts = &bind_group_layout;
98 const WGPUPipelineLayout pipeline_layout = wgpuDeviceCreatePipelineLayout(
99 window->device, &pipeline_layout_options);
100
101 wgpuBindGroupLayoutRelease(bind_group_layout);
102
103 return pipeline_layout;
104}
105
106void configure_surface(const AlbaWindow* window)
107{
108 int width, height;
109 float x_scale, y_scale;
110 glfwGetWindowSize(window->glfw_window, &width, &height);
111 glfwGetWindowContentScale(window->glfw_window, &x_scale, &y_scale);
112
113 if (width == 0 || height == 0) return;
114
115 // Update uniforms
116 const float scale[2] = {width / (2. * x_scale), height / (2. * y_scale)};
117 wgpuQueueWriteBuffer(window->queue, window->uniforms, 0, &scale, 2 * sizeof(uint32_t));
118
119 const WGPUTextureFormat format = wgpuSurfaceGetPreferredFormat(window->surface, window->adapter);
120
121 WGPUSurfaceConfiguration surface_options = {0};
122 surface_options.device = window->device;
123 surface_options.format = format;
124 surface_options.usage = WGPUTextureUsage_RenderAttachment;
125 surface_options.presentMode = WGPUPresentMode_Fifo;
126 surface_options.width = width;
127 surface_options.height = height;
128 wgpuSurfaceConfigure(window->surface, &surface_options);
129}
130
131
132void configure_pipeline(AlbaWindow* window)
133{
134 WGPUShaderModuleWGSLDescriptor shader_options = {0};
135 shader_options.chain.sType = WGPUSType_ShaderModuleWGSLDescriptor;
136 shader_options.code = _binary_shaders_wgsl_start;
137
138 WGPUShaderModuleDescriptor shader_loader_options = {0};
139 shader_loader_options.nextInChain = (WGPUChainedStruct*)&shader_options;
140
141 window->shaders = wgpuDeviceCreateShaderModule(window->device, &shader_loader_options);
142
143 // Configure render pipeline
144 WGPURenderPipelineDescriptor pipleine_options = {0};
145 // Rendering settings
146 pipleine_options.primitive.topology = WGPUPrimitiveTopology_TriangleList;
147 pipleine_options.primitive.frontFace = WGPUFrontFace_CCW; // counter clockwise
148 pipleine_options.multisample.count = 1; // no anti-aliasing
149 pipleine_options.multisample.mask = 0xFFFFFFFF;
150
151 // Vertex shader
152 pipleine_options.vertex.module = window->shaders;
153 pipleine_options.vertex.entryPoint = "vertex_shader";
154 pipleine_options.vertex.bufferCount = 2;
155 pipleine_options.vertex.buffers = (WGPUVertexBufferLayout []){
156 // Position
157 {
158 .arrayStride = 2 * sizeof(float),
159 .stepMode = WGPUVertexStepMode_Vertex,
160 .attributeCount = 1,
161 .attributes = &(WGPUVertexAttribute){
162 .format = WGPUVertexFormat_Float32x2,
163 .offset = 0,
164 .shaderLocation = 0,
165 },
166 },
167 // Color
168 {
169 .arrayStride = 4 * sizeof(float),
170 .stepMode = WGPUVertexStepMode_Vertex,
171 .attributeCount = 1,
172 .attributes = &(WGPUVertexAttribute){
173 .format = WGPUVertexFormat_Float32x4,
174 .offset = 0,
175 .shaderLocation = 1,
176 },
177 },
178 };
179
180 // Fragment shader
181 const WGPUBlendState blend_state = {
182 .color = {
183 .operation = WGPUBlendOperation_Add,
184 .srcFactor = WGPUBlendFactor_SrcAlpha,
185 .dstFactor = WGPUBlendFactor_OneMinusSrcAlpha,
186 },
187 .alpha = {
188 .operation = WGPUBlendOperation_Add,
189 .srcFactor = WGPUBlendFactor_Zero,
190 .dstFactor = WGPUBlendFactor_One,
191 }
192 };
193 WGPUColorTargetState color_state = {0};
194 color_state.format = WGPUTextureFormat_BGRA8UnormSrgb; // TODO: avoid hardcoding
195 color_state.blend = &blend_state;
196 color_state.writeMask = WGPUColorWriteMask_All;
197 WGPUFragmentState fragment_state = {0};
198 fragment_state.module = window->shaders;
199 fragment_state.entryPoint = "fragment_shader";
200 fragment_state.targetCount = 1;
201 fragment_state.targets = &color_state;
202 pipleine_options.fragment = &fragment_state;
203
204 // Uniform buffer
205 pipleine_options.layout = configure_uniforms(window);
206
207 window->pipeline = wgpuDeviceCreateRenderPipeline(window->device, &pipleine_options);
208}
209
210AlbaWindow* create_window(const AlbaWindowOptions* options)
211{
212 const AlbaWindowOptions default_options = {0};
213 if (options == NULL)
214 {
215 options = &default_options;
216 }
217
218 AlbaWindow* window = malloc(sizeof(AlbaWindow));
219 memset(window, 0, sizeof(AlbaWindow));
220 window->options = *options;
221
222 if (!glfwInit())
223 {
224 printf("fatal error: initializeing GLFW failed");
225 exit(1);
226 }
227
228 // GLFW window
229 glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
230 window->glfw_window = glfwCreateWindow(
231 options->initial_width == 0 ? 800 : options->initial_width,
232 options->initial_height == 0 ? 600 : options->initial_height,
233 options->title == NULL ? "Alba" : options->title,
234 NULL,
235 NULL
236 );
237
238 // Instance & surface initialization
239 window->instance = wgpuCreateInstance(NULL);
240 window->surface = get_window_surface(window->instance, window);
241
242 // Adapter
243 WGPURequestAdapterOptions adapter_options = {0};
244 adapter_options.compatibleSurface = window->surface;
245 wgpuInstanceRequestAdapter(
246 window->instance,
247 &adapter_options,
248 on_receive_adapter,
249 &window->adapter
250 );
251
252 // Device
253 WGPUDeviceDescriptor device_options = {0};
254 device_options.deviceLostCallback = on_device_lost_error;
255 wgpuAdapterRequestDevice(
256 window->adapter,
257 &device_options,
258 on_receive_device,
259 &window->device
260 );
261
262 // Errors
263 wgpuDeviceSetUncapturedErrorCallback(window->device, on_error, NULL);
264 // Queue
265 window->queue = wgpuDeviceGetQueue(window->device);
266
267 configure_pipeline(window);
268 configure_surface(window);
269
270 // This is necessary to initialize buffers
271 window->dirty = 1;
272
273 return window;
274}
275
276uint32_t window_should_close(const AlbaWindow* window)
277{
278 const int should_close = glfwWindowShouldClose(window->glfw_window);
279 if (!should_close)
280 {
281 glfwWaitEventsTimeout(0.01);
282 }
283 return should_close;
284}
285
286void window_release(AlbaWindow* window)
287{
288 wgpuBufferDestroy(window->vertices);
289 wgpuBufferRelease(window->vertices);
290 float_array_release(&window->new_vertices);
291
292 wgpuBufferDestroy(window->attributes);
293 wgpuBufferRelease(window->attributes);
294 float_array_release(&window->new_attributes);
295
296 wgpuBufferDestroy(window->indices);
297 wgpuBufferRelease(window->indices);
298 uint32_array_release(&window->new_indices);
299
300 wgpuShaderModuleRelease(window->shaders);
301 wgpuRenderPipelineRelease(window->pipeline);
302 wgpuQueueRelease(window->queue);
303 wgpuDeviceRelease(window->device);
304 wgpuAdapterRelease(window->adapter);
305 wgpuSurfaceRelease(window->surface);
306 wgpuInstanceRelease(window->instance);
307
308 glfwDestroyWindow(window->glfw_window);
309 free(window);
310 glfwTerminate();
311}
312
313uint32_t frame_status_is_valid(const AlbaWindow* window, const WGPUSurfaceTexture frame)
314{
315 switch (frame.status)
316 {
317 case WGPUSurfaceGetCurrentTextureStatus_Success:
318 break;
319 case WGPUSurfaceGetCurrentTextureStatus_Timeout:
320 case WGPUSurfaceGetCurrentTextureStatus_Outdated:
321 case WGPUSurfaceGetCurrentTextureStatus_Lost:
322 return 0;
323 case WGPUSurfaceGetCurrentTextureStatus_OutOfMemory:
324 printf("fatal error: frame allocation failed due to insufficient memory (%d)\n",
325 frame.status);
326 case WGPUSurfaceGetCurrentTextureStatus_DeviceLost:
327 printf("fatal error: device lost (%d)\n", frame.status);
328 case WGPUSurfaceGetCurrentTextureStatus_Force32:
329 printf("fatal error: force 32 error (%d)\n", frame.status);
330 exit(1);
331 }
332
333 return 1;
334}
335
336void copy_color(const AlbaColor src, WGPUColor* dst)
337{
338 dst->r = src.r;
339 dst->g = src.g;
340 dst->b = src.b;
341 dst->a = src.a;
342}
343
344WGPUBuffer create_buffer(const AlbaWindow* window, const uint64_t size, const void* data, WGPUBufferUsageFlags flags)
345{
346 WGPUBufferDescriptor buffer_options = {0};
347 buffer_options.usage = WGPUBufferUsage_CopyDst | flags;
348 buffer_options.size = size;
349 const WGPUBuffer buffer = wgpuDeviceCreateBuffer(window->device, &buffer_options);
350 wgpuQueueWriteBuffer(window->queue, buffer, 0, data, size);
351 return buffer;
352}
353
354void clear_buffer(const WGPUBuffer buffer)
355{
356 if (buffer != NULL)
357 {
358 wgpuBufferDestroy(buffer);
359 wgpuBufferRelease(buffer);
360 }
361}
362
363WGPUBuffer float_update_buffer(const AlbaWindow* window, WGPUBuffer buffer, FloatArray data)
364{
365 // Deallocate old buffer if any
366 clear_buffer(buffer);
367 // Copy data to new buffer
368 buffer = create_buffer(window, data.length * sizeof(float), data.data, WGPUBufferUsage_Vertex);
369 // Clear local copy for next frame
370 float_array_clear(&data);
371 return buffer;
372}
373
374WGPUBuffer uint32_update_buffer(const AlbaWindow* window, WGPUBuffer buffer, Uint32Array data)
375{
376 // Deallocate old buffer if any
377 clear_buffer(buffer);
378 // Copy data to new buffer
379 buffer = create_buffer(window, data.length * sizeof(uint32_t), data.data, WGPUBufferUsage_Index);
380 // Clear local copy for next frame
381 uint32_array_clear(&data);
382 return buffer;
383}
384
385void window_render(AlbaWindow* window)
386{
387 if (window->dirty)
388 {
389 window->vertices = float_update_buffer(window, window->vertices, window->new_vertices);
390 window->attributes = float_update_buffer(window, window->attributes, window->new_attributes);
391 window->indices = uint32_update_buffer(window, window->indices, window->new_indices);
392 window->dirty = 0;
393 }
394
395 WGPUSurfaceTexture frame;
396 wgpuSurfaceGetCurrentTexture(window->surface, &frame);
397 if (!frame_status_is_valid(window, frame))
398 {
399 // Re-configure surface and skip frame
400 if (frame.texture != NULL)
401 {
402 wgpuTextureRelease(frame.texture);
403 }
404
405 configure_surface(window);
406 return;
407 }
408
409 const WGPUTextureView view = wgpuTextureCreateView(frame.texture, NULL);
410 if (view == NULL)
411 {
412 printf("warning: could not get frame view, dropping frame (%d)\n", frame.status);
413 return;
414 }
415
416 const WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(window->device, NULL);
417
418 // Configures rendering
419 WGPURenderPassColorAttachment render_pass_attachment_options = {0};
420 render_pass_attachment_options.view = view;
421 render_pass_attachment_options.loadOp = WGPULoadOp_Clear;
422 render_pass_attachment_options.storeOp = WGPUStoreOp_Store;
423 copy_color(window->options.clear_color, &render_pass_attachment_options.clearValue);
424
425 WGPURenderPassDescriptor render_pass_options = {0};
426 render_pass_options.colorAttachmentCount = 1;
427 render_pass_options.colorAttachments = &render_pass_attachment_options;
428
429 // Draw calls
430 const WGPURenderPassEncoder render_pass = wgpuCommandEncoderBeginRenderPass(
431 encoder, &render_pass_options);
432
433 wgpuRenderPassEncoderSetPipeline(render_pass, window->pipeline);
434 wgpuRenderPassEncoderSetBindGroup(render_pass, 0, window->bind_group, 0, NULL);
435
436 const uint32_t vertex_size = wgpuBufferGetSize(window->vertices);
437 const uint32_t attributes_size = wgpuBufferGetSize(window->attributes);
438 const uint32_t indices_size = wgpuBufferGetSize(window->indices);
439 if (vertex_size > 0)
440 {
441 wgpuRenderPassEncoderSetVertexBuffer(render_pass, 0, window->vertices, 0, vertex_size);
442 wgpuRenderPassEncoderSetVertexBuffer(render_pass, 1, window->attributes, 0, attributes_size);
443 wgpuRenderPassEncoderSetIndexBuffer(render_pass, window->indices, WGPUIndexFormat_Uint32, 0, indices_size);
444 wgpuRenderPassEncoderDrawIndexed(render_pass, indices_size / sizeof(uint32_t), 1, 0, 0, 0);
445 }
446
447 wgpuRenderPassEncoderEnd(render_pass);
448
449 // Encode render command and send to GPU
450 const WGPUCommandBuffer command = wgpuCommandEncoderFinish(encoder, NULL);
451 wgpuQueueSubmit(window->queue, 1, &command);
452
453 // Update screen
454 wgpuSurfacePresent(window->surface);
455
456 wgpuCommandBufferRelease(command);
457 wgpuRenderPassEncoderRelease(render_pass);
458 wgpuCommandEncoderRelease(encoder);
459 wgpuTextureViewRelease(view);
460 wgpuTextureRelease(frame.texture);
461}
462
463void window_get_size(const AlbaWindow* window, float* width, float* height)
464{
465 int raw_width, raw_height;
466 float x_scale, y_scale;
467 glfwGetWindowSize(window->glfw_window, &raw_width, &raw_height);
468 glfwGetWindowContentScale(window->glfw_window, &x_scale, &y_scale);
469
470 *width = raw_width / x_scale;
471 *height = raw_height / y_scale;
472}