initial set of core vulkan objects

This commit is contained in:
PancakeTAS 2025-06-29 05:05:47 +02:00
parent fe5a8520e5
commit 40d7d032a3
No known key found for this signature in database
12 changed files with 715 additions and 0 deletions

View file

@ -0,0 +1,105 @@
#ifndef COMMANDBUFFER_HPP
#define COMMANDBUFFER_HPP
#include "core/commandpool.hpp"
#include "core/semaphore.hpp"
#include "device.hpp"
#include <vulkan/vulkan_core.h>
#include <optional>
#include <vector>
#include <memory>
namespace Vulkan::Core {
/// State of the command buffer.
enum class CommandBufferState {
/// Command buffer is not initialized or has been destroyed.
Invalid,
/// Command buffer has been created.
Empty,
/// Command buffer recording has started.
Recording,
/// Command buffer recording has ended.
Full,
/// Command buffer has been submitted to a queue.
Submitted
};
///
/// C++ wrapper class for a Vulkan command buffer.
///
/// This class manages the lifetime of a Vulkan command buffer.
///
class CommandBuffer {
public:
///
/// Create the command buffer.
///
/// @param device Vulkan device
/// @param pool Vulkan command pool
///
/// @throws std::invalid_argument if the device or pool are invalid.
/// @throws ls::vulkan_error if object creation fails.
///
CommandBuffer(const Device& device, const CommandPool& pool);
///
/// Begin recording commands in the command buffer.
///
/// @throws std::logic_error if the command buffer is in Empty state
/// @throws ls::vulkan_error if beginning the command buffer fails.
///
void begin();
///
/// End recording commands in the command buffer.
///
/// @throws std::logic_error if the command buffer is not in Recording state
/// @throws ls::vulkan_error if ending the command buffer fails.
///
void end();
///
/// Submit the command buffer to a queue.
///
/// @param queue Vulkan queue to submit to
/// @param waitSemaphores Semaphores to wait on before executing the command buffer
/// @param waitSemaphoreValues Values for the semaphores to wait on
/// @param signalSemaphores Semaphores to signal after executing the command buffer
/// @param signalSemaphoreValues Values for the semaphores to signal
///
/// @throws std::invalid_argument if the queue is null.
/// @throws std::logic_error if the command buffer is not in Full state.
/// @throws ls::vulkan_error if submission fails.
///
void submit(VkQueue queue, // TODO: fence
const std::vector<Semaphore>& waitSemaphores = {},
std::optional<std::vector<uint64_t>> waitSemaphoreValues = std::nullopt,
const std::vector<Semaphore>& signalSemaphores = {},
std::optional<std::vector<uint64_t>> signalSemaphoreValues = std::nullopt);
/// Get the state of the command buffer.
[[nodiscard]] CommandBufferState getState() const { return *this->state; }
/// Get the Vulkan handle.
[[nodiscard]] auto handle() const { *this->commandBuffer; }
/// Check whether the object is valid.
[[nodiscard]] bool isValid() const { return static_cast<bool>(this->commandBuffer); }
/// if (obj) operator. Checks if the object is valid.
explicit operator bool() const { return this->isValid(); }
/// Trivially copyable, moveable and destructible
CommandBuffer(const CommandBuffer&) noexcept = default;
CommandBuffer& operator=(const CommandBuffer&) noexcept = default;
CommandBuffer(CommandBuffer&&) noexcept = default;
CommandBuffer& operator=(CommandBuffer&&) noexcept = default;
~CommandBuffer() = default;
private:
std::shared_ptr<CommandBufferState> state;
std::shared_ptr<VkCommandBuffer> commandBuffer;
};
}
#endif // COMMANDBUFFER_HPP

View file

