SDEngine
Game Engine
Loading...
Searching...
No Matches
utils.hpp
Go to the documentation of this file.
1// TODO(docs): Add file-level Doxygen header
2// - @file Utils.hpp
3// - @brief Vulkan utility functions and macros
4// - Note: This is a "kitchen sink" file - consider splitting
5// - Categories: Error checking, buffer/image creation, texture loading
6#pragma once
7#include <algorithm>
8#include <expected>
9#include <filesystem>
10#include <limits>
11#include <ranges>
12#include <stb_image.h>
13#include <string>
14#include <string_view>
15#include <vector>
16
17#include <vulkan/vulkan.hpp>
18
19#include "SD/core/base.hpp"
21
26namespace sd {
27
28
29// TODO(docs): Document VK_CHECK macro
30// - Purpose: Vulkan error checking with abort on failure
31// - When to use vs CheckVulkanRes
32// - Example usage
33#define VK_CHECK(x) \
34 do { \
35 VkResult res = (x); \
36 if (res != VK_SUCCESS) \
37 log::engine::critical("Vulkan error: " #x); \
38 } while (0)
39
40// TODO(docs): Document SingleTimeCommand function
41// - Purpose: Execute Vulkan commands with automatic synchronization
42// - Error handling (returns std::expected)
43// - Performance considerations
44// - Example usage
45inline std::expected<void, std::string>
46single_time_command(const vk::Device& device,
47 const vk::Queue& queue,
48 const vk::CommandPool& command_pool,
49 const std::function<void(const vk::CommandBuffer&)>& action) {
50 vk::CommandBufferAllocateInfo alloc_info{.commandPool = command_pool,
51 .level = vk::CommandBufferLevel::ePrimary,
52 .commandBufferCount = 1};
53
54 auto alloc_res = device.allocateCommandBuffers(alloc_info);
55 if (alloc_res.result != vk::Result::eSuccess) {
56 return std::unexpected("Failed to allocate command buffers: " +
57 vk::to_string(alloc_res.result));
58 }
59 vk::CommandBuffer command_buffers = alloc_res.value.front();
60
61 vk::CommandBufferBeginInfo begin_info{.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit};
62
63 if (auto res = command_buffers.begin(begin_info); res != vk::Result::eSuccess) {
64 device.freeCommandBuffers(command_pool, command_buffers);
65 return std::unexpected("Failed to begin cmdBuffer: " + vk::to_string(res));
66 }
67
69
70 if (auto res = command_buffers.end(); res != vk::Result::eSuccess) {
71 device.freeCommandBuffers(command_pool, command_buffers);
72 return std::unexpected("Failed to end cmdbuffer: " + vk::to_string(res));
73 }
74
75 auto fence_res = device.createFenceUnique({});
76 if (fence_res.result != vk::Result::eSuccess) {
77 device.freeCommandBuffers(command_pool, command_buffers);
78 return std::unexpected("Failed to create fence: " + vk::to_string(fence_res.result));
79 }
80 vk::UniqueFence fence = std::move(fence_res.value);
81
82 vk::SubmitInfo submit_info{.commandBufferCount = 1, .pCommandBuffers = &command_buffers};
83
84 if (auto res = queue.submit(submit_info, *fence); res != vk::Result::eSuccess) {
85 device.freeCommandBuffers(command_pool, command_buffers);
86 return std::unexpected("Failed to submit to queue: " + vk::to_string(res));
87 }
88
89 if (auto res = device.waitForFences(*fence, VK_TRUE, std::numeric_limits<uint64_t>::max());
90 res != vk::Result::eSuccess) {
91 device.freeCommandBuffers(command_pool, command_buffers);
92 return std::unexpected("Failed to wait for fence: " + vk::to_string(res));
93 }
94
95 device.freeCommandBuffers(command_pool, command_buffers);
96 return {};
97}
98
99
100// TODO(docs): Document CreateBuffer function
101// - Purpose: Create a Vulkan buffer with memory allocation
102// - Memory property flags explanation
103// - Note about VMA TODO - this will be deprecated
104// - Example usage
105inline std::pair<vk::UniqueBuffer, vk::UniqueDeviceMemory>
106create_buffer(const vk::Device& device,
107 const vk::PhysicalDevice& physical_device,
108 vk::DeviceSize size,
109 vk::BufferUsageFlags usage,
110 vk::MemoryPropertyFlags properties) {
111 // TODO: Use VMA (Vulkan Memory Allocator) instead of manual memory allocation
112 // TODO: Create a Buffer abstraction class to handle creation, mapping, and destruction
113
114 vk::BufferCreateInfo buffer_info{.size = size,
115 .usage = usage,
116 .sharingMode = vk::SharingMode::eExclusive};
117
118 vk::UniqueBuffer buffer = check_vulkan_res_val(device.createBufferUnique(buffer_info),
119 "Failed to create unique buffer: ");
120
121 vk::MemoryRequirements mem_requirements = device.getBufferMemoryRequirements(*buffer);
122
123 vk::MemoryAllocateInfo allocate_info{
124 .allocationSize = mem_requirements.size,
125 .memoryTypeIndex =
127
128 vk::UniqueDeviceMemory buffer_memory =
129 check_vulkan_res_val(device.allocateMemoryUnique(allocate_info),
130 "Failed to allocate unique memory for buffer: ");
131
132 check_vulkan_res(device.bindBufferMemory(*buffer, *buffer_memory, 0),
133 "Failed to bind buffer memory");
134
135 return {std::move(buffer), std::move(buffer_memory)};
136}
137
138// TODO(docs): Document CreateImage function
139// - Purpose: Create a Vulkan image with memory allocation
140// - Format, tiling, usage parameter guidance
141// - Note about VMA TODO
142inline std::pair<vk::UniqueImage, vk::UniqueDeviceMemory>
143create_image(const vk::Device& device,
144 const vk::PhysicalDevice& physical_device,
145 uint32_t width,
146 uint32_t height,
147 vk::Format format,
148 vk::ImageTiling tiling,
149 vk::ImageUsageFlags usage,
150 vk::MemoryPropertyFlags properties) {
151 // TODO: Use VMA (Vulkan Memory Allocator) instead of manual memory allocation
152 // TODO: Create an Image abstraction class to handle creation, views, and memory
153 vk::ImageCreateInfo image_info{
154 .imageType = vk::ImageType::e2D,
155 .format = format,
156 .extent = vk::Extent3D{width, height, 1},
157 .mipLevels = 1,
158 .arrayLayers = 1,
159 .samples = vk::SampleCountFlagBits::e1,
160 .tiling = tiling,
161 .usage = usage,
162 .sharingMode = vk::SharingMode::eExclusive
163 };
164
165 vk::UniqueImage image =
166 check_vulkan_res_val(device.createImageUnique(image_info), "Failed to create unique image: ");
167
168 vk::MemoryRequirements mem_requirements = device.getImageMemoryRequirements(*image);
169 vk::MemoryAllocateInfo allocate_info{
170 .allocationSize = mem_requirements.size,
171 .memoryTypeIndex =
173 vk::UniqueDeviceMemory image_memory =
174 check_vulkan_res_val(device.allocateMemoryUnique(allocate_info),
175 "Failed to allocate unique memory:");
176
177 check_vulkan_res(device.bindImageMemory(*image, *image_memory, 0),
178 "Failed to bind image memory: ");
179
180 return {std::move(image), std::move(image_memory)};
181}
182
183// TODO(docs): Document CopyBufferToImage function
184// - Purpose: Copy buffer data to an image via command buffer
185// - Requires image to be in eTransferDstOptimal layout
186inline void copy_buffer_to_image(const vk::CommandBuffer& cmd_buffer,
187 const vk::Buffer& buffer,
188 const vk::Image& image,
189 uint32_t width,
190 uint32_t height) {
191 vk::BufferImageCopy region{
192 .bufferOffset = 0,
193 .bufferRowLength = 0,
194 .bufferImageHeight = 0,
195 .imageSubresource = vk::ImageSubresourceLayers{.aspectMask = vk::ImageAspectFlagBits::eColor,
196 .mipLevel = 0,
197 .baseArrayLayer = 0,
198 .layerCount = 1},
199 .imageOffset = vk::Offset3D{0, 0, 0},
200 .imageExtent = vk::Extent3D{width, height, 1}
201 };
202 cmd_buffer.copyBufferToImage(buffer, image, vk::ImageLayout::eTransferDstOptimal, region);
203}
204
205// TODO(docs): Document TransitionImageLayout function
206// - Purpose: Transition image layout with pipeline barrier
207// - Supported transitions (Undefined->TransferDst, TransferDst->ShaderReadOnly)
208// - Pipeline stage and access mask logic
209// - Note about ImageMemoryBarrier2 TODO
210inline void transition_image_layout(const vk::CommandBuffer& cmd_buffer,
211 const vk::Image& image,
212 [[maybe_unused]] vk::Format format,
213 vk::ImageLayout old_layout,
214 vk::ImageLayout new_layout) {
215 // TODO: Use vk::ImageMemoryBarrier2 for better synchronization (requires Vulkan 1.3 or extension)
216 vk::PipelineStageFlags source_stage;
217 vk::PipelineStageFlags destination_stage;
218 vk::AccessFlags src_access_mask;
219 vk::AccessFlags dst_access_mask;
220
221 if (old_layout == vk::ImageLayout::eUndefined &&
222 new_layout == vk::ImageLayout::eTransferDstOptimal) {
223 src_access_mask = {};
224 dst_access_mask = vk::AccessFlagBits::eTransferWrite;
225
226 source_stage = vk::PipelineStageFlagBits::eTopOfPipe;
227 destination_stage = vk::PipelineStageFlagBits::eTransfer;
228 } else if (old_layout == vk::ImageLayout::eTransferDstOptimal &&
229 new_layout == vk::ImageLayout::eShaderReadOnlyOptimal) {
230 src_access_mask = vk::AccessFlagBits::eTransferWrite;
231 dst_access_mask = vk::AccessFlagBits::eShaderRead;
232
233 source_stage = vk::PipelineStageFlagBits::eTransfer;
234 destination_stage = vk::PipelineStageFlagBits::eFragmentShader;
235 } else {
236 log::engine::critical("unsupported layout transition!");
237 }
238
239 vk::ImageMemoryBarrier barrier{
240 .srcAccessMask = src_access_mask,
241 .dstAccessMask = dst_access_mask,
242 .oldLayout = old_layout,
243 .newLayout = new_layout,
244 .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
245 .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
246 .image = image,
247 .subresourceRange = vk::ImageSubresourceRange{.aspectMask = vk::ImageAspectFlagBits::eColor,
248 .baseMipLevel = 0,
249 .levelCount = 1,
250 .baseArrayLayer = 0,
251 .layerCount = 1}
252 };
253
254 cmd_buffer.pipelineBarrier(source_stage, destination_stage, {}, nullptr, nullptr, barrier);
255}
256
257
258// TODO(docs): Document Texture struct
259// - Purpose: Simple texture container (image + memory + view)
260// - Note about Texture class TODO - this will be replaced
261// - Ownership semantics
262struct Texture {
263 vk::UniqueImage image;
264 vk::UniqueDeviceMemory image_memory;
265 vk::UniqueImageView image_view;
266};
267
268// TODO(docs): Document CreateTexture function
269// - Purpose: Load a texture from file (STB image + Vulkan upload)
270// - Format (R8G8B8A8Srgb) and why
271// - Error handling (file not found, Vulkan errors)
272// - Performance notes (staging buffer)
273// - Example usage
274inline std::expected<Texture, std::string> create_texture(const vk::Device& device,
275 const vk::PhysicalDevice& physical_device,
276 const vk::Queue& graphics_queue,
277 const vk::CommandPool& command_pool,
278 const std::filesystem::path& file_path) {
280 stbi_uc* pixels =
282 const vk::DeviceSize image_size = tex_width * tex_height * 4;
283
284 if (!pixels) {
285 return std::unexpected("Failed to load texture image: " + file_path.string());
286 }
287
289 device,
292 vk::BufferUsageFlagBits::eTransferSrc,
293 vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent);
294
295 void* data = check_vulkan_res_val(device.mapMemory(*staging_buffer_memory, 0, image_size),
296 "Failed to map texture image: ");
297
298 memcpy(data, pixels, image_size);
299 device.unmapMemory(*staging_buffer_memory);
300
302
303 auto [image, image_memory] =
306 tex_width,
308 vk::Format::eR8G8B8A8Srgb,
309 vk::ImageTiling::eOptimal,
310 vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled,
311 vk::MemoryPropertyFlagBits::eDeviceLocal);
312
313 auto cmd_res =
317 [&](const vk::CommandBuffer& cmdBuffer) {
319 *image,
320 vk::Format::eR8G8B8A8Srgb,
321 vk::ImageLayout::eUndefined,
322 vk::ImageLayout::eTransferDstOptimal);
325 *image,
326 static_cast<uint32_t>(tex_width),
327 static_cast<uint32_t>(tex_height));
329 *image,
330 vk::Format::eR8G8B8A8Srgb,
331 vk::ImageLayout::eTransferDstOptimal,
332 vk::ImageLayout::eShaderReadOnlyOptimal);
333 });
334
335 if (!cmd_res) {
336 return std::unexpected(cmd_res.error());
337 }
338
339 vk::ImageViewCreateInfo view_info{
340 .image = *image,
341 .viewType = vk::ImageViewType::e2D,
342 .format = vk::Format::eR8G8B8A8Srgb,
343 .subresourceRange = vk::ImageSubresourceRange{.aspectMask = vk::ImageAspectFlagBits::eColor,
344 .baseMipLevel = 0,
345 .levelCount = 1,
346 .baseArrayLayer = 0,
347 .layerCount = 1}
348 };
349 vk::UniqueImageView image_view = check_vulkan_res_val(device.createImageViewUnique(view_info),
350 "Failed to create unique image view: ");
351
352 return Texture{.image = std::move(image),
353 .image_memory = std::move(image_memory),
354 .image_view = std::move(image_view)};
355}
356
363template<std::ranges::input_range R>
364 requires std::constructible_from<std::string_view, std::ranges::range_value_t<R>>
365std::string tab_format(R&& items, USize cols = 4, USize spacing = 2, bool row_major = true) {
366 std::vector<std::string_view> svs;
367 USize max_width = 0;
368 for (auto&& item : items) {
369 std::string_view sv(item);
370 svs.push_back(sv);
371 if (sv.size() > max_width)
372 max_width = sv.size();
373 }
374
375 if (svs.empty())
376 return {};
377
379 USize cols_ = cols > 0 ? cols : 1;
380 USize rows_ = (svs.size() + cols_ - 1) / cols_;
381 std::string result;
382 result.reserve(svs.size() * col_width + rows_);
383
384 if (row_major) {
385 for (USize i = 0; i < svs.size(); ++i) {
386 result += svs[i];
387 if (svs[i].size() < col_width)
388 result.append(col_width - svs[i].size(), ' ');
389 if ((i + 1) % cols_ == 0 && (i + 1) < svs.size())
390 result += '\n';
391 }
392 } else {
393 for (USize r = 0; r < rows_; ++r) {
394 for (USize c = 0; c < cols_; ++c) {
395 USize idx = r + c * rows_;
396 if (idx >= svs.size())
397 break;
398 result += svs[idx];
399 if (svs[idx].size() < col_width)
400 result.append(col_width - svs[idx].size(), ' ');
401 }
402 if (r + 1 < rows_)
403 result += '\n';
404 }
405 }
406 return result;
407}
408} // namespace sd
Definition Application.hpp:22
void transition_image_layout(const vk::CommandBuffer &cmd_buffer, const vk::Image &image, vk::Format format, vk::ImageLayout old_layout, vk::ImageLayout new_layout)
Definition utils.hpp:210
auto check_vulkan_res_val(T &&result, std::string_view message, std::source_location loc=std::source_location::current())
Definition vulkan_utils.hpp:25
std::pair< vk::UniqueImage, vk::UniqueDeviceMemory > create_image(const vk::Device &device, const vk::PhysicalDevice &physical_device, uint32_t width, uint32_t height, vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties)
Definition utils.hpp:143
void check_vulkan_res(vk::Result result, std::string_view message, std::source_location loc=std::source_location::current())
Definition vulkan_utils.hpp:10
void copy_buffer_to_image(const vk::CommandBuffer &cmd_buffer, const vk::Buffer &buffer, const vk::Image &image, uint32_t width, uint32_t height)
Definition utils.hpp:186
std::pair< vk::UniqueBuffer, vk::UniqueDeviceMemory > create_buffer(const vk::Device &device, const vk::PhysicalDevice &physical_device, vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags properties)
Definition utils.hpp:106
std::string tab_format(R &&items, USize cols=4, USize spacing=2, bool row_major=true)
Definition utils.hpp:365
U32 find_memory_type(const vk::PhysicalDevice &physical_device, U32 type_filter, vk::MemoryPropertyFlags properties)
Definition vulkan_utils.hpp:43
std::expected< void, std::string > single_time_command(const vk::Device &device, const vk::Queue &queue, const vk::CommandPool &command_pool, const std::function< void(const vk::CommandBuffer &)> &action)
Definition utils.hpp:46
std::expected< Texture, std::string > create_texture(const vk::Device &device, const vk::PhysicalDevice &physical_device, const vk::Queue &graphics_queue, const vk::CommandPool &command_pool, const std::filesystem::path &file_path)
Definition utils.hpp:274
Definition utils.hpp:262
vk::UniqueDeviceMemory image_memory
Definition utils.hpp:264
vk::UniqueImageView image_view
Definition utils.hpp:265
vk::UniqueImage image
Definition utils.hpp:263
consteval U64 type_id_of()
Definition type_id.hpp:6
std::size_t USize
Definition types.hpp:18