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.hpp
Go to the documentation of this file.
1/**
2 * @file mxvk_text.hpp
3 * @brief Vulkan SDL_ttf text renderer.
4 *
5 * VKText rasterises text strings into SDL surfaces, uploads them as
6 * Vulkan image textures, and batches the resulting quads for submission
7 * during the render pass. Each printTextG_Solid() call adds one TextQuad
8 * to the pending queue, which is flushed by renderText().
9 */
10#pragma once
11
12#include <volk/volk.h>
13
14#include "mxvk_exception.hpp"
15#include <SDL3/SDL.h>
16#include <SDL3_ttf/SDL_ttf.h>
17#include <cstdlib>
18#include <cstring>
19#include <format>
20#include <iostream>
21#include <stdexcept>
22#include <string>
23#include <unordered_map>
24#include <utility>
25#include <vector>
26
27#ifndef VK_CHECK_RESULT
28#define VK_CHECK_RESULT(f) \
29 { \
30 VkResult res = (f); \
31 if (res != VK_SUCCESS) { \
32 throw mxvk::Exception(std::format("Fatal : VkResult is \"{}\" in {} at line {}", static_cast<int>(res), __FILE__, __LINE__)); \
33 } \
34 }
35#endif
36
37namespace mxvk {
38
39 /**
40 * @class Font
41 * @brief Small RAII wrapper for an SDL_ttf font handle.
42 *
43 * This is optional convenience for APIs that accept a font object. Existing
44 * code can continue using VK_Text::setFont() and printText() unchanged.
45 */
46 class Font {
47 public:
48 Font() = default;
49 Font(const std::string &fontPath, int fontSize);
50 ~Font();
51
52 Font(const Font &) = delete;
53 Font &operator=(const Font &) = delete;
54
55 Font(Font &&other) noexcept;
56 Font &operator=(Font &&other) noexcept;
57
58 [[nodiscard]] TTF_Font *get() const noexcept { return font; }
59 [[nodiscard]] explicit operator bool() const noexcept { return font != nullptr; }
60 [[nodiscard]] const std::string &path() const noexcept { return font_path; }
61 [[nodiscard]] int size() const noexcept { return font_size; }
62
63 void reset();
64 void reset(const std::string &fontPath, int fontSize);
65
66 private:
67 TTF_Font *font = nullptr;
68 std::string font_path{};
69 int font_size = 0;
70 bool ownsTtfInit = false;
71 };
72
73 /**
74 * @class VKText
75 * @brief Renders SDL_ttf text into Vulkan image textures.
76 *
77 * Loads a TrueType font via SDL_ttf, creates a descriptor pool for per-glyph
78 * textures, and provides a print-then-render workflow:
79 * 1. Call printTextG_Solid() for each string to display.
80 * 2. Call renderText() once during the render pass to draw all queued strings.
81 * 3. Optionally call clearQueue() to discard pending strings.
82 */
83 /// Cache key combining text content and colour.
84 struct CacheKey {
85 std::string text;
86 TTF_Font *font = nullptr;
87 uint8_t r, g, b, a;
88 bool operator==(const CacheKey &other) const = default;
89 };
90
91 /// Hash functor for CacheKey.
92 struct CacheKeyHash {
93 size_t operator()(const CacheKey &k) const {
94 size_t h = std::hash<std::string>{}(k.text);
95 h ^= std::hash<TTF_Font *>{}(k.font) + 0x9e3779b9 + (h << 6) + (h >> 2);
96 h ^= std::hash<uint8_t>{}(k.r) + 0x9e3779b9 + (h << 6) + (h >> 2);
97 h ^= std::hash<uint8_t>{}(k.g) + 0x9e3779b9 + (h << 6) + (h >> 2);
98 h ^= std::hash<uint8_t>{}(k.b) + 0x9e3779b9 + (h << 6) + (h >> 2);
99 h ^= std::hash<uint8_t>{}(k.a) + 0x9e3779b9 + (h << 6) + (h >> 2);
100 return h;
101 }
102 };
103
104 /// GPU texture resources cached for a rendered text string.
106 VkImage image = VK_NULL_HANDLE;
107 VkDeviceMemory imageMemory = VK_NULL_HANDLE;
108 VkImageView imageView = VK_NULL_HANDLE;
109 int width = 0;
110 int height = 0;
111 uint64_t lastUsedSerial = 0;
112 };
113
114 class VK_Text {
115 public:
116 /**
117 * @brief Construct VKText and load the font.
118 * @param device Logical device.
119 * @param physicalDevice Physical device.
120 * @param graphicsQueue Graphics queue.
121 * @param commandPool Command pool for staging.
122 * @param fontPath Path to the TTF font file.
123 * @param fontSize Point size.
124 */
125 VK_Text(VkDevice device, VkPhysicalDevice physicalDevice, VkQueue graphicsQueue, VkCommandPool commandPool, const std::string &fontPath, int fontSize = 24);
126
127 /** @brief Destructor -- destroys all Vulkan and SDL_ttf resources. */
128 ~VK_Text();
129
130 VK_Text(const VK_Text &) = delete;
131 VK_Text &operator=(const VK_Text &) = delete;
132 VK_Text(VK_Text &&) = delete;
133 VK_Text &operator=(VK_Text &&) = delete;
134
135 /**
136 * @brief Queue a text string for solid (opaque) rendering.
137 * @param text String to draw.
138 * @param x Destination X in pixels.
139 * @param y Destination Y in pixels.
140 * @param col Text colour.
141 */
142 void printTextG_Solid(const std::string &text, int x, int y, const SDL_Color &col);
143 void printTextG_Solid(const std::string &text, int x, int y, const SDL_Color &col, TTF_Font *textFont);
144 void printTextG_Solid(const std::string &text, int x, int y, const SDL_Color &col, const Font &textFont);
145
146 /**
147 * @brief Record all queued text quads into a command buffer.
148 * @param cmdBuffer Active Vulkan command buffer.
149 * @param pipelineLayout Pipeline layout for push constants.
150 * @param screenWidth Viewport width.
151 * @param screenHeight Viewport height.
152 */
153 void renderText(VkCommandBuffer cmdBuffer, VkPipelineLayout pipelineLayout, uint32_t screenWidth, uint32_t screenHeight);
154
155 /** @brief Discard all pending text quads without rendering them. */
156 void clearQueue();
157
158 /** @brief Assign an external descriptor-set layout. */
159 void setDescriptorSetLayout(VkDescriptorSetLayout layout) { descriptorSetLayout = layout; }
160
161 /**
162 * @brief Change the active font.
163 * @param fontPath New font file path.
164 * @param fontSize New point size.
165 */
166 void setFont(const std::string &fontPath, int fontSize);
167
168 /** @brief Destroy all cached text textures, freeing GPU memory. */
169 void clearCache();
170
171 /**
172 * @brief Measure the pixel dimensions of a string with the current font.
173 * @param text String to measure.
174 * @param width Output: pixel width.
175 * @param height Output: pixel height.
176 * @return @c true if measurement succeeded.
177 */
178 bool getTextDimensions(const std::string &text, int &width, int &height);
179 bool getTextDimensions(const std::string &text, int &width, int &height, TTF_Font *textFont);
180 bool getTextDimensions(const std::string &text, int &width, int &height, const Font &textFont);
181
182 VkSampler fontSampler = VK_NULL_HANDLE; ///< Sampler used for all text textures.
183 private:
184 struct TextVertex {
185 float pos[2];
186 float texCoord[2];
187 };
188
189 struct TextQuad {
190 std::string text;
191 int x, y;
192 int width, height;
193 SDL_Color color;
194 float alpha = 1.0f;
195 std::vector<TextVertex> vertices;
196 std::vector<uint16_t> indices;
197 VkBuffer vertexBuffer = VK_NULL_HANDLE;
198 VkDeviceMemory vertexBufferMemory = VK_NULL_HANDLE;
199 VkBuffer indexBuffer = VK_NULL_HANDLE;
200 VkDeviceMemory indexBufferMemory = VK_NULL_HANDLE;
201 VkImage textImage = VK_NULL_HANDLE;
202 VkDeviceMemory textImageMemory = VK_NULL_HANDLE;
203 VkImageView textImageView = VK_NULL_HANDLE;
204 VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
205 uint32_t indexCount = 0;
206 VkDevice device = VK_NULL_HANDLE;
207 bool ownsTexture = true; ///< false when texture is owned by the cache.
208
209 TextQuad() = default;
210 TextQuad(const TextQuad &) = delete;
211 TextQuad &operator=(const TextQuad &) = delete;
212 TextQuad(TextQuad &&other) noexcept { *this = std::move(other); }
213 TextQuad &operator=(TextQuad &&other) noexcept {
214 if (this != &other) {
215 text = std::move(other.text);
216 x = other.x;
217 y = other.y;
218 width = other.width;
219 height = other.height;
220 color = other.color;
221 alpha = other.alpha;
222 vertices = std::move(other.vertices);
223 indices = std::move(other.indices);
224 vertexBuffer = other.vertexBuffer;
225 vertexBufferMemory = other.vertexBufferMemory;
226 indexBuffer = other.indexBuffer;
227 indexBufferMemory = other.indexBufferMemory;
228 textImage = other.textImage;
229 textImageMemory = other.textImageMemory;
230 textImageView = other.textImageView;
231 descriptorSet = other.descriptorSet;
232 indexCount = other.indexCount;
233 device = other.device;
234 ownsTexture = other.ownsTexture;
235
236 other.ownsTexture = false;
237 other.vertexBuffer = VK_NULL_HANDLE;
238 other.vertexBufferMemory = VK_NULL_HANDLE;
239 other.indexBuffer = VK_NULL_HANDLE;
240 other.indexBufferMemory = VK_NULL_HANDLE;
241 other.textImage = VK_NULL_HANDLE;
242 other.textImageMemory = VK_NULL_HANDLE;
243 other.textImageView = VK_NULL_HANDLE;
244 other.descriptorSet = VK_NULL_HANDLE;
245 other.indexCount = 0;
246 other.device = VK_NULL_HANDLE;
247 }
248 return *this;
249 }
250
251 ~TextQuad() {
252 if (device != VK_NULL_HANDLE) {
253 if (ownsTexture) {
254 if (textImageView != VK_NULL_HANDLE) {
255 vkDestroyImageView(device, textImageView, nullptr);
256 }
257 if (textImage != VK_NULL_HANDLE) {
258 vkDestroyImage(device, textImage, nullptr);
259 vkFreeMemory(device, textImageMemory, nullptr);
260 }
261 }
262 if (vertexBuffer != VK_NULL_HANDLE) {
263 vkDestroyBuffer(device, vertexBuffer, nullptr);
264 vkFreeMemory(device, vertexBufferMemory, nullptr);
265 }
266 if (indexBuffer != VK_NULL_HANDLE) {
267 vkDestroyBuffer(device, indexBuffer, nullptr);
268 vkFreeMemory(device, indexBufferMemory, nullptr);
269 }
270 }
271 }
272 };
273
274 VkDevice device = VK_NULL_HANDLE;
275 VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
276 VkQueue graphicsQueue = VK_NULL_HANDLE;
277 VkCommandPool commandPool = VK_NULL_HANDLE;
278
279 TTF_Font *font = nullptr;
280 std::vector<TextQuad> textQuads;
281 VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
282 VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
283 uint32_t maxPoolSets = 100;
284 static constexpr size_t MAX_CACHED_TEXTURES = 256;
285 uint64_t cacheUseSerial = 0;
286 std::unordered_map<CacheKey, CachedTexture, CacheKeyHash> textureCache;
287
288 void initFont(const std::string &fontPath, int fontSize);
289 void initSampler();
290 void printTextG_SolidWithFont(const std::string &text, int x, int y, const SDL_Color &col, TTF_Font *textFont);
291 void pruneCache();
292 void destroyCachedTexture(CachedTexture &cached);
293 void createDescriptorPool();
294 void createDescriptorPool(uint32_t maxSets);
295 void growDescriptorPool();
296 VkDescriptorSet createDescriptorSet(VkImageView imageView);
297 void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer &buffer, VkDeviceMemory &bufferMemory);
298 uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties);
299 [[nodiscard]] bool copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width, uint32_t height);
300 VkCommandBuffer beginSingleTimeCommands();
301 [[nodiscard]] bool endSingleTimeCommands(VkCommandBuffer commandBuffer);
302 [[nodiscard]] bool transitionImageLayout(VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout);
303 void createImage(uint32_t width, uint32_t height, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage &image, VkDeviceMemory &imageMemory);
304 VkImageView createImageView(VkImage image, VkFormat format);
305 SDL_Surface *convertToRGBA(SDL_Surface *surface);
306 };
307} // namespace mxvk
Small RAII wrapper for an SDL_ttf font handle.
Definition mxvk_text.hpp:46
Font()=default
int size() const noexcept
Definition mxvk_text.hpp:61
void reset()
Definition mxvk_text.cpp:27
TTF_Font * get() const noexcept
Definition mxvk_text.hpp:58
Font & operator=(const Font &)=delete
Font(const Font &)=delete
const std::string & path() const noexcept
Definition mxvk_text.hpp:60
void setDescriptorSetLayout(VkDescriptorSetLayout layout)
Assign an external descriptor-set layout.
VK_Text & operator=(const VK_Text &)=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.
VK_Text(const VK_Text &)=delete
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(VK_Text &&)=delete
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
VK_Text & operator=(VK_Text &&)=delete
void printTextG_Solid(const std::string &text, int x, int y, const SDL_Color &col)
Queue a text string for solid (opaque) rendering.
Utilities for loading and saving PNG images.
Definition mxvk.hpp:31
Hash functor for CacheKey.
Definition mxvk_text.hpp:92
size_t operator()(const CacheKey &k) const
Definition mxvk_text.hpp:93
Cache key combining text content and colour.
Definition mxvk_text.hpp:84
TTF_Font * font
Definition mxvk_text.hpp:86
bool operator==(const CacheKey &other) const =default
std::string text
Definition mxvk_text.hpp:85
GPU texture resources cached for a rendered text string.
VkDeviceMemory imageMemory
VkImageView imageView