@ -0,0 +1,56 @@
#ifndef COMMANDPOOL_HPP
#define COMMANDPOOL_HPP
#include "device.hpp"
#include <vulkan/vulkan_core.h>
#include <memory>
namespace Vulkan::Core {
/// Enumeration for different types of command pools.
enum class CommandPoolType {
/// Used for compute-type command buffers.
Compute
};
///
/// C++ wrapper class for a Vulkan command pool.
///
/// This class manages the lifetime of a Vulkan command pool.
///
class CommandPool {
public:
///
/// Create the command pool.
///
/// @param device Vulkan device
/// @param type Type of command pool to create.
///
/// @throws std::invalid_argument if the device is invalid.
/// @throws ls::vulkan_error if object creation fails.
///
CommandPool(const Device& device, CommandPoolType type);
/// Get the Vulkan handle.
[[nodiscard]] auto handle() const { return *this->commandPool; }
/// Check whether the object is valid.
[[nodiscard]] bool isValid() const { return static_cast<bool>(this->commandPool); }
/// if (obj) operator. Checks if the object is valid.
explicit operator bool() const { return this->isValid(); }
/// Trivially copyable, moveable and destructible
CommandPool(const CommandPool&) noexcept = default;
CommandPool& operator=(const CommandPool&) noexcept = default;
CommandPool(CommandPool&&) noexcept = default;
CommandPool& operator=(CommandPool&&) noexcept = default;
~CommandPool() = default;
private:
std::shared_ptr<VkCommandPool> commandPool;
};
}
#endif // COMMANDPOOL_HPP

69
include/core/fence.hpp Normal file
View file

@ -0,0 +1,69 @@
#ifndef FENCE_HPP
#define FENCE_HPP
#include "device.hpp"
#include <vulkan/vulkan_core.h>
#include <memory>
namespace Vulkan::Core {
///
/// C++ wrapper class for a Vulkan fence.
///
/// This class manages the lifetime of a Vulkan fence.
///
class Fence {
public:
///
/// Create the fence.
///
/// @param device Vulkan device
///
/// @throws std::invalid_argument if the device is null.
/// @throws ls::vulkan_error if object creation fails.
///
Fence(const Device& device);
///
/// Reset the fence to an unsignaled state.
///
/// @throws std::logic_error if the fence is not valid.
/// @throws ls::vulkan_error if resetting fails.
///
void reset() const;
///
/// Wait for the fence
///
/// @param timeout The timeout in nanoseconds, or UINT64_MAX for no timeout.
/// @returns true if the fence signaled, false if it timed out.
///
/// @throws std::logic_error if the fence is not valid.
/// @throws ls::vulkan_error if waiting fails.
///
bool wait(uint64_t timeout = UINT64_MAX);
/// Get the Vulkan handle.
[[nodiscard]] auto handle() const { return *this->fence; }
/// Check whether the object is valid.
[[nodiscard]] bool isValid() const { return static_cast<bool>(this->fence); }
/// if (obj) operator. Checks if the object is valid.
explicit operator bool() const { return this->isValid(); }
// Trivially copyable, moveable and destructible
Fence(const Fence&) noexcept = default;
Fence& operator=(const Fence&) noexcept = default;
Fence(Fence&&) noexcept = default;
Fence& operator=(Fence&&) noexcept = default;
~Fence() = default;
private:
std::shared_ptr<VkFence> fence;
VkDevice device{};
};
}
#endif // FENCE_HPP

View file

