MXVK Vulkan Framework 0.35.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
mxvk_text.cpp
Go to the documentation of this file.
1/**
2 * @file mxvk_text.cpp
3 * @brief Implementation of mxvk::VK_Text Vulkan SDL_ttf text renderer.
4 */
5#include "mxvk/mxvk_text.hpp"
6#include <algorithm>
7#include <iostream>
8
9namespace mxvk {
10 Font::Font(const std::string &fontPath, int fontSize) { reset(fontPath, fontSize); }
11
13
14 Font::Font(Font &&other) noexcept : font(std::exchange(other.font, nullptr)), font_path(std::move(other.font_path)), font_size(std::exchange(other.font_size, 0)), ownsTtfInit(std::exchange(other.ownsTtfInit, false)) {}
15
16 Font &Font::operator=(Font &&other) noexcept {
17 if (this != &other) {
18 reset();
19 font = std::exchange(other.font, nullptr);
20 font_path = std::move(other.font_path);
21 font_size = std::exchange(other.font_size, 0);
22 ownsTtfInit = std::exchange(other.ownsTtfInit, false);
23 }
24 return *this;
25 }
26
27 void Font::reset() {
28 if (font != nullptr) {
29 TTF_CloseFont(font);
30 font = nullptr;
31 }
32 font_path.clear();
33 font_size = 0;
34 if (ownsTtfInit) {
35 TTF_Quit();
36 ownsTtfInit = false;
37 }
38 }
39
40 void Font::reset(const std::string &fontPath, int fontSize) {
41 if (fontPath.empty() || fontSize <= 0) {
42 throw mxvk::Exception("Font requires a non-empty path and positive font size");
43 }
44
45 reset();
46
47 if (!TTF_Init()) {
48 throw mxvk::Exception("Failed to initialize SDL_ttf: " + std::string(SDL_GetError()));
49 }
50
51 TTF_Font *newFont = TTF_OpenFont(fontPath.c_str(), fontSize);
52 if (newFont == nullptr) {
53 TTF_Quit();
54 throw mxvk::Exception("Failed to load font: " + std::string(SDL_GetError()));
55 }
56
57 font = newFont;
58 font_path = fontPath;
59 font_size = fontSize;
60 ownsTtfInit = true;
61 }
62
63 VK_Text::VK_Text(VkDevice dev, VkPhysicalDevice physDev, VkQueue gQueue, VkCommandPool cmdPool, const std::string &fontPath, int fontSize) : device(dev), physicalDevice(physDev), graphicsQueue(gQueue), commandPool(cmdPool) {
64
65 if (!TTF_Init()) {
66 throw mxvk::Exception("Failed to initialize SDL_ttf: " + std::string(SDL_GetError()));
67 }
68
69 initFont(fontPath, fontSize);
70 }
71
73 vkDeviceWaitIdle(device);
74 textQuads.clear();
75 clearCache();
76
77 if (descriptorPool != VK_NULL_HANDLE) {
78 std::cout << "vk: destroying text descriptor pool\n";
79 vkDestroyDescriptorPool(device, descriptorPool, nullptr);
80 }
81
82 if (fontSampler != VK_NULL_HANDLE) {
83 std::cout << "vk: destroying text font sampler\n";
84 vkDestroySampler(device, fontSampler, nullptr);
85 }
86
87 if (font) {
88 std::cout << "mxvk: closing text font\n";
89 TTF_CloseFont(font);
90 }
91 TTF_Quit();
92 }
93
95 for (auto &[key, cached] : textureCache) {
96 destroyCachedTexture(cached);
97 }
98 textureCache.clear();
99 }
100
101 void VK_Text::destroyCachedTexture(CachedTexture &cached) {
102 if (cached.imageView != VK_NULL_HANDLE) {
103 vkDestroyImageView(device, cached.imageView, nullptr);
104 cached.imageView = VK_NULL_HANDLE;
105 }
106 if (cached.image != VK_NULL_HANDLE) {
107 vkDestroyImage(device, cached.image, nullptr);
108 cached.image = VK_NULL_HANDLE;
109 }
110 if (cached.imageMemory != VK_NULL_HANDLE) {
111 vkFreeMemory(device, cached.imageMemory, nullptr);
112 cached.imageMemory = VK_NULL_HANDLE;
113 }
114 }
115
116 void VK_Text::pruneCache() {
117 if (textureCache.size() <= MAX_CACHED_TEXTURES) {
118 return;
119 }
120
121 std::vector<std::pair<uint64_t, CacheKey>> entries;
122 entries.reserve(textureCache.size());
123 for (const auto &[key, cached] : textureCache) {
124 entries.emplace_back(cached.lastUsedSerial, key);
125 }
126
127 const size_t removeCount = textureCache.size() - MAX_CACHED_TEXTURES;
128 std::nth_element(entries.begin(), entries.begin() + static_cast<std::ptrdiff_t>(removeCount), entries.end(), [](const auto &lhs, const auto &rhs) { return lhs.first < rhs.first; });
129
130 for (size_t i = 0; i < removeCount; ++i) {
131 auto it = textureCache.find(entries[i].second);
132 if (it == textureCache.end()) {
133 continue;
134 }
135 destroyCachedTexture(it->second);
136 textureCache.erase(it);
137 }
138 }
139
140 void VK_Text::createDescriptorPool() { createDescriptorPool(maxPoolSets); }
141
142 void VK_Text::createDescriptorPool(uint32_t maxSets) {
143 VkDescriptorPoolSize poolSize{};
144 poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
145 poolSize.descriptorCount = maxSets;
146
147 VkDescriptorPoolCreateInfo poolInfo{};
148 poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
149 poolInfo.poolSizeCount = 1;
150 poolInfo.pPoolSizes = &poolSize;
151 poolInfo.maxSets = maxSets;
152 poolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
153
154 VK_CHECK_RESULT(vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool));
155 maxPoolSets = maxSets;
156 }
157
158 void VK_Text::growDescriptorPool() {
159 vkDeviceWaitIdle(device);
160 textQuads.clear();
161 if (descriptorPool != VK_NULL_HANDLE) {
162 vkDestroyDescriptorPool(device, descriptorPool, nullptr);
163 descriptorPool = VK_NULL_HANDLE;
164 }
165 maxPoolSets *= 2;
166 createDescriptorPool(maxPoolSets);
167 }
168
169 VkDescriptorSet VK_Text::createDescriptorSet(VkImageView imageView) {
170 if (descriptorSetLayout == VK_NULL_HANDLE) {
171 throw mxvk::Exception("VKText::createDescriptorSet called before setDescriptorSetLayout");
172 }
173
174 if (descriptorPool == VK_NULL_HANDLE) {
175 createDescriptorPool();
176 }
177
178 VkDescriptorSetAllocateInfo allocInfo{};
179 allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
180 allocInfo.descriptorPool = descriptorPool;
181 allocInfo.descriptorSetCount = 1;
182 allocInfo.pSetLayouts = &descriptorSetLayout;
183
184 VkDescriptorSet descriptorSet;
185 VkResult res = vkAllocateDescriptorSets(device, &allocInfo, &descriptorSet);
186 if (res == VK_ERROR_OUT_OF_POOL_MEMORY || res == VK_ERROR_FRAGMENTED_POOL) {
187 growDescriptorPool();
188 allocInfo.descriptorPool = descriptorPool;
189 VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &allocInfo, &descriptorSet));
190 } else if (res != VK_SUCCESS) {
191 throw mxvk::Exception(std::format("Fatal : VkResult is \"{}\" in {} at line {}", static_cast<int>(res), __FILE__, __LINE__));
192 }
193
194 VkDescriptorImageInfo imageInfo{};
195 imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
196 imageInfo.imageView = imageView;
197 imageInfo.sampler = fontSampler;
198
199 VkWriteDescriptorSet descriptorWrite{};
200 descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
201 descriptorWrite.dstSet = descriptorSet;
202 descriptorWrite.dstBinding = 0;
203 descriptorWrite.dstArrayElement = 0;
204 descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
205 descriptorWrite.descriptorCount = 1;
206 descriptorWrite.pImageInfo = &imageInfo;
207
208 vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);
209
210 return descriptorSet;
211 }
212
213 void VK_Text::initSampler() {
214 VkSamplerCreateInfo samplerInfo{};
215 samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
216 samplerInfo.magFilter = VK_FILTER_LINEAR;
217 samplerInfo.minFilter = VK_FILTER_LINEAR;
218 samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
219 samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
220 samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
221 samplerInfo.anisotropyEnable = VK_FALSE;
222 samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
223 samplerInfo.unnormalizedCoordinates = VK_FALSE;
224 samplerInfo.compareEnable = VK_FALSE;
225 samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;
226 samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
227
228 VK_CHECK_RESULT(vkCreateSampler(device, &samplerInfo, nullptr, &fontSampler));
229 }
230
231 void VK_Text::initFont(const std::string &fontPath, int fontSize) {
232 font = TTF_OpenFont(fontPath.c_str(), fontSize);
233 if (!font) {
234 throw mxvk::Exception("Failed to load font: " + std::string(SDL_GetError()));
235 }
236 if (fontSampler == VK_NULL_HANDLE) {
237 initSampler();
238 }
239 std::cout << std::format("mxvk: Font loaded: {} @ {}pt\n", fontPath, fontSize);
240 }
241
242 void VK_Text::setFont(const std::string &fontPath, int fontSize) {
243 vkDeviceWaitIdle(device);
244 clearQueue();
245 clearCache();
246 if (font) {
247 TTF_CloseFont(font);
248 font = nullptr;
249 }
250 if (fontSampler != VK_NULL_HANDLE) {
251 vkDestroySampler(device, fontSampler, nullptr);
252 fontSampler = VK_NULL_HANDLE;
253 }
254 initFont(fontPath, fontSize);
255 }
256
257 SDL_Surface *VK_Text::convertToRGBA(SDL_Surface *surface) {
258 // SDL3: SDL_ConvertSurface handles RGBA conversion directly
259 SDL_Surface *converted = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32);
260 return converted;
261 }
262
263 VkImageView VK_Text::createImageView(VkImage image, VkFormat format) {
264 VkImageViewCreateInfo viewInfo{};
265 viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
266 viewInfo.image = image;
267 viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
268 viewInfo.format = format;
269 viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
270 viewInfo.subresourceRange.baseMipLevel = 0;
271 viewInfo.subresourceRange.levelCount = 1;
272 viewInfo.subresourceRange.baseArrayLayer = 0;
273 viewInfo.subresourceRange.layerCount = 1;
274
275 VkImageView imageView;
276 VK_CHECK_RESULT(vkCreateImageView(device, &viewInfo, nullptr, &imageView));
277 return imageView;
278 }
279
280 void VK_Text::printTextG_Solid(const std::string &text, int x, int y, const SDL_Color &col) { printTextG_SolidWithFont(text, x, y, col, font); }
281
282 void VK_Text::printTextG_Solid(const std::string &text, int x, int y, const SDL_Color &col, TTF_Font *textFont) { printTextG_SolidWithFont(text, x, y, col, textFont); }
283
284 void VK_Text::printTextG_Solid(const std::string &text, int x, int y, const SDL_Color &col, const Font &textFont) { printTextG_SolidWithFont(text, x, y, col, textFont.get()); }
285
286 void VK_Text::printTextG_SolidWithFont(const std::string &text, int x, int y, const SDL_Color &col, TTF_Font *textFont) {
287 if (text.empty() || !textFont)
288 return;
289
290 if (fontSampler == VK_NULL_HANDLE) {
291 initSampler();
292 }
293
294 TextQuad quad;
295 quad.device = device;
296 quad.text = text;
297 quad.x = x;
298 quad.y = y;
299 quad.color = col;
300 quad.alpha = static_cast<float>(col.a) / 255.0f;
301 quad.ownsTexture = false; // cache owns all textures
302
303 SDL_Color textureColor = col;
304 textureColor.a = 255;
305 CacheKey key{text, textFont, textureColor.r, textureColor.g, textureColor.b, textureColor.a};
306 auto it = textureCache.find(key);
307
308 if (it != textureCache.end()) {
309 // Cache hit -- reuse the existing GPU texture
310 auto &cached = it->second;
311 cached.lastUsedSerial = ++cacheUseSerial;
312 quad.textImage = cached.image;
313 quad.textImageMemory = cached.imageMemory;
314 quad.textImageView = cached.imageView;
315 quad.width = cached.width;
316 quad.height = cached.height;
317 } else {
318 // Cache miss -- render with SDL_ttf and upload to GPU
319 // SDL3_ttf: TTF_RenderText_Blended requires an explicit length (0 = null-terminated)
320 SDL_Surface *textSurface = TTF_RenderText_Blended(textFont, text.c_str(), 0, textureColor);
321 if (!textSurface) {
322 return;
323 }
324
325 SDL_Surface *rgbaSurface = convertToRGBA(textSurface);
326 SDL_DestroySurface(textSurface);
327 if (!rgbaSurface) {
328 return;
329 }
330
331 quad.width = rgbaSurface->w;
332 quad.height = rgbaSurface->h;
333
334 VkImage uploadedImage = VK_NULL_HANDLE;
335 VkDeviceMemory uploadedImageMemory = VK_NULL_HANDLE;
336 VkImageView uploadedImageView = VK_NULL_HANDLE;
337 VkBuffer stagingBuffer = VK_NULL_HANDLE;
338 VkDeviceMemory stagingMemory = VK_NULL_HANDLE;
339 const VkDeviceSize imageSize = static_cast<VkDeviceSize>(rgbaSurface->w) * static_cast<VkDeviceSize>(rgbaSurface->h) * 4u;
340
341 auto cleanupUploadResources = [&]() {
342 if (stagingBuffer != VK_NULL_HANDLE) {
343 vkDestroyBuffer(device, stagingBuffer, nullptr);
344 stagingBuffer = VK_NULL_HANDLE;
345 }
346 if (stagingMemory != VK_NULL_HANDLE) {
347 vkFreeMemory(device, stagingMemory, nullptr);
348 stagingMemory = VK_NULL_HANDLE;
349 }
350 if (uploadedImageView != VK_NULL_HANDLE) {
351 vkDestroyImageView(device, uploadedImageView, nullptr);
352 uploadedImageView = VK_NULL_HANDLE;
353 }
354 if (uploadedImage != VK_NULL_HANDLE) {
355 vkDestroyImage(device, uploadedImage, nullptr);
356 uploadedImage = VK_NULL_HANDLE;
357 }
358 if (uploadedImageMemory != VK_NULL_HANDLE) {
359 vkFreeMemory(device, uploadedImageMemory, nullptr);
360 uploadedImageMemory = VK_NULL_HANDLE;
361 }
362 };
363
364 bool uploadSucceeded = false;
365 try {
366 createImage(rgbaSurface->w, rgbaSurface->h, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, uploadedImage, uploadedImageMemory);
367
368 createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingMemory);
369
370 void *data = nullptr;
371 VK_CHECK_RESULT(vkMapMemory(device, stagingMemory, 0, imageSize, 0, &data));
372 memcpy(data, rgbaSurface->pixels, static_cast<size_t>(imageSize));
373 vkUnmapMemory(device, stagingMemory);
374
375 if (!transitionImageLayout(uploadedImage, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)) {
376 SDL_DestroySurface(rgbaSurface);
377 cleanupUploadResources();
378 return;
379 }
380 if (!copyBufferToImage(stagingBuffer, uploadedImage, static_cast<uint32_t>(rgbaSurface->w), static_cast<uint32_t>(rgbaSurface->h))) {
381 SDL_DestroySurface(rgbaSurface);
382 cleanupUploadResources();
383 return;
384 }
385 if (!transitionImageLayout(uploadedImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)) {
386 SDL_DestroySurface(rgbaSurface);
387 cleanupUploadResources();
388 return;
389 }
390
391 uploadedImageView = createImageView(uploadedImage, VK_FORMAT_R8G8B8A8_UNORM);
392 uploadSucceeded = true;
393 } catch (const mxvk::Exception &ex) {
394 SDL_DestroySurface(rgbaSurface);
395 cleanupUploadResources();
396 static bool textTextureWarningLogged = false;
397 if (!textTextureWarningLogged) {
398 std::cerr << "mxvk: dropping text texture after Vulkan allocation failure: " << ex.text() << "\n";
399 textTextureWarningLogged = true;
400 }
401 return;
402 }
403
404 SDL_DestroySurface(rgbaSurface);
405
406 if (!uploadSucceeded) {
407 cleanupUploadResources();
408 return;
409 }
410
411 if (stagingBuffer != VK_NULL_HANDLE) {
412 vkDestroyBuffer(device, stagingBuffer, nullptr);
413 stagingBuffer = VK_NULL_HANDLE;
414 }
415 if (stagingMemory != VK_NULL_HANDLE) {
416 vkFreeMemory(device, stagingMemory, nullptr);
417 stagingMemory = VK_NULL_HANDLE;
418 }
419
420 quad.textImage = uploadedImage;
421 quad.textImageMemory = uploadedImageMemory;
422 quad.textImageView = uploadedImageView;
423
424 // Store in cache
425 textureCache[key] = {quad.textImage, quad.textImageMemory, quad.textImageView, quad.width, quad.height, ++cacheUseSerial};
426 }
427
428 // Build the screen-space quad (position-dependent, not cached)
429 float x0 = (float)x;
430 float y0 = (float)y;
431 float x1 = x0 + quad.width;
432 float y1 = y0 + quad.height;
433
434 TextVertex v0 = {{x0, y0}, {0.0f, 0.0f}};
435 TextVertex v1 = {{x1, y0}, {1.0f, 0.0f}};
436 TextVertex v2 = {{x1, y1}, {1.0f, 1.0f}};
437 TextVertex v3 = {{x0, y1}, {0.0f, 1.0f}};
438
439 quad.vertices = {v0, v1, v2, v3};
440 quad.indices = {0, 1, 2, 0, 2, 3};
441 quad.indexCount = 6;
442
443 try {
444 void *data;
445 VkDeviceSize vertexSize = quad.vertices.size() * sizeof(TextVertex);
446 createBuffer(vertexSize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, quad.vertexBuffer, quad.vertexBufferMemory);
447
448 VK_CHECK_RESULT(vkMapMemory(device, quad.vertexBufferMemory, 0, vertexSize, 0, &data));
449 memcpy(data, quad.vertices.data(), vertexSize);
450 vkUnmapMemory(device, quad.vertexBufferMemory);
451
452 VkDeviceSize indexSize = quad.indices.size() * sizeof(uint16_t);
453 createBuffer(indexSize, VK_BUFFER_USAGE_INDEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, quad.indexBuffer, quad.indexBufferMemory);
454
455 VK_CHECK_RESULT(vkMapMemory(device, quad.indexBufferMemory, 0, indexSize, 0, &data));
456 memcpy(data, quad.indices.data(), indexSize);
457 vkUnmapMemory(device, quad.indexBufferMemory);
458
459 quad.descriptorSet = createDescriptorSet(quad.textImageView);
460 } catch (const mxvk::Exception &ex) {
461 static bool textAllocationWarningLogged = false;
462 if (!textAllocationWarningLogged) {
463 std::cerr << "mxvk: dropping text draw after Vulkan allocation failure: " << ex.text() << "\n";
464 textAllocationWarningLogged = true;
465 }
466 return;
467 }
468
469 textQuads.emplace_back(std::move(quad));
470 }
471
472 void VK_Text::renderText(VkCommandBuffer cmdBuffer, VkPipelineLayout pipelineLayout, uint32_t screenWidth, uint32_t screenHeight) {
473
474 struct TextPushConstants {
475 float screenWidth;
476 float screenHeight;
477 float alpha;
478 float padding;
479 } pc{static_cast<float>(screenWidth), static_cast<float>(screenHeight), 1.0f, 0.0f};
480
481 for (auto &quad : textQuads) {
482 pc.alpha = quad.alpha;
483 vkCmdPushConstants(cmdBuffer, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(TextPushConstants), &pc);
484 VkBuffer vertexBuffers[] = {quad.vertexBuffer};
485 VkDeviceSize offsets[] = {0};
486 vkCmdBindVertexBuffers(cmdBuffer, 0, 1, vertexBuffers, offsets);
487 vkCmdBindIndexBuffer(cmdBuffer, quad.indexBuffer, 0, VK_INDEX_TYPE_UINT16);
488 vkCmdBindDescriptorSets(cmdBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &quad.descriptorSet, 0, nullptr);
489
490 vkCmdDrawIndexed(cmdBuffer, quad.indexCount, 1, 0, 0, 0);
491 }
492 }
493
495 if (textQuads.empty()) {
496 return;
497 }
498 if (device == VK_NULL_HANDLE) {
499 textQuads.clear();
500 return;
501 }
502
503 // Text quads own vertex/index buffers referenced by submitted command buffers.
504 // Wait for graphics work to finish before destroying these resources.
505 const VkResult queue_idle_result = vkQueueWaitIdle(graphicsQueue);
506 if (queue_idle_result == VK_ERROR_DEVICE_LOST) {
507 std::cerr << "mxvk: Device lost while clearing queue; skipping text resource reset\n";
508 textQuads.clear();
509 return;
510 }
511 if (queue_idle_result != VK_SUCCESS) {
512 throw mxvk::Exception(std::format("Fatal : VkResult is \"{}\" in {} at line {}", static_cast<int>(queue_idle_result), __FILE__, __LINE__));
513 }
514
515 textQuads.clear();
516 if (descriptorPool != VK_NULL_HANDLE) {
517 VK_CHECK_RESULT(vkResetDescriptorPool(device, descriptorPool, 0));
518 }
519 pruneCache();
520 }
521
522 void VK_Text::createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer &buffer, VkDeviceMemory &bufferMemory) {
523 VkBuffer newBuffer = VK_NULL_HANDLE;
524 VkDeviceMemory newMemory = VK_NULL_HANDLE;
525
526 VkBufferCreateInfo bufferInfo{};
527 bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
528 bufferInfo.size = size;
529 bufferInfo.usage = usage;
530 bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
531
532 try {
533 VK_CHECK_RESULT(vkCreateBuffer(device, &bufferInfo, nullptr, &newBuffer));
534
535 VkMemoryRequirements memRequirements;
536 vkGetBufferMemoryRequirements(device, newBuffer, &memRequirements);
537
538 VkMemoryAllocateInfo allocInfo{};
539 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
540 allocInfo.allocationSize = memRequirements.size;
541 allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties);
542 VK_CHECK_RESULT(vkAllocateMemory(device, &allocInfo, nullptr, &newMemory));
543 VK_CHECK_RESULT(vkBindBufferMemory(device, newBuffer, newMemory, 0));
544 } catch (...) {
545 if (newBuffer != VK_NULL_HANDLE) {
546 vkDestroyBuffer(device, newBuffer, nullptr);
547 }
548 if (newMemory != VK_NULL_HANDLE) {
549 vkFreeMemory(device, newMemory, nullptr);
550 }
551 throw;
552 }
553
554 if (buffer != VK_NULL_HANDLE) {
555 vkDestroyBuffer(device, buffer, nullptr);
556 }
557 if (bufferMemory != VK_NULL_HANDLE) {
558 vkFreeMemory(device, bufferMemory, nullptr);
559 }
560 buffer = newBuffer;
561 bufferMemory = newMemory;
562 }
563
564 bool VK_Text::getTextDimensions(const std::string &text, int &width, int &height) { return getTextDimensions(text, width, height, font); }
565
566 bool VK_Text::getTextDimensions(const std::string &text, int &width, int &height, TTF_Font *textFont) {
567 if (text.empty() || !textFont) {
568 width = 0;
569 height = 0;
570 return false;
571 }
572
573 // SDL3_ttf: TTF_GetStringSize replaces TTF_SizeText; length=0 for null-terminated
574 return TTF_GetStringSize(textFont, text.c_str(), 0, &width, &height);
575 }
576
577 bool VK_Text::getTextDimensions(const std::string &text, int &width, int &height, const Font &textFont) { return getTextDimensions(text, width, height, textFont.get()); }
578
579 uint32_t VK_Text::findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) {
580 VkPhysicalDeviceMemoryProperties memProperties;
581 vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);
582
583 for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
584 if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
585 return i;
586 }
587 }
588
589 throw mxvk::Exception("Failed to find suitable memory type!");
590 }
591
592 bool VK_Text::transitionImageLayout(VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout) {
593 VkCommandBuffer commandBuffer = beginSingleTimeCommands();
594
595 VkImageMemoryBarrier barrier{};
596 barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
597 barrier.oldLayout = oldLayout;
598 barrier.newLayout = newLayout;
599 barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
600 barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
601 barrier.image = image;
602 barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
603 barrier.subresourceRange.baseMipLevel = 0;
604 barrier.subresourceRange.levelCount = 1;
605 barrier.subresourceRange.baseArrayLayer = 0;
606 barrier.subresourceRange.layerCount = 1;
607
608 VkPipelineStageFlags sourceStage;
609 VkPipelineStageFlags destinationStage;
610
611 if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
612 barrier.srcAccessMask = 0;
613 barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
614 sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
615 destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
616 } else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
617 barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
618 barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
619 sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
620 destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
621 } else {
622 throw std::invalid_argument("unsupported layout transition!");
623 }
624
625 vkCmdPipelineBarrier(commandBuffer, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier);
626 return endSingleTimeCommands(commandBuffer);
627 }
628
629 bool VK_Text::copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width, uint32_t height) {
630 VkCommandBuffer commandBuffer = beginSingleTimeCommands();
631
632 VkBufferImageCopy region{};
633 region.bufferOffset = 0;
634 region.bufferRowLength = 0;
635 region.bufferImageHeight = 0;
636 region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
637 region.imageSubresource.mipLevel = 0;
638 region.imageSubresource.baseArrayLayer = 0;
639 region.imageSubresource.layerCount = 1;
640 region.imageOffset = {0, 0, 0};
641 region.imageExtent = {width, height, 1};
642
643 vkCmdCopyBufferToImage(commandBuffer, buffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
644
645 return endSingleTimeCommands(commandBuffer);
646 }
647
648 VkCommandBuffer VK_Text::beginSingleTimeCommands() {
649 VkCommandBufferAllocateInfo allocInfo{};
650 allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
651 allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
652 allocInfo.commandPool = commandPool;
653 allocInfo.commandBufferCount = 1;
654
655 VkCommandBuffer commandBuffer;
656 VK_CHECK_RESULT(vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer));
657
658 VkCommandBufferBeginInfo beginInfo{};
659 beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
660 beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
661
662 VK_CHECK_RESULT(vkBeginCommandBuffer(commandBuffer, &beginInfo));
663
664 return commandBuffer;
665 }
666
667 bool VK_Text::endSingleTimeCommands(VkCommandBuffer commandBuffer) {
668 VK_CHECK_RESULT(vkEndCommandBuffer(commandBuffer));
669
670 VkSubmitInfo submitInfo{};
671 submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
672 submitInfo.commandBufferCount = 1;
673 submitInfo.pCommandBuffers = &commandBuffer;
674
675 const VkResult submitResult = vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);
676 if (submitResult == VK_ERROR_DEVICE_LOST) {
677 vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
678 std::cerr << "mxvk: Device lost during text command submit; skipping upload\n";
679 return false;
680 }
681 VK_CHECK_RESULT(submitResult);
682
683 const VkResult waitResult = vkQueueWaitIdle(graphicsQueue);
684 if (waitResult == VK_ERROR_DEVICE_LOST) {
685 vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
686 std::cerr << "mxvk: Device lost while waiting text queue idle; skipping upload\n";
687 return false;
688 }
689 VK_CHECK_RESULT(waitResult);
690
691 vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
692 return true;
693 }
694
695 void VK_Text::createImage(uint32_t width, uint32_t height, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage &image, VkDeviceMemory &imageMemory) {
696 VkImage newImage = VK_NULL_HANDLE;
697 VkDeviceMemory newMemory = VK_NULL_HANDLE;
698
699 VkImageCreateInfo imageInfo{};
700 imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
701 imageInfo.imageType = VK_IMAGE_TYPE_2D;
702 imageInfo.extent.width = width;
703 imageInfo.extent.height = height;
704 imageInfo.extent.depth = 1;
705 imageInfo.mipLevels = 1;
706 imageInfo.arrayLayers = 1;
707 imageInfo.format = format;
708 imageInfo.tiling = tiling;
709 imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
710 imageInfo.usage = usage;
711 imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
712 imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
713
714 try {
715 VK_CHECK_RESULT(vkCreateImage(device, &imageInfo, nullptr, &newImage));
716
717 VkMemoryRequirements memRequirements;
718 vkGetImageMemoryRequirements(device, newImage, &memRequirements);
719
720 VkMemoryAllocateInfo allocInfo{};
721 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
722 allocInfo.allocationSize = memRequirements.size;
723 allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties);
724 VK_CHECK_RESULT(vkAllocateMemory(device, &allocInfo, nullptr, &newMemory));
725 VK_CHECK_RESULT(vkBindImageMemory(device, newImage, newMemory, 0));
726 } catch (...) {
727 if (newImage != VK_NULL_HANDLE) {
728 vkDestroyImage(device, newImage, nullptr);
729 }
730 if (newMemory != VK_NULL_HANDLE) {
731 vkFreeMemory(device, newMemory, nullptr);
732 }
733 throw;
734 }
735
736 if (image != VK_NULL_HANDLE) {
737 vkDestroyImage(device, image, nullptr);
738 }
739 if (imageMemory != VK_NULL_HANDLE) {
740 vkFreeMemory(device, imageMemory, nullptr);
741 }
742 image = newImage;
743 imageMemory = newMemory;
744 }
745
746} // namespace mxvk
std::string text() const
Small RAII wrapper for an SDL_ttf font handle.
Definition mxvk_text.hpp:46
Font()=default
void reset()
Definition mxvk_text.cpp:27
TTF_Font * get() const noexcept
Definition mxvk_text.hpp:58
Font & operator=(const Font &)=delete
VkSampler fontSampler
Sampler used for all text textures.
void setFont(const std::string &fontPath, int fontSize)
Change the active font.
bool getTextDimensions(const std::string &text, int &width, int &height)
Measure the pixel dimensions of a string with the current font.
void clearQueue()
Discard all pending text quads without rendering them.
void clearCache()
Destroy all cached text textures, freeing GPU memory.
Definition mxvk_text.cpp:94
void renderText(VkCommandBuffer cmdBuffer, VkPipelineLayout pipelineLayout, uint32_t screenWidth, uint32_t screenHeight)
Record all queued text quads into a command buffer.
~VK_Text()
Destructor – destroys all Vulkan and SDL_ttf resources.
Definition mxvk_text.cpp:72
VK_Text(VkDevice device, VkPhysicalDevice physicalDevice, VkQueue graphicsQueue, VkCommandPool commandPool, const std::string &fontPath, int fontSize=24)
Construct VKText and load the font.
Definition mxvk_text.cpp:63
void printTextG_Solid(const std::string &text, int x, int y, const SDL_Color &col)
Queue a text string for solid (opaque) rendering.
#define VK_CHECK_RESULT(f)
Vulkan SDL_ttf text renderer.
Utilities for loading and saving PNG images.
Definition mxvk.hpp:31
GPU texture resources cached for a rendered text string.
VkDeviceMemory imageMemory
VkImageView imageView