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.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,
126 VkCommandPool commandPool, const std::string &fontPath, int fontSize = 24);
127
128 /** @brief Destructor -- destroys all Vulkan and SDL_ttf resources. */
129 ~VK_Text();
130
131 VK_Text(const VK_Text &) = delete;
132 VK_Text &operator=(const VK_Text &) = delete;
133 VK_Text(VK_Text &&) = delete;
134 VK_Text &operator=(VK_Text &&) = delete;
135
136 /**
137 * @brief Queue a text string for solid (opaque) rendering.
138 * @param text String to draw.
139 * @param x Destination X in pixels.
140 * @param y Destination Y in pixels.
141 * @param col Text colour.
142 */
143 void printTextG_Solid(const std::string &text, int x, int y, const SDL_Color &col);
144 void printTextG_Solid(const std::string &text, int x, int y, const SDL_Color &col, TTF_Font *textFont);
145 void printTextG_Solid(const std::string &text, int x, int y, const SDL_Color &col, const Font &textFont);
146
147 /**
148 * @brief Record all queued text quads into a command buffer.
149 * @param cmdBuffer Active Vulkan command buffer.
150 * @param pipelineLayout Pipeline layout for push constants.
151 * @param screenWidth Viewport width.
152 * @param screenHeight Viewport height.
153 */
154 void renderText(VkCommandBuffer cmdBuffer, VkPipelineLayout pipelineLayout,
155 uint32_t screenWidth, uint32_t screenHeight);
156
157 /** @brief Discard all pending text quads without rendering them. */
158 void clearQueue();
159
160 /** @brief Assign an external descriptor-set layout. */
161 void setDescriptorSetLayout(VkDescriptorSetLayout layout) { descriptorSetLayout = layout; }
162
163 /**
164 * @brief Change the active font.
165 * @param fontPath New font file path.
166 * @param fontSize New point size.
167 */
168 void setFont(const std::string &fontPath, int fontSize);
169
170 /** @brief Destroy all cached text textures, freeing GPU memory. */
171 void clearCache();
172
173 /**
174 * @brief Measure the pixel dimensions of a string with the current font.
175 * @param text String to measure.
176 * @param width Output: pixel width.
177 * @param height Output: pixel height.
178 * @return @c true if measurement succeeded.
179 */
180 bool getTextDimensions(const std::string &text, int &width, int &height);
181 bool getTextDimensions(const std::string &text, int &width, int &height, TTF_Font *textFont);
182 bool getTextDimensions(const std::string &text, int &width, int &height, const Font &textFont);
183
184 VkSampler fontSampler = VK_NULL_HANDLE; ///< Sampler used for all text textures.
185 private:
186 struct TextVertex {
187 float pos[2];
188 float texCoord[2];
189 };
190
191 struct TextQuad {
192 std::string text;
193 int x, y;
194 int width, height;
195 SDL_Color color;
196 float alpha = 1.0f;
197 std::vector<TextVertex> vertices;
198 std::vector<uint16_t> indices;
199 VkBuffer vertexBuffer = VK_NULL_HANDLE;
200 VkDeviceMemory vertexBufferMemory = VK_NULL_HANDLE;
201 VkBuffer indexBuffer = VK_NULL_HANDLE;
202 VkDeviceMemory indexBufferMemory = VK_NULL_HANDLE;
203 VkImage textImage = VK_NULL_HANDLE;
204 VkDeviceMemory textImageMemory = VK_NULL_HANDLE;
205 VkImageView textImageView = VK_NULL_HANDLE;
206 VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
207 uint32_t indexCount = 0;
208 VkDevice device = VK_NULL_HANDLE;
209 bool ownsTexture = true; ///< false when texture is owned by the cache.
210
211 TextQuad() = default;
212 TextQuad(const TextQuad &) = delete;
213 TextQuad &operator=(const TextQuad &) = delete;
214 TextQuad(TextQuad &&other) noexcept {
215 *this = std::move(other);
216 }
217 TextQuad &operator=(TextQuad &&other) noexcept {
218 if (this != &other) {
219 text = std::move(other.text);
220 x = other.x;
221 y = other.y;
222 width = other.width;
223 height = other.height;
224 color = other.color;
225 alpha = other.alpha;
226 vertices = std::move(other.vertices);
227 indices = std::move(other.indices);
228 vertexBuffer = other.vertexBuffer;
229 vertexBufferMemory = other.vertexBufferMemory;
230 indexBuffer = other.indexBuffer;
231 indexBufferMemory = other.indexBufferMemory;
232 textImage = other.textImage;
233 textImageMemory = other.textImageMemory;
234 textImageView = other.textImageView;
235 descriptorSet = other.descriptorSet;
236 indexCount = other.indexCount;
237 device = other.device;
238 ownsTexture = other.ownsTexture;
239
240 other.ownsTexture = false;
241 other.vertexBuffer = VK_NULL_HANDLE;
242 other.vertexBufferMemory = VK_NULL_HANDLE;
243 other.indexBuffer = VK_NULL_HANDLE;
244 other.indexBufferMemory = VK_NULL_HANDLE;
245 other.textImage = VK_NULL_HANDLE;
246 other.textImageMemory = VK_NULL_HANDLE;
247 other.textImageView = VK_NULL_HANDLE;
248 other.descriptorSet = VK_NULL_HANDLE;
249 other.indexCount = 0;
250 other.device = VK_NULL_HANDLE;
251 }
252 return *this;
253 }
254
255 ~TextQuad() {
256 if (device != VK_NULL_HANDLE) {
257 if (ownsTexture) {
258 if (textImageView != VK_NULL_HANDLE) {
259 vkDestroyImageView(device, textImageView, nullptr);
260 }
261 if (textImage != VK_NULL_HANDLE) {
262 vkDestroyImage(device, textImage, nullptr);
263 vkFreeMemory(device, textImageMemory, nullptr);
264 }
265 }
266 if (vertexBuffer != VK_NULL_HANDLE) {
267 vkDestroyBuffer(device, vertexBuffer, nullptr);
268 vkFreeMemory(device, vertexBufferMemory, nullptr);
269 }
270 if (indexBuffer != VK_NULL_HANDLE) {
271 vkDestroyBuffer(device, indexBuffer, nullptr);
272 vkFreeMemory(device, indexBufferMemory, nullptr);
273 }
274 }
275 }
276 };
277
278 VkDevice device = VK_NULL_HANDLE;
279 VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
280 VkQueue graphicsQueue = VK_NULL_HANDLE;
281 VkCommandPool commandPool = VK_NULL_HANDLE;
282
283 TTF_Font *font = nullptr;
284 std::vector<TextQuad> textQuads;
285 VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
286 VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
287 uint32_t maxPoolSets = 100;
288 static constexpr size_t MAX_CACHED_TEXTURES = 256;
289 uint64_t cacheUseSerial = 0;
290 std::unordered_map<CacheKey, CachedTexture, CacheKeyHash> textureCache;
291
292 void initFont(const std::string &fontPath, int fontSize);
293 void initSampler();
294 void printTextG_SolidWithFont(const std::string &text, int x, int y, const SDL_Color &col, TTF_Font *textFont);
295 void pruneCache();
296 void destroyCachedTexture(CachedTexture &cached);
297 void createDescriptorPool();
298 void createDescriptorPool(uint32_t maxSets);
299 void growDescriptorPool();
300 VkDescriptorSet createDescriptorSet(VkImageView imageView);
301 void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage,
302 VkMemoryPropertyFlags properties, VkBuffer &buffer,
303 VkDeviceMemory &bufferMemory);
304 uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties);
305 [[nodiscard]] bool copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width, uint32_t height);
306 VkCommandBuffer beginSingleTimeCommands();
307 [[nodiscard]] bool endSingleTimeCommands(VkCommandBuffer commandBuffer);
308 [[nodiscard]] bool transitionImageLayout(VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout);
309 void createImage(uint32_t width, uint32_t height, VkFormat format, VkImageTiling tiling,
310 VkImageUsageFlags usage, VkMemoryPropertyFlags properties,
311 VkImage &image, VkDeviceMemory &imageMemory);
312 VkImageView createImageView(VkImage image, VkFormat format);
313 SDL_Surface *convertToRGBA(SDL_Surface *surface);
314 };
315} // 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:35
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.
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()
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
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:30
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