@ -0,0 +1,75 @@
#ifndef SEMAPHORE_HPP
#define SEMAPHORE_HPP
#include "device.hpp"
#include <vulkan/vulkan_core.h>
#include <optional>
#include <memory>
namespace Vulkan::Core {
///
/// C++ wrapper class for a Vulkan semaphore.
///
/// This class manages the lifetime of a Vulkan semaphore.
///
class Semaphore {
public:
///
/// Create the semaphore.
///
/// @param device Vulkan device
/// @param initial Optional initial value for creating a timeline semaphore.
///
/// @throws std::invalid_argument if the device is null.
/// @throws ls::vulkan_error if object creation fails.
///
Semaphore(const Device& device, std::optional<uint32_t> initial = std::nullopt);
///
/// Signal the semaphore to a specific value.
///
/// @param value The value to signal the semaphore to.
///
/// @throws std::logic_error if the semaphore is not a timeline semaphore.
/// @throws ls::vulkan_error if signaling fails.
///
void signal(uint64_t value) const;
///
/// Wait for the semaphore to reach a specific value.
///
/// @param value The value to wait for.
/// @param timeout The timeout in nanoseconds, or UINT64_MAX for no timeout.
/// @returns true if the semaphore reached the value, false if it timed out.
///
/// @throws std::logic_error if the semaphore is not a timeline semaphore.
/// @throws ls::vulkan_error if waiting fails.
///
bool wait(uint64_t value, uint64_t timeout = UINT64_MAX);
/// Get the Vulkan handle.
[[nodiscard]] auto handle() const { return *this->semaphore; }
/// Check whether the object is valid.
[[nodiscard]] bool isValid() const { return static_cast<bool>(this->semaphore); }
/// if (obj) operator. Checks if the object is valid.
explicit operator bool() const { return this->isValid(); }
// Trivially copyable, moveable and destructible
Semaphore(const Semaphore&) noexcept = default;
Semaphore& operator=(const Semaphore&) noexcept = default;
Semaphore(Semaphore&&) noexcept = default;
Semaphore& operator=(Semaphore&&) noexcept = default;
~Semaphore() = default;
private:
std::shared_ptr<VkSemaphore> semaphore;
VkDevice device{};
bool isTimeline{};
};
}
#endif // SEMAPHORE_HPP

View file

@ -0,0 +1,52 @@
#ifndef SHADERMODULE_HPP
#define SHADERMODULE_HPP
#include "device.hpp"
#include <string>
#include <vulkan/vulkan_core.h>
#include <memory>
namespace Vulkan::Core {
///
/// C++ wrapper class for a Vulkan shader module.
///
/// This class manages the lifetime of a Vulkan shader module.
///
class ShaderModule {
public:
///
/// Create the shader module.
///
/// @param device Vulkan device
/// @param path Path to the shader file.
///
/// @throws std::invalid_argument if the device is invalid.
/// @throws std::system_error if the shader file cannot be opened or read.
/// @throws ls::vulkan_error if object creation fails.
///
ShaderModule(const Device& device, const std::string& path);
/// Get the Vulkan handle.
[[nodiscard]] auto handle() const { return *this->shaderModule; }
/// Check whether the object is valid.
[[nodiscard]] bool isValid() const { return static_cast<bool>(this->shaderModule); }
/// if (obj) operator. Checks if the object is valid.
explicit operator bool() const { return this->isValid(); }
/// Trivially copyable, moveable and destructible
ShaderModule(const ShaderModule&) noexcept = default;
ShaderModule& operator=(const ShaderModule&) noexcept = default;
ShaderModule(ShaderModule&&) noexcept = default;
ShaderModule& operator=(ShaderModule&&) noexcept = default;
~ShaderModule() = default;
private:
std::shared_ptr<VkShaderModule> shaderModule;
};
}
#endif // SHADERMODULE_HPP

View file

@ -0,0 +1,37 @@
#ifndef EXCEPTIONS_HPP
#define EXCEPTIONS_HPP
#include <vulkan/vulkan_core.h>
#include <stdexcept>
#include <string>
namespace ls {
/// Simple exception class for Vulkan errors.
class vulkan_error : public std::runtime_error {
public:
///
/// Construct a vulkan_error with a message and a Vulkan result code.
///
/// @param result The Vulkan result code associated with the error.
/// @param message The error message.
///
explicit vulkan_error(VkResult result, const std::string& message);
/// Get the Vulkan result code associated with this error.
[[nodiscard]] VkResult error() const { return this->result; }
// Trivially copyable, moveable and destructible
vulkan_error(const vulkan_error&) = default;
vulkan_error(vulkan_error&&) = default;
vulkan_error& operator=(const vulkan_error&) = default;
vulkan_error& operator=(vulkan_error&&) = default;
~vulkan_error() noexcept override;
private:
VkResult result;
};
}
#endif // EXCEPTIONS_HPP

115
src/core/commandbuffer.cpp Normal file
View file

