MXVK Vulkan Framework 0.33.1
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 (textQuads.empty()) {
530 return;
531 }
532 if (device == VK_NULL_HANDLE) {
533 textQuads.clear();
534 return;
535 }
536
537 // Text quads own vertex/index buffers referenced by submitted command buffers.
538 // Wait for graphics work to finish before destroying these resources.
539 const VkResult queue_idle_result = vkQueueWaitIdle(graphicsQueue);
540 if (queue_idle_result == VK_ERROR_DEVICE_LOST) {
541 std::cerr << "mxvk: Device lost while clearing queue; skipping text resource reset\n";
542 textQuads.clear();
543 return;
544 }
545 if (queue_idle_result != VK_SUCCESS) {
546 throw mxvk::Exception(std::format(
547 "Fatal : VkResult is \"{}\" in {} at line {}",
548 static_cast<int>(queue_idle_result),
549 __FILE__,
550 __LINE__));
551 }
552
553 textQuads.clear();
554 if (descriptorPool != VK_NULL_HANDLE) {
555 VK_CHECK_RESULT(vkResetDescriptorPool(device, descriptorPool, 0));
556 }
557 pruneCache();
558 }
559
560 void VK_Text::createBuffer(VkDeviceSize size, VkBufferUsageFlags usage,
561 VkMemoryPropertyFlags properties, VkBuffer &buffer,
562 VkDeviceMemory &bufferMemory) {
563 VkBuffer newBuffer = VK_NULL_HANDLE;
564 VkDeviceMemory newMemory = VK_NULL_HANDLE;
565
566 VkBufferCreateInfo bufferInfo{};
567 bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
568 bufferInfo.size = size;
569 bufferInfo.usage = usage;
570 bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
571
572 try {
573 VK_CHECK_RESULT(vkCreateBuffer(device, &bufferInfo, nullptr, &newBuffer));
574
575 VkMemoryRequirements memRequirements;
576 vkGetBufferMemoryRequirements(device, newBuffer, &memRequirements);
577
578 VkMemoryAllocateInfo allocInfo{};
579 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
580 allocInfo.allocationSize = memRequirements.size;
581 allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties);
582 VK_CHECK_RESULT(vkAllocateMemory(device, &allocInfo, nullptr, &newMemory));
583 VK_CHECK_RESULT(vkBindBufferMemory(device, newBuffer, newMemory, 0));
584 } catch (...) {
585 if (newBuffer != VK_NULL_HANDLE) {
586 vkDestroyBuffer(device, newBuffer, nullptr);
587 }
588 if (newMemory != VK_NULL_HANDLE) {
589 vkFreeMemory(device, newMemory, nullptr);
590 }
591 throw;
592 }
593
594 if (buffer != VK_NULL_HANDLE) {
595 vkDestroyBuffer(device, buffer, nullptr);
596 }
597 if (bufferMemory != VK_NULL_HANDLE) {
598 vkFreeMemory(device, bufferMemory, nullptr);
599 }
600 buffer = newBuffer;
601 bufferMemory = newMemory;
602 }
603
604 bool VK_Text::getTextDimensions(const std::string &text, int &width, int &height) {
605 return getTextDimensions(text, width, height, font);
606 }
607
608 bool VK_Text::getTextDimensions(const std::string &text, int &width, int &height, TTF_Font *textFont) {
609 if (text.empty() || !textFont) {
610 width = 0;
611 height = 0;
612 return false;
613 }
614
615 // SDL3_ttf: TTF_GetStringSize replaces TTF_SizeText; length=0 for null-terminated
616 return TTF_GetStringSize(textFont, text.c_str(), 0, &width, &height);
617 }
618
619 bool VK_Text::getTextDimensions(const std::string &text, int &width, int &height, const Font &textFont) {
620 return getTextDimensions(text, width, height, textFont.get());
621 }
622
623 uint32_t VK_Text::findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) {
624 VkPhysicalDeviceMemoryProperties memProperties;
625 vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);
626
627 for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
628 if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) {
629 return i;
630 }
631 }
632
633 throw mxvk::Exception("Failed to find suitable memory type!");
634 }
635
636 bool VK_Text::transitionImageLayout(VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout) {
637 VkCommandBuffer commandBuffer = beginSingleTimeCommands();
638
639 VkImageMemoryBarrier barrier{};
640 barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
641 barrier.oldLayout = oldLayout;
642 barrier.newLayout = newLayout;
643 barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
644 barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
645 barrier.image = image;
646 barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
647 barrier.subresourceRange.baseMipLevel = 0;
648 barrier.subresourceRange.levelCount = 1;
649 barrier.subresourceRange.baseArrayLayer = 0;
650 barrier.subresourceRange.layerCount = 1;
651
652 VkPipelineStageFlags sourceStage;
653 VkPipelineStageFlags destinationStage;
654
655 if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
656 barrier.srcAccessMask = 0;
657 barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
658 sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
659 destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
660 } else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
661 barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
662 barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
663 sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
664 destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
665 } else {
666 throw std::invalid_argument("unsupported layout transition!");
667 }
668
669 vkCmdPipelineBarrier(commandBuffer, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier);
670 return endSingleTimeCommands(commandBuffer);
671 }
672
673 bool VK_Text::copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width, uint32_t height) {
674 VkCommandBuffer commandBuffer = beginSingleTimeCommands();
675
676 VkBufferImageCopy region{};
677 region.bufferOffset = 0;
678 region.bufferRowLength = 0;
679 region.bufferImageHeight = 0;
680 region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
681 region.imageSubresource.mipLevel = 0;
682 region.imageSubresource.baseArrayLayer = 0;
683 region.imageSubresource.layerCount = 1;
684 region.imageOffset = {0, 0, 0};
685 region.imageExtent = {width, height, 1};
686
687 vkCmdCopyBufferToImage(commandBuffer, buffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
688
689 return endSingleTimeCommands(commandBuffer);
690 }
691
692 VkCommandBuffer VK_Text::beginSingleTimeCommands() {
693 VkCommandBufferAllocateInfo allocInfo{};
694 allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
695 allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
696 allocInfo.commandPool = commandPool;
697 allocInfo.commandBufferCount = 1;
698
699 VkCommandBuffer commandBuffer;
700 VK_CHECK_RESULT(vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer));
701
702 VkCommandBufferBeginInfo beginInfo{};
703 beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
704 beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
705
706 VK_CHECK_RESULT(vkBeginCommandBuffer(commandBuffer, &beginInfo));
707
708 return commandBuffer;
709 }
710
711 bool VK_Text::endSingleTimeCommands(VkCommandBuffer commandBuffer) {
712 VK_CHECK_RESULT(vkEndCommandBuffer(commandBuffer));
713
714 VkSubmitInfo submitInfo{};
715 submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
716 submitInfo.commandBufferCount = 1;
717 submitInfo.pCommandBuffers = &commandBuffer;
718
719 const VkResult submitResult = vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);
720 if (submitResult == VK_ERROR_DEVICE_LOST) {
721 vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
722 std::cerr << "mxvk: Device lost during text command submit; skipping upload\n";
723 return false;
724 }
725 VK_CHECK_RESULT(submitResult);
726
727 const VkResult waitResult = vkQueueWaitIdle(graphicsQueue);
728 if (waitResult == VK_ERROR_DEVICE_LOST) {
729 vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
730 std::cerr << "mxvk: Device lost while waiting text queue idle; skipping upload\n";
731 return false;
732 }
733 VK_CHECK_RESULT(waitResult);
734
735 vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
736 return true;
737 }
738
739 void VK_Text::createImage(uint32_t width, uint32_t height, VkFormat format, VkImageTiling tiling,
740 VkImageUsageFlags usage, VkMemoryPropertyFlags properties,
741 VkImage &image, VkDeviceMemory &imageMemory) {
742 VkImage newImage = VK_NULL_HANDLE;
743 VkDeviceMemory newMemory = VK_NULL_HANDLE;
744
745 VkImageCreateInfo imageInfo{};
746 imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
747 imageInfo.imageType = VK_IMAGE_TYPE_2D;
748 imageInfo.extent.width = width;
749 imageInfo.extent.height = height;
750 imageInfo.extent.depth = 1;
751 imageInfo.mipLevels = 1;
752 imageInfo.arrayLayers = 1;
753 imageInfo.format = format;
754 imageInfo.tiling = tiling;
755 imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
756 imageInfo.usage = usage;
757 imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
758 imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
759
760 try {
761 VK_CHECK_RESULT(vkCreateImage(device, &imageInfo, nullptr, &newImage));
762
763 VkMemoryRequirements memRequirements;
764 vkGetImageMemoryRequirements(device, newImage, &memRequirements);
765
766 VkMemoryAllocateInfo allocInfo{};
767 allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
768 allocInfo.allocationSize = memRequirements.size;
769 allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties);
770 VK_CHECK_RESULT(vkAllocateMemory(device, &allocInfo, nullptr, &newMemory));
771 VK_CHECK_RESULT(vkBindImageMemory(device, newImage, newMemory, 0));
772 } catch (...) {
773 if (newImage != VK_NULL_HANDLE) {
774 vkDestroyImage(device, newImage, nullptr);
775 }
776 if (newMemory != VK_NULL_HANDLE) {
777 vkFreeMemory(device, newMemory, nullptr);
778 }
779 throw;
780 }
781
782 if (image != VK_NULL_HANDLE) {
783 vkDestroyImage(device, image, nullptr);
784 }
785 if (imageMemory != VK_NULL_HANDLE) {
786 vkFreeMemory(device, imageMemory, nullptr);
787 }
788 image = newImage;
789 imageMemory = newMemory;
790 }
791
792} // 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:31
GPU texture resources cached for a rendered text string.
VkDeviceMemory imageMemory
VkImageView imageView