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