@ -0,0 +1,115 @@
#include "core/commandbuffer.hpp"
#include "core/semaphore.hpp"
#include "utils/exceptions.hpp"
using namespace Vulkan::Core;
CommandBuffer::CommandBuffer(const Device& device, const CommandPool& pool) {
if (!device || !pool)
throw std::invalid_argument("Invalid Vulkan device or command pool");
// create command buffer
const VkCommandBufferAllocateInfo desc = {
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
.commandPool = pool.handle(),
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
.commandBufferCount = 1
};
VkCommandBuffer commandBufferHandle{};
auto res = vkAllocateCommandBuffers(device.handle(), &desc, &commandBufferHandle);
if (res != VK_SUCCESS || commandBuffer == VK_NULL_HANDLE)
throw ls::vulkan_error(res, "Unable to allocate command buffer");
// store command buffer in shared ptr
this->state = std::make_shared<CommandBufferState>(CommandBufferState::Empty);
this->commandBuffer = std::shared_ptr<VkCommandBuffer>(
new VkCommandBuffer(commandBufferHandle),
[dev = device.handle(), pool = pool.handle()](VkCommandBuffer* cmdBuffer) {
vkFreeCommandBuffers(dev, pool, 1, cmdBuffer);
}
);
}
void CommandBuffer::begin() {
if (*this->state != CommandBufferState::Empty)
throw std::logic_error("Command buffer is not in Empty state");
const VkCommandBufferBeginInfo beginInfo = {
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT
};
auto res = vkBeginCommandBuffer(*this->commandBuffer, &beginInfo);
if (res != VK_SUCCESS)
throw ls::vulkan_error(res, "Unable to begin command buffer");
*this->state = CommandBufferState::Recording;
}
void CommandBuffer::end() {
if (*this->state != CommandBufferState::Recording)
throw std::logic_error("Command buffer is not in Recording state");
auto res = vkEndCommandBuffer(*this->commandBuffer);
if (res != VK_SUCCESS)
throw ls::vulkan_error(res, "Unable to end command buffer");
*this->state = CommandBufferState::Full;
}
void CommandBuffer::submit(VkQueue queue,
const std::vector<Semaphore>& waitSemaphores,
std::optional<std::vector<uint64_t>> waitSemaphoreValues,
const std::vector<Semaphore>& signalSemaphores,
std::optional<std::vector<uint64_t>> signalSemaphoreValues) {
if (!queue)
throw std::invalid_argument("Invalid Vulkan queue");
if (*this->state != CommandBufferState::Full)
throw std::logic_error("Command buffer is not in Full state");
const std::vector<VkPipelineStageFlags> waitStages(waitSemaphores.size(),
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT);
VkTimelineSemaphoreSubmitInfo timelineInfo = {
.sType = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO,
};
if (waitSemaphoreValues.has_value()) {
timelineInfo.waitSemaphoreValueCount =
static_cast<uint32_t>(waitSemaphoreValues->size());
timelineInfo.pWaitSemaphoreValues = waitSemaphoreValues->data();
}
if (signalSemaphoreValues.has_value()) {
timelineInfo.signalSemaphoreValueCount =
static_cast<uint32_t>(signalSemaphoreValues->size());
timelineInfo.pSignalSemaphoreValues = signalSemaphoreValues->data();
}
std::vector<VkSemaphore> waitSemaphoresHandles;
for (const auto& semaphore : waitSemaphores) {
if (!semaphore)
throw std::invalid_argument("Invalid Vulkan semaphore in waitSemaphores");
waitSemaphoresHandles.push_back(semaphore.handle());
}
std::vector<VkSemaphore> signalSemaphoresHandles;
for (const auto& semaphore : signalSemaphores) {
if (!semaphore)
throw std::invalid_argument("Invalid Vulkan semaphore in signalSemaphores");
signalSemaphoresHandles.push_back(semaphore.handle());
}
const VkSubmitInfo submitInfo = {
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
.pNext = (waitSemaphoreValues.has_value() || signalSemaphoreValues.has_value())
? &timelineInfo : nullptr,
.waitSemaphoreCount = static_cast<uint32_t>(waitSemaphores.size()),
.pWaitSemaphores = waitSemaphoresHandles.data(),
.pWaitDstStageMask = waitStages.data(),
.commandBufferCount = 1,
.pCommandBuffers = &(*this->commandBuffer),
.signalSemaphoreCount = static_cast<uint32_t>(signalSemaphores.size()),
.pSignalSemaphores = signalSemaphoresHandles.data()
};
auto res = vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE);
if (res != VK_SUCCESS)
throw ls::vulkan_error(res, "Unable to submit command buffer");
*this->state = CommandBufferState::Submitted;
}

