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_sprite.hpp
Go to the documentation of this file.
1/**
2 * @file mxvk_sprite.hpp
3 * @brief Vulkan 2-D sprite renderer with optional custom shaders and instancing.
4 *
5 * VKSprite manages a Vulkan texture, a screen-space quad, and an optional
6 * custom graphics pipeline. It supports:
7 * - Loading images from PNG files or SDL_Surface objects.
8 * - Drawing at arbitrary positions, scales, and rotations.
9 * - GPU instancing for large batches of identical sprites.
10 * - Extended UBO with mouse state and four custom vec4 uniforms.
11 */
12#pragma once
13
14#include <volk/volk.h>
15
16#include <SDL3/SDL.h>
17
18#include "mxvk_exception.hpp"
19#include <array>
20#include <cstdlib>
21#include <cstring>
22#include <format>
23#include <fstream>
24#include <glm/glm.hpp>
25#include <iostream>
26#include <stdexcept>
27#include <string>
28#include <unordered_map>
29#include <vector>
30#ifdef MXVK_CUDA
31#include <cuda_runtime_api.h>
32#include <opencv2/core/cuda.hpp>
33#endif
34
35#ifndef VK_CHECK_RESULT
36#define VK_CHECK_RESULT(f) \
37 { \
38 VkResult res = (f); \
39 if (res != VK_SUCCESS) { \
40 throw mxvk::Exception(std::format("Fatal : VkResult is \"{}\" in {} at line {}", static_cast<int>(res), __FILE__, __LINE__)); \
41 } \
42 }
43#endif
44
45namespace mxvk {
46
47 /**
48 * @class VKSprite
49 * @brief Vulkan 2-D sprite with texture, custom shader, and instancing support.
50 *
51 * Allocates and manages all Vulkan resources required to render one sprite
52 * type: image, sampler, descriptor set, vertex/index buffers, and an
53 * optional custom pipeline. Multiple draw commands are batched into a
54 * queue and submitted together in renderSprites().
55 */
56 class VK_Sprite {
57 public:
58 /**
59 * @brief Construct and record Vulkan context handles.
60 * @param device Logical device.
61 * @param physicalDevice Physical device.
62 * @param graphicsQueue Graphics queue.
63 * @param commandPool Command pool for staging operations.
64 */
65 VK_Sprite(VkDevice device, VkPhysicalDevice physicalDevice, VkQueue graphicsQueue,
66 VkCommandPool commandPool);
67
68 /** @brief Destructor — frees all Vulkan resources. */
69 ~VK_Sprite();
70
71 VK_Sprite(const VK_Sprite &) = delete;
72 VK_Sprite &operator=(const VK_Sprite &) = delete;
73 VK_Sprite(VK_Sprite &&) = delete;
75
76 /**
77 * @brief Load sprite texture from a PNG file.
78 * @param pngPath Path to the PNG file.
79 * @param fragmentShaderPath Optional custom fragment shader (SPIR-V .spv).
80 */
81 void loadSprite(const std::string &pngPath, const std::string &fragmentShaderPath = "");
82
83 /**
84 * @brief Load sprite texture from an SDL_Surface.
85 * @param surface Source surface (not consumed).
86 * @param fragmentShaderPath Optional custom fragment shader.
87 */
88 void loadSprite(SDL_Surface *surface, const std::string &fragmentShaderPath = "");
89
90 /**
91 * @brief Create a blank (un-initialised) sprite texture.
92 * @param width Pixel width.
93 * @param height Pixel height.
94 * @param vertexShaderPath Optional vertex shader.
95 * @param fragmentShaderPath Optional fragment shader.
96 */
97 void createEmptySprite(int width, int height, const std::string &vertexShaderPath = "", const std::string &fragmentShaderPath = "");
98
99 /**
100 * @brief Queue a draw at the given pixel position.
101 * @param x Destination X.
102 * @param y Destination Y.
103 */
104 void drawSprite(int x, int y);
105
106 /**
107 * @brief Queue a scaled draw.
108 * @param x Destination X.
109 * @param y Destination Y.
110 * @param scaleX Horizontal scale factor.
111 * @param scaleY Vertical scale factor.
112 */
113 void drawSprite(int x, int y, float scaleX, float scaleY);
114
115 /**
116 * @brief Queue a scaled and rotated draw.
117 * @param x Destination X.
118 * @param y Destination Y.
119 * @param scaleX Horizontal scale.
120 * @param scaleY Vertical scale.
121 * @param rotation Clockwise rotation in degrees.
122 */
123 void drawSprite(int x, int y, float scaleX, float scaleY, float rotation);
124
125 /**
126 * @brief Queue a draw into an explicit destination rectangle.
127 * @param x,y,w,h Destination rectangle.
128 */
129 void drawSpriteRect(int x, int y, int w, int h);
130
131 /**
132 * @brief Replace the sprite texture from an SDL_Surface.
133 * @param surface New surface (not consumed).
134 */
135 void updateTexture(SDL_Surface *surface);
136
137 /**
138 * @brief Replace the sprite texture from a raw pixel buffer.
139 * @param pixels Pointer to RGBA data.
140 * @param width Buffer width.
141 * @param height Buffer height.
142 * @param pitch Row stride in bytes (0 = auto).
143 */
144 void updateTexture(const void *pixels, int width, int height, int pitch = 0);
145
146 void setExternalTexture(VkImageView image_view, int width, int height);
148
149#ifdef MXVK_CUDA
150 /** @brief Replace the sprite texture directly from CUDA device memory. */
151 bool updateTextureCuda(const cv::cuda::GpuMat &rgba, cv::cuda::Stream &stream);
152#endif
153
154 /**
155 * @brief Set up to four custom shader float parameters.
156 * @param p1,p2,p3,p4 Parameter values packed into a vec4.
157 */
158 void setShaderParams(float p1 = 0.0f, float p2 = 0.0f, float p3 = 0.0f, float p4 = 0.0f);
159
160 /**
161 * @brief Enable or disable the custom fragment shader effects.
162 * @param enabled @c true to enable effects.
163 */
164 void setEffectsEnabled(bool enabled) { effectsEnabled = enabled; }
165
166 /** @return @c true if shader effects are enabled. */
167 bool getEffectsEnabled() const { return effectsEnabled; }
168
169 /**
170 * @brief Record all queued draw commands into the given command buffer.
171 * @param cmdBuffer Active command buffer.
172 * @param pipelineLayout Pipeline layout for push constants/descriptors.
173 * @param screenWidth Current viewport width.
174 * @param screenHeight Current viewport height.
175 */
176 void renderSprites(VkCommandBuffer cmdBuffer, VkPipelineLayout pipelineLayout,
177 uint32_t screenWidth, uint32_t screenHeight);
178
179 /**
180 * @brief Record texture barriers that must happen before dynamic rendering begins.
181 * @param cmdBuffer Command buffer currently being recorded outside a rendering instance.
182 */
183 void prepareForRendering(VkCommandBuffer cmdBuffer);
184
185 /** @brief Discard all pending draw commands without rendering. */
186 void clearQueue();
187
188 /** @return Sprite texture width in pixels. */
189 int getWidth() const { return spriteWidth; }
190 /** @return Sprite texture height in pixels. */
191 int getHeight() const { return spriteHeight; }
192
193 /**
194 * @brief Select the hardware filter used when scaling this sprite.
195 * @param filter VK_FILTER_NEAREST for sharp pixels or VK_FILTER_LINEAR for smoothing.
196 */
197 void setTextureFilter(VkFilter filter);
198
199 /** @brief Assign an external descriptor-set layout. */
200 void setDescriptorSetLayout(VkDescriptorSetLayout layout) { descriptorSetLayout = layout; }
201 /** @brief Assign the render pass used to build the custom pipeline. */
202 void setRenderPass(VkRenderPass rp) { renderPass = rp; }
203 /** @brief Assign dynamic-rendering color attachment format used to build pipelines. */
204 void setColorAttachmentFormat(VkFormat format) { colorAttachmentFormat = format; }
205 /** @brief Assign dynamic-rendering depth attachment format used to build pipelines. */
206 void setDepthAttachmentFormat(VkFormat format) { depthAttachmentFormat = format; }
207 /**
208 * @brief Rebind the command pool used for upload/staging operations.
209 *
210 * Any in-flight upload resources tied to the previous pool are released first.
211 */
212 void setCommandPool(VkCommandPool pool);
213 /** @brief Use the shared pipeline cache for custom/instanced pipeline creation. */
214 void setPipelineCache(VkPipelineCache cache) { pipelineCache = cache; }
215 /**
216 * @brief Release upload/staging resources tied to the current command pool.
217 *
218 * Call this before destroying or recreating the command pool.
219 */
221 /** @brief Override the vertex shader path (used when rebuilding the pipeline). */
222 void setVertexShaderPath(const std::string &path) { vertexShaderPath = path; }
223 /** @brief Replace the fragment shader path and rebuild the custom pipeline. */
224 void setFragmentShaderPath(const std::string &path);
225
226 /** @return @c true if a custom pipeline has been built. */
227 bool hasOwnPipeline() const { return customPipeline != VK_NULL_HANDLE; }
228 /** @return The custom VkPipeline handle (may be VK_NULL_HANDLE). */
229 VkPipeline getPipeline() const { return customPipeline; }
230 /** @return The custom pipeline layout handle. */
231 VkPipelineLayout getPipelineLayout() const { return customPipelineLayout; }
232
233 /** @brief Destroy and recreate the custom graphics pipeline. */
234 void rebuildPipeline();
235 /** @brief Destroy and recreate the instanced graphics pipeline. */
237
238 VkSampler spriteSampler = VK_NULL_HANDLE; ///< Texture sampler.
239
240 /**
241 * @brief Enable GPU instancing for this sprite type.
242 * @param maxInstances Maximum simultaneous instances.
243 * @param instanceVertShaderPath Vertex shader supporting instancing.
244 * @param instanceFragShaderPath Fragment shader.
245 */
246 void enableInstancing(uint32_t maxInstances,
247 const std::string &instanceVertShaderPath,
248 const std::string &instanceFragShaderPath);
249
250 /** @return @c true if GPU instancing is active. */
251 bool isInstancingEnabled() const { return instancingEnabled; }
252
253 /** @brief Allocate and initialise the extended uniform buffer object. */
254 void enableExtendedUBO();
255
256 /** @return @c true if the extended UBO is active. */
257 bool isExtendedUBOEnabled() const { return extendedUBOEnabled; }
258
259 /**
260 * @brief Upload mouse state to the extended UBO.
261 * @param mx Mouse X (normalised or pixels).
262 * @param my Mouse Y.
263 * @param pressed Mouse button state.
264 * @param reserved Reserved channel.
265 */
266 void setMouseState(float mx, float my, float pressed, float reserved = 0.0f);
267
268 /** @brief Upload user uniform 0 to the extended UBO. @param x,y,z,w Components. */
269 void setUniform0(float x, float y, float z, float w);
270 /** @brief Upload user uniform 1 to the extended UBO. @param x,y,z,w Components. */
271 void setUniform1(float x, float y, float z, float w);
272 /** @brief Upload user uniform 2 to the extended UBO. @param x,y,z,w Components. */
273 void setUniform2(float x, float y, float z, float w);
274 /** @brief Upload user uniform 3 to the extended UBO. @param x,y,z,w Components. */
275 void setUniform3(float x, float y, float z, float w);
276
277 private:
278 struct SpriteVertex {
279 float pos[2];
280 float texCoord[2];
281 };
282
283 struct SpriteDrawCmd {
284 float x, y, w, h;
285 float rotation;
286 glm::vec4 params;
287 };
288
289 VkDevice device = VK_NULL_HANDLE;
290 VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
291 VkQueue graphicsQueue = VK_NULL_HANDLE;
292 VkCommandPool commandPool = VK_NULL_HANDLE;
293 VkPipelineCache pipelineCache = VK_NULL_HANDLE;
294 VkImage spriteImage = VK_NULL_HANDLE;
295 VkDeviceMemory spriteImageMemory = VK_NULL_HANDLE;
296 VkImageView spriteImageView = VK_NULL_HANDLE;
297 int spriteWidth = 0;
298 int spriteHeight = 0;
299 bool spriteLoaded = false;
300 bool externalTexture = false;
301 VkShaderModule fragmentShaderModule = VK_NULL_HANDLE;
302 bool hasCustomShader = false;
303 glm::vec4 shaderParams = glm::vec4(0.0f);
304 bool effectsEnabled = true;
305 std::vector<SpriteDrawCmd> drawQueue;
306
307 VkPipeline customPipeline = VK_NULL_HANDLE;
308 VkPipelineLayout customPipelineLayout = VK_NULL_HANDLE;
309 VkRenderPass renderPass = VK_NULL_HANDLE;
310 VkFormat colorAttachmentFormat = VK_FORMAT_UNDEFINED;
311 VkFormat depthAttachmentFormat = VK_FORMAT_UNDEFINED;
312 std::string vertexShaderPath;
313 std::string fragmentShaderPath;
314 void createCustomPipeline();
315
316 VkBuffer quadVertexBuffer = VK_NULL_HANDLE;
317 VkDeviceMemory quadVertexBufferMemory = VK_NULL_HANDLE;
318 VkBuffer quadIndexBuffer = VK_NULL_HANDLE;
319 VkDeviceMemory quadIndexBufferMemory = VK_NULL_HANDLE;
320 bool quadBufferCreated = false;
321 void createQuadBuffer();
322
323 VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
324 std::vector<VkDescriptorPool> descriptorPools{};
325 VkDescriptorPool descriptorSetPool = VK_NULL_HANDLE;
326 uint32_t nextDescriptorPoolSets = 16;
327 VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
328 VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
329 std::unordered_map<VkImageView, VkDescriptorSet> externalDescriptorSets{};
330 VkFilter textureFilter = VK_FILTER_LINEAR;
331 void createDescriptorPool();
332 void destroyDescriptorPools();
333 void destroyTextureDescriptorPools();
334 VkDescriptorSet createDescriptorSet(VkImageView imageView);
335 void destroySpriteResources();
336
337 VkBuffer persistentStagingBuffer = VK_NULL_HANDLE;
338 VkDeviceMemory persistentStagingMemory = VK_NULL_HANDLE;
339 void *persistentStagingMapped = nullptr;
340 VkDeviceSize persistentStagingSize = 0;
341 VkFence uploadFence = VK_NULL_HANDLE;
342 VkCommandBuffer uploadCmdBuffer = VK_NULL_HANDLE;
343 bool stagingResourcesCreated = false;
344 [[nodiscard]] VkDeviceSize stagingAllocationSize(VkDeviceSize requiredSize) const;
345 void createStagingResources(VkDeviceSize size);
346 void destroyStagingResources();
347 void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage,
348 VkMemoryPropertyFlags properties, VkBuffer &buffer,
349 VkDeviceMemory &bufferMemory);
350 uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties);
351 void copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width, uint32_t height);
352 VkCommandBuffer beginSingleTimeCommands();
353 void endSingleTimeCommands(VkCommandBuffer commandBuffer);
354 void transitionImageLayout(VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout);
355 void createImage(uint32_t width, uint32_t height, VkFormat format, VkImageTiling tiling,
356 VkImageUsageFlags usage, VkMemoryPropertyFlags properties,
357 VkImage &image, VkDeviceMemory &imageMemory);
358 VkImageView createImageView(VkImage image, VkFormat format);
359 void createSampler();
360 SDL_Surface *convertToRGBA(SDL_Surface *surface);
361 void createSpriteTexture(SDL_Surface *surface);
362 void updateSpriteTexture(const void *pixels, uint32_t width, uint32_t height);
363#ifdef MXVK_CUDA
364 void destroyCudaInterop();
365 bool ensureCudaInterop();
366 bool transitionCudaImageForWrite();
367 bool transitionCudaImageForShaderRead();
368 bool updateTextureCudaHost(const void *pixels, uint32_t width, uint32_t height, uint32_t pitch);
369 void recordCudaReadyBarrier(VkCommandBuffer cmdBuffer);
370 void createCudaExportableImage(uint32_t width, uint32_t height, VkImage &image, VkDeviceMemory &imageMemory);
371 VkDeviceSize cudaExportMemorySize = 0;
372 cudaExternalMemory_t cudaExternalMemory = nullptr;
373 cudaMipmappedArray_t cudaMipmappedArray = nullptr;
374 cudaArray_t cudaArray = nullptr;
375 bool cudaInteropEnabled = false;
376 bool cudaInteropUnavailableLogged = false;
377 bool cudaUploadLogged = false;
378 bool cudaWriteTransitionLogged = false;
379 bool cudaSampleBarrierLogged = false;
380 bool cudaImageNeedsShaderBarrier = false;
381 VkImageLayout cudaImageLayout = VK_IMAGE_LAYOUT_UNDEFINED;
382#endif
383 std::vector<char> readShaderFile(const std::string &filename);
384
385 struct SpriteExtendedUBO {
386 glm::vec4 mouse;
387 glm::vec4 u0;
388 glm::vec4 u1;
389 glm::vec4 u2;
390 glm::vec4 u3;
391 };
392 bool extendedUBOEnabled = false;
393 SpriteExtendedUBO extendedUBOData{};
394 VkBuffer extendedUBOBuffer = VK_NULL_HANDLE;
395 VkDeviceMemory extendedUBOMemory = VK_NULL_HANDLE;
396 void *extendedUBOMapped = nullptr;
397 VkDescriptorSetLayout extendedDescriptorSetLayout = VK_NULL_HANDLE;
398 VkDescriptorPool extendedDescriptorPool = VK_NULL_HANDLE;
399 VkDescriptorSet extendedDescriptorSet = VK_NULL_HANDLE;
400 bool ownExtendedDescriptorSetLayout = false;
401 void createExtendedUBO();
402 void updateExtendedUBO();
403 void createExtendedDescriptorSetLayout();
404 void createExtendedDescriptorSet();
405 void destroyExtendedUBO();
406
407 struct SpriteInstanceData {
408 float posX, posY, sizeW, sizeH;
409 float params[4];
410 };
411 VkBuffer instanceBuffer = VK_NULL_HANDLE;
412 VkDeviceMemory instanceBufferMemory = VK_NULL_HANDLE;
413 void *instanceBufferMapped = nullptr;
414 uint32_t instanceBufferCapacity = 0;
415 bool instancingEnabled = false;
416 VkPipeline instancedPipeline = VK_NULL_HANDLE;
417 VkPipelineLayout instancedPipelineLayout = VK_NULL_HANDLE;
418 std::string instanceVertPath;
419 std::string instanceFragPath;
420 void createInstancedPipeline(const std::string &vertPath, const std::string &fragPath);
421 void ensureInstanceBuffer(uint32_t count);
422 void destroyInstanceResources();
423 };
424
425} // namespace mxvk
void setUniform3(float x, float y, float z, float w)
Upload user uniform 3 to the extended UBO.
VkPipelineLayout getPipelineLayout() const
void enableInstancing(uint32_t maxInstances, const std::string &instanceVertShaderPath, const std::string &instanceFragShaderPath)
Enable GPU instancing for this sprite type.
void renderSprites(VkCommandBuffer cmdBuffer, VkPipelineLayout pipelineLayout, uint32_t screenWidth, uint32_t screenHeight)
Record all queued draw commands into the given command buffer.
void setVertexShaderPath(const std::string &path)
Override the vertex shader path (used when rebuilding the pipeline).
bool isInstancingEnabled() const
void releaseUploadResources()
Release upload/staging resources tied to the current command pool.
~VK_Sprite()
Destructor — frees all Vulkan resources.
void setUniform1(float x, float y, float z, float w)
Upload user uniform 1 to the extended UBO.
int getHeight() const
bool getEffectsEnabled() const
void setUniform0(float x, float y, float z, float w)
Upload user uniform 0 to the extended UBO.
void setDepthAttachmentFormat(VkFormat format)
Assign dynamic-rendering depth attachment format used to build pipelines.
VK_Sprite(VK_Sprite &&)=delete
void setCommandPool(VkCommandPool pool)
Rebind the command pool used for upload/staging operations.
VK_Sprite & operator=(VK_Sprite &&)=delete
void setShaderParams(float p1=0.0f, float p2=0.0f, float p3=0.0f, float p4=0.0f)
Set up to four custom shader float parameters.
void setDescriptorSetLayout(VkDescriptorSetLayout layout)
Assign an external descriptor-set layout.
bool isExtendedUBOEnabled() const
void enableExtendedUBO()
Allocate and initialise the extended uniform buffer object.
void prepareForRendering(VkCommandBuffer cmdBuffer)
Record texture barriers that must happen before dynamic rendering begins.
void updateTexture(SDL_Surface *surface)
Replace the sprite texture from an SDL_Surface.
bool hasOwnPipeline() const
void rebuildInstancedPipeline()
Destroy and recreate the instanced graphics pipeline.
int getWidth() const
void setColorAttachmentFormat(VkFormat format)
Assign dynamic-rendering color attachment format used to build pipelines.
void setRenderPass(VkRenderPass rp)
Assign the render pass used to build the custom pipeline.
void loadSprite(const std::string &pngPath, const std::string &fragmentShaderPath="")
Load sprite texture from a PNG file.
void clearExternalTextureDescriptors()
void createEmptySprite(int width, int height, const std::string &vertexShaderPath="", const std::string &fragmentShaderPath="")
Create a blank (un-initialised) sprite texture.
VkPipeline getPipeline() const
void drawSprite(int x, int y)
Queue a draw at the given pixel position.
void setMouseState(float mx, float my, float pressed, float reserved=0.0f)
Upload mouse state to the extended UBO.
void setExternalTexture(VkImageView image_view, int width, int height)
void setEffectsEnabled(bool enabled)
Enable or disable the custom fragment shader effects.
void setPipelineCache(VkPipelineCache cache)
Use the shared pipeline cache for custom/instanced pipeline creation.
void setTextureFilter(VkFilter filter)
Select the hardware filter used when scaling this sprite.
void setFragmentShaderPath(const std::string &path)
Replace the fragment shader path and rebuild the custom pipeline.
void clearQueue()
Discard all pending draw commands without rendering.
void rebuildPipeline()
Destroy and recreate the custom graphics pipeline.
VK_Sprite & operator=(const VK_Sprite &)=delete
VK_Sprite(const VK_Sprite &)=delete
VK_Sprite(VkDevice device, VkPhysicalDevice physicalDevice, VkQueue graphicsQueue, VkCommandPool commandPool)
Construct and record Vulkan context handles.
void setUniform2(float x, float y, float z, float w)
Upload user uniform 2 to the extended UBO.
void drawSpriteRect(int x, int y, int w, int h)
Queue a draw into an explicit destination rectangle.
VkSampler spriteSampler
Texture sampler.
Utilities for loading and saving PNG images.
Definition mxvk.hpp:30