34
src/core/commandpool.cpp Normal file
View file

@ -0,0 +1,34 @@
#include "core/commandpool.hpp"
#include "utils/exceptions.hpp"
using namespace Vulkan::Core;
CommandPool::CommandPool(const Device& device, CommandPoolType type) {
if (!device)
throw std::invalid_argument("Invalid Vulkan device");
uint32_t familyIdx{};
switch (type) {
case CommandPoolType::Compute:
familyIdx = device.getComputeFamilyIdx();
break;
}
// create command pool
const VkCommandPoolCreateInfo desc = {
.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
.queueFamilyIndex = familyIdx
};
VkCommandPool commandPoolHandle{};
auto res = vkCreateCommandPool(device.handle(), &desc, nullptr, &commandPoolHandle);
if (res != VK_SUCCESS || commandPoolHandle == VK_NULL_HANDLE)
throw ls::vulkan_error(res, "Unable to create command pool");
// store the command pool in a shared pointer
this->commandPool = std::shared_ptr<VkCommandPool>(
new VkCommandPool(commandPoolHandle),
[dev = device.handle()](VkCommandPool* commandPoolHandle) {
vkDestroyCommandPool(dev, *commandPoolHandle, nullptr);
}
);
}

50
src/core/fence.cpp Normal file
View file

@ -0,0 +1,50 @@
#include "core/fence.hpp"
#include "utils/exceptions.hpp"
#include <vulkan/vulkan_core.h>
using namespace Vulkan::Core;
Fence::Fence(const Device& device) {
if (!device)
throw std::invalid_argument("Invalid Vulkan device");
// create fence
const VkFenceCreateInfo desc = {
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO
};
VkFence fenceHandle{};
auto res = vkCreateFence(device.handle(), &desc, nullptr, &fenceHandle);
if (res != VK_SUCCESS || fenceHandle == VK_NULL_HANDLE)
throw ls::vulkan_error(res, "Unable to create fence");
// store fence in shared ptr
this->device = device.handle();
this->fence = std::shared_ptr<VkFence>(
new VkFence(fenceHandle),
[dev = device.handle()](VkFence* fenceHandle) {
vkDestroyFence(dev, *fenceHandle, nullptr);
}
);
}
void Fence::reset() const {
if (!this->isValid())
throw std::runtime_error("Invalid fence");
VkFence fenceHandle = this->handle();
auto res = vkResetFences(this->device, 1, &fenceHandle);
if (res != VK_SUCCESS)
throw ls::vulkan_error(res, "Unable to reset fence");
}
bool Fence::wait(uint64_t timeout) {
if (!this->isValid())
throw std::runtime_error("Invalid fence");
VkFence fenceHandle = this->handle();
auto res = vkWaitForFences(this->device, 1, &fenceHandle, VK_TRUE, timeout);
if (res != VK_SUCCESS && res != VK_TIMEOUT)
throw ls::vulkan_error(res, "Unable to wait for fence");
return res == VK_SUCCESS;
}

66
src/core/semaphore.cpp Normal file
View file

@ -0,0 +1,66 @@
#include "core/semaphore.hpp"
#include "utils/exceptions.hpp"
using namespace Vulkan::Core;
Semaphore::Semaphore(const Device& device, std::optional<uint32_t> initial) {
if (!device)
throw std::invalid_argument("Invalid Vulkan device");
// create semaphore
const VkSemaphoreTypeCreateInfo typeInfo{
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO,
.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE,
.initialValue = initial.value_or(0)
};
const VkSemaphoreCreateInfo desc = {
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
.pNext = initial.has_value() ? &typeInfo : nullptr,
};
VkSemaphore semaphoreHandle{};
auto res = vkCreateSemaphore(device.handle(), &desc, nullptr, &semaphoreHandle);
if (res != VK_SUCCESS || semaphoreHandle == VK_NULL_HANDLE)
throw ls::vulkan_error(res, "Unable to create semaphore");
// store semaphore in shared ptr
this->isTimeline = initial.has_value();
this->device = device.handle();
this->semaphore = std::shared_ptr<VkSemaphore>(
new VkSemaphore(semaphoreHandle),
[dev = device.handle()](VkSemaphore* semaphoreHandle) {
vkDestroySemaphore(dev, *semaphoreHandle, nullptr);
}
);
}
void Semaphore::signal(uint64_t value) const {
if (!this->isValid() || !this->isTimeline)
throw std::runtime_error("Invalid timeline semaphore");
const VkSemaphoreSignalInfo signalInfo{
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO,
.semaphore = this->handle(),
.value = value
};
auto res = vkSignalSemaphore(this->device, &signalInfo);
if (res != VK_SUCCESS)
throw ls::vulkan_error(res, "Unable to signal semaphore");
}
bool Semaphore::wait(uint64_t value, uint64_t timeout) {
if (!this->isValid() || !this->isTimeline)
throw std::runtime_error("Invalid timeline semaphore");
VkSemaphore semaphore = this->handle();
const VkSemaphoreWaitInfo waitInfo{
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO,
.semaphoreCount = 1,
.pSemaphores = &semaphore,
.pValues = &value
};
auto res = vkWaitSemaphores(this->device, &waitInfo, timeout);
if (res != VK_SUCCESS && res != VK_TIMEOUT)
throw ls::vulkan_error(res, "Unable to wait for semaphore");
return res == VK_SUCCESS;
}

46
src/core/shadermodule.cpp Normal file
View file

@ -0,0 +1,46 @@
#include "core/shadermodule.hpp"
#include "utils/exceptions.hpp"
#include <fstream>
#include <vector>
using namespace Vulkan::Core;
ShaderModule::ShaderModule(const Device& device, const std::string& path) {
if (!device)
throw std::invalid_argument("Invalid Vulkan device");
// read shader bytecode
std::ifstream file(path, std::ios::ate | std::ios::binary);
if (!file)
throw std::system_error(errno, std::generic_category(), "Failed to open shader file: " + path);
const std::streamsize size = file.tellg();
std::vector<uint8_t> code(static_cast<size_t>(size));
file.seekg(0, std::ios::beg);
if (!file.read(reinterpret_cast<char*>(code.data()), size))
throw std::system_error(errno, std::generic_category(), "Failed to read shader file: " + path);
file.close();
// create shader module
const uint8_t* data_ptr = code.data();
const VkShaderModuleCreateInfo createInfo{
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
.codeSize = code.size() * sizeof(uint32_t),
.pCode = reinterpret_cast<const uint32_t*>(data_ptr)
};
VkShaderModule shaderModuleHandle{};
auto res = vkCreateShaderModule(device.handle(), &createInfo, nullptr, &shaderModuleHandle);
if (res != VK_SUCCESS || !shaderModuleHandle)
throw ls::vulkan_error(res, "Failed to create shader module");
// store shader module in shared ptr
this->shaderModule = std::shared_ptr<VkShaderModule>(
new VkShaderModule(shaderModuleHandle),
[dev = device.handle()](VkShaderModule* shaderModuleHandle) {
vkDestroyShaderModule(dev, *shaderModuleHandle, nullptr);
}
);
}

10
src/utils/exceptions.cpp Normal file
View file

@ -0,0 +1,10 @@
#include "utils/exceptions.hpp"
#include <format>
using namespace ls;
vulkan_error::vulkan_error(VkResult result, const std::string& message)
: std::runtime_error(std::format("{} (error {})", message, static_cast<uint32_t>(result))), result(result) {}
vulkan_error::~vulkan_error() noexcept = default;