#include #include "destrum/Graphics/Util.h" #define VOLK_IMPLEMENTATION #include #define VMA_IMPLEMENTATION #include #include #include #include #include #include #include "destrum/Graphics/imageLoader.h" #include "destrum/Util/GameState.h" #include "spdlog/spdlog.h" GfxDevice::GfxDevice(): imageCache(*this) { } void GfxDevice::init(SDL_Window* window, const std::string& appName, bool vSync) { VK_CHECK(volkInitialize()); instance = vkb::InstanceBuilder{} .set_app_name(appName.c_str()) .set_app_version(1, 0, 0) .request_validation_layers() .use_default_debug_messenger() .require_api_version(1, 3, 0) .build() .value(); volkLoadInstance(instance); const auto res = SDL_Vulkan_CreateSurface(window, instance, &surface); if (res != SDL_TRUE) { spdlog::error("Failed to create Vulkan surface: {}", SDL_GetError()); std::exit(1); } constexpr auto deviceFeatures = VkPhysicalDeviceFeatures{ .imageCubeArray = VK_TRUE, .geometryShader = VK_TRUE, // for im3d .depthClamp = VK_TRUE, .fillModeNonSolid = VK_TRUE, .samplerAnisotropy = VK_TRUE }; constexpr auto features12 = VkPhysicalDeviceVulkan12Features{ .descriptorIndexing = true, .descriptorBindingSampledImageUpdateAfterBind = true, .descriptorBindingStorageImageUpdateAfterBind = true, .descriptorBindingPartiallyBound = true, .descriptorBindingVariableDescriptorCount = true, .runtimeDescriptorArray = true, .scalarBlockLayout = true, .bufferDeviceAddress = true, }; constexpr auto features13 = VkPhysicalDeviceVulkan13Features{ .synchronization2 = true, .dynamicRendering = true, }; physicalDevice = vkb::PhysicalDeviceSelector{instance} .set_minimum_version(1, 3) .set_required_features(deviceFeatures) .set_required_features_12(features12) .set_required_features_13(features13) .add_required_extension(VK_EXT_EXTENDED_DYNAMIC_STATE_3_EXTENSION_NAME) .set_surface(surface) .prefer_gpu_device_type(vkb::PreferredDeviceType::discrete) .select() .value(); device = vkb::DeviceBuilder{physicalDevice}.build().value(); volkLoadDevice(device); graphicsQueueFamily = device.get_queue_index(vkb::QueueType::graphics).value(); graphicsQueue = device.get_queue(vkb::QueueType::graphics).value(); //Vma const auto vulkanFunctions = VmaVulkanFunctions{ .vkGetInstanceProcAddr = vkGetInstanceProcAddr, .vkGetDeviceProcAddr = vkGetDeviceProcAddr, }; const auto allocatorInfo = VmaAllocatorCreateInfo{ .flags = VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT, .physicalDevice = physicalDevice, .device = device, .pVulkanFunctions = &vulkanFunctions, .instance = instance, }; vmaCreateAllocator(&allocatorInfo, &allocator); executor.init(device, graphicsQueueFamily, graphicsQueue); int w, h; SDL_GetWindowSize(window, &w, &h); swapchainFormat = VK_FORMAT_B8G8R8A8_SRGB; swapchain.createSwapchain(this, swapchainFormat, w, h, vSync); VkPhysicalDeviceProperties props{}; vkGetPhysicalDeviceProperties(physicalDevice, &props); imageCache.bindlessSetManager.init(device, props.limits.maxSamplerAnisotropy); swapchain.initSync(device); const auto poolCreateInfo = vkinit::commandPoolCreateInfo(VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, graphicsQueueFamily); for (std::uint32_t i = 0; i < FRAMES_IN_FLIGHT; ++i) { auto& commandPool = frames[i].commandPool; VK_CHECK(vkCreateCommandPool(device, &poolCreateInfo, nullptr, &commandPool)); const auto cmdAllocInfo = vkinit::commandBufferAllocateInfo(commandPool, 1); auto& mainCommandBuffer = frames[i].commandBuffer; VK_CHECK(vkAllocateCommandBuffers(device, &cmdAllocInfo, &mainCommandBuffer)); } { // create white texture std::uint32_t pixel = 0xFFFFFFFF; whiteImageId = createImage( { .format = VK_FORMAT_R8G8B8A8_UNORM, .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, .extent = VkExtent3D{1, 1, 1}, }, "white texture", &pixel); } { // create error texture (black/magenta checker) constexpr auto black = 0xFF000000; constexpr auto magenta = 0xFFFF00FF; std::array pixels{black, magenta, magenta, black}; errorImageId = createImage( { .format = VK_FORMAT_R8G8B8A8_UNORM, .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, .extent = VkExtent3D{2, 2, 1}, }, "error texture", pixels.data()); imageCache.setErrorImageId(errorImageId); } GameState::GetInstance().SetGfxDevice(this); } void GfxDevice::recreateSwapchain(int width, int height) { assert(width != 0 && height != 0); waitIdle(); swapchain.recreateSwapchain(*this, swapchainFormat, width, height, true); } VkCommandBuffer GfxDevice::beginFrame() { swapchain.beginFrame(getCurrentFrameIndex()); const auto& frame = getCurrentFrame(); const auto& cmd = frame.commandBuffer; const auto cmdBeginInfo = VkCommandBufferBeginInfo{ .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, }; VK_CHECK(vkBeginCommandBuffer(cmd, &cmdBeginInfo)); return cmd; } VulkanImmediateExecutor& GfxDevice::GetImmediateExecuter() { return executor; } void GfxDevice::endFrame(VkCommandBuffer cmd, const GPUImage& drawImage, const EndFrameProps& props) { // get swapchain image const auto [swapchainImage, swapchainImageIndex] = swapchain.acquireNextImage(getCurrentFrameIndex()); if (swapchainImage == VK_NULL_HANDLE) { spdlog::info("Swapchain is freaky, skipping frame..."); return; } // Fences are reset here to prevent the deadlock in case swapchain becomes dirty swapchain.resetFences(getCurrentFrameIndex()); auto swapchainLayout = VK_IMAGE_LAYOUT_UNDEFINED; { const VkImageSubresourceRange clearRange = vkinit::imageSubresourceRange(VK_IMAGE_ASPECT_COLOR_BIT); vkutil::transitionImage(cmd, swapchainImage, swapchainLayout, VK_IMAGE_LAYOUT_GENERAL); swapchainLayout = VK_IMAGE_LAYOUT_GENERAL; const auto clearValue = props.clearColor; vkCmdClearColorImage(cmd, swapchainImage, VK_IMAGE_LAYOUT_GENERAL, &clearValue, 1, &clearRange); } if (true) { // copy from draw image into swapchain vkutil::transitionImage( cmd, drawImage.image, VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); vkutil::transitionImage( cmd, swapchainImage, swapchainLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); swapchainLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; auto filter = false ? VK_FILTER_LINEAR : VK_FILTER_NEAREST; filter = VK_FILTER_NEAREST; if (false) { vkutil::copyImageToImage( cmd, drawImage.image, swapchainImage, drawImage.getExtent2D(), props.drawImageBlitRect.x, props.drawImageBlitRect.y, props.drawImageBlitRect.z, props.drawImageBlitRect.w, filter); } else { // will stretch image to swapchain vkutil::copyImageToImage( cmd, drawImage.image, swapchainImage, drawImage.getExtent2D(), getSwapchainExtent(), filter); } } // prepare for present // vkutil::transitionImage(cmd, swapchainImage, swapchainLayout, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); // swapchainLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; if (props.imguiPass) { props.imguiPass->render( cmd, swapchainImage, getSwapchainImageView(static_cast(swapchainImageIndex)), swapchainLayout, getSwapchainExtent() ); } // prepare for present vkutil::transitionImage(cmd, swapchainImage, swapchainLayout, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); swapchainLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; VK_CHECK(vkEndCommandBuffer(cmd)); // swapchain.submitAndPresent(cmd, graphicsQueue, getCurrentFrameIndex(), swapchainImageIndex); swapchain.submitAndPresent(cmd, graphicsQueue, swapchainImageIndex, getCurrentFrameIndex()); frameNumber++; } void GfxDevice::cleanup() { } void GfxDevice::waitIdle() { VK_CHECK(vkDeviceWaitIdle(device)); } BindlessSetManager& GfxDevice::getBindlessSetManager() { return imageCache.bindlessSetManager; } VkDescriptorSetLayout GfxDevice::getBindlessDescSetLayout() const { return imageCache.bindlessSetManager.getDescSetLayout(); } const VkDescriptorSet& GfxDevice::getBindlessDescSet() const { return imageCache.bindlessSetManager.getDescSet(); } void GfxDevice::bindBindlessDescSet(VkCommandBuffer cmd, VkPipelineLayout layout) const { vkCmdBindDescriptorSets( cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, 1, &imageCache.bindlessSetManager.getDescSet(), 0, nullptr); } void GfxDevice::immediateSubmit(ImmediateExecuteFunction&& f) const { executor.immediateSubmit(std::move(f)); } GPUBuffer GfxDevice::createBuffer( std::size_t allocSize, VkBufferUsageFlags usage, VmaMemoryUsage memoryUsage) const { const auto bufferInfo = VkBufferCreateInfo{ .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = allocSize, .usage = usage, }; const auto allocInfo = VmaAllocationCreateInfo{ .flags = VMA_ALLOCATION_CREATE_MAPPED_BIT | // TODO: allow to set VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT when needed VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT, .usage = memoryUsage, }; GPUBuffer buffer{}; VK_CHECK(vmaCreateBuffer(allocator, &bufferInfo, &allocInfo, &buffer.buffer, &buffer.allocation, &buffer.info)); if ((usage & VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT) != 0) { const auto deviceAdressInfo = VkBufferDeviceAddressInfo{ .sType = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, .buffer = buffer.buffer, }; buffer.address = vkGetBufferDeviceAddress(device, &deviceAdressInfo); } return buffer; } VkDeviceAddress GfxDevice::getBufferAddress(const GPUBuffer& buffer) const { const auto deviceAdressInfo = VkBufferDeviceAddressInfo{ .sType = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO, .buffer = buffer.buffer, }; return vkGetBufferDeviceAddress(device, &deviceAdressInfo); } void GfxDevice::destroyBuffer(const GPUBuffer& buffer) const { vmaDestroyBuffer(allocator, buffer.buffer, buffer.allocation); } // // GPUImage GfxDevice::loadImageFromFileRaw( // const std::filesystem::path& path, // VkFormat format, // VkImageUsageFlags usage, // bool mipMap) const // { // auto data = util::loadImage(path); // if (!data.pixels) { // fmt::println("[error] failed to load image from '{}'", path.string()); // return getImage(errorImageId); // } // // auto image = createImageRaw({ // .format = format, // .usage = usage | // // VK_IMAGE_USAGE_TRANSFER_DST_BIT | // for uploading pixel data to image // VK_IMAGE_USAGE_TRANSFER_SRC_BIT, // for generating mips // .extent = // VkExtent3D{ // .width = (std::uint32_t)data.width, // .height = (std::uint32_t)data.height, // .depth = 1, // }, // .mipMap = mipMap, // }); // uploadImageData(image, data.pixels); // // image.debugName = path.string(); // vkutil::addDebugLabel(device, image.image, path.string().c_str()); // // return image; // } ImageID GfxDevice::createImage( const vkutil::CreateImageInfo& createInfo, const std::string& debugName, void* pixelData, ImageID imageId) { auto image = createImageRaw(createInfo); if (!debugName.empty()) { vkutil::addDebugLabel(device, image.image, debugName.c_str()); image.debugName = debugName; } if (pixelData) { const std::size_t bytes = std::size_t(image.extent.width) * std::size_t(image.extent.height) * std::size_t(image.extent.depth) * BytesPerTexel(image.format); uploadImageDataSized(image, pixelData, bytes, 0); } if (imageId != NULL_IMAGE_ID) { return imageCache.addImage(imageId, std::move(image)); } else { return addImageToCache(std::move(image)); } } ImageID GfxDevice::createDrawImage( VkFormat format, glm::ivec2 size, const std::string& debugName, ImageID imageId) { assert(size.x > 0 && size.y > 0); const auto extent = VkExtent3D{ .width = (std::uint32_t)size.x, .height = (std::uint32_t)size.y, .depth = 1, }; VkImageUsageFlags usages{}; usages |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; usages |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; usages |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; usages |= VK_IMAGE_USAGE_SAMPLED_BIT; const auto createImageInfo = vkutil::CreateImageInfo{ .format = format, .usage = usages, .extent = extent, }; return createImage(createImageInfo, debugName, nullptr, imageId); } ImageID GfxDevice::loadImageFromFile(const std::filesystem::path& path, VkImageUsageFlags usage, bool mipMap, TextureIntent intent) { return imageCache.loadImageFromFile(path, usage, mipMap, intent); } const GPUImage& GfxDevice::getImage(ImageID id) const { return imageCache.getImage(id); } ImageID GfxDevice::addImageToCache(GPUImage img) { return imageCache.addImage(std::move(img)); } GPUImage GfxDevice::createImageRaw( const vkutil::CreateImageInfo& createInfo, std::optional customAllocationCreateInfo) const { std::uint32_t mipLevels = 1; if (createInfo.mipMap) { const auto maxExtent = std::max(createInfo.extent.width, createInfo.extent.height); mipLevels = (std::uint32_t)std::floor(std::log2(maxExtent)) + 1; } if (createInfo.isCubemap) { assert(createInfo.numLayers % 6 == 0); // assert(!createInfo.mipMap); assert((createInfo.flags & VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT) != 0); } auto imgInfo = VkImageCreateInfo{ .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .flags = createInfo.flags, .imageType = VK_IMAGE_TYPE_2D, .format = createInfo.format, .extent = createInfo.extent, .mipLevels = mipLevels, .arrayLayers = createInfo.numLayers, .samples = createInfo.samples, .tiling = createInfo.tiling, .usage = createInfo.usage, }; static const auto defaultAllocInfo = VmaAllocationCreateInfo{ .usage = VMA_MEMORY_USAGE_AUTO, .requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, }; const auto allocInfo = customAllocationCreateInfo.has_value() ? customAllocationCreateInfo.value() : defaultAllocInfo; GPUImage image{}; image.format = createInfo.format; image.usage = createInfo.usage; image.extent = createInfo.extent; image.mipLevels = mipLevels; image.numLayers = createInfo.numLayers; image.isCubemap = createInfo.isCubemap; VK_CHECK(vmaCreateImage(allocator, &imgInfo, &allocInfo, &image.image, &image.allocation, nullptr)); // create view only when usage flags allow it bool shouldCreateView = ((createInfo.usage & VK_IMAGE_USAGE_SAMPLED_BIT) != 0) || ((createInfo.usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0) || ((createInfo.usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) != 0) || ((createInfo.usage & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0); if (shouldCreateView) { VkImageAspectFlags aspectFlag = VK_IMAGE_ASPECT_COLOR_BIT; if (createInfo.format == VK_FORMAT_D32_SFLOAT) { // TODO: support other depth formats aspectFlag = VK_IMAGE_ASPECT_DEPTH_BIT; } auto viewType = createInfo.numLayers == 1 ? VK_IMAGE_VIEW_TYPE_2D : VK_IMAGE_VIEW_TYPE_2D_ARRAY; if (createInfo.isCubemap && createInfo.numLayers == 6) { viewType = VK_IMAGE_VIEW_TYPE_CUBE; } const auto viewCreateInfo = VkImageViewCreateInfo{ .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = image.image, .viewType = viewType, .format = createInfo.format, .subresourceRange = VkImageSubresourceRange{ .aspectMask = aspectFlag, .baseMipLevel = 0, .levelCount = mipLevels, .baseArrayLayer = 0, .layerCount = createInfo.numLayers, }, }; VK_CHECK(vkCreateImageView(device, &viewCreateInfo, nullptr, &image.imageView)); } return image; } GPUImage GfxDevice::loadImageFromFileRaw(const std::filesystem::path& path, VkImageUsageFlags usage, bool mipMap, TextureIntent intent) const { const auto data = util::loadImage(path, intent); if (data.vkFormat == VK_FORMAT_UNDEFINED || data.byteSize == 0 || (data.hdr ? (data.hdrPixels == nullptr) : (data.pixels == nullptr))) { spdlog::error("Failed to load image '{}'", path.string()); return getImage(errorImageId); } auto image = createImageRaw({ .format = data.vkFormat, .usage = usage | VK_IMAGE_USAGE_TRANSFER_DST_BIT | (mipMap ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0), .extent = VkExtent3D{ .width = static_cast(data.width), .height = static_cast(data.height), .depth = 1, }, .mipMap = mipMap, }); const void* src = data.hdr ? static_cast(data.hdrPixels) : static_cast(data.pixels); uploadImageDataSized(image, src, data.byteSize, 0); image.debugName = path.string(); vkutil::addDebugLabel(device, image.image, path.string().c_str()); return image; } // void GfxDevice::uploadImageData(const GPUImage& image, void* pixelData, std::uint32_t layer) const { // VkDeviceSize dataSize = // VkDeviceSize(image.extent.depth) * // image.extent.width * // image.extent.height * // BytesPerTexel(image.format); // // auto uploadBuffer = createBuffer(dataSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); // memcpy(uploadBuffer.info.pMappedData, pixelData, size_t(dataSize)); // // executor.immediateSubmit([&] (VkCommandBuffer cmd) { // assert( // (image.usage & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0 && // "Image needs to have VK_IMAGE_USAGE_TRANSFER_DST_BIT to upload data to it"); // vkutil::transitionImage( // cmd, image.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); // // const auto copyRegion = VkBufferImageCopy{ // .bufferOffset = 0, // .bufferRowLength = 0, // .bufferImageHeight = 0, // .imageSubresource = // { // .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, // .mipLevel = 0, // .baseArrayLayer = layer, // .layerCount = 1, // }, // .imageExtent = image.extent, // }; // // vkCmdCopyBufferToImage( // cmd, // uploadBuffer.buffer, // image.image, // VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, // 1, // ©Region); // // if (image.mipLevels > 1) { // assert( // (image.usage & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0 && // (image.usage & VK_IMAGE_USAGE_TRANSFER_SRC_BIT) != 0 && // "Image needs to have VK_IMAGE_USAGE_TRANSFER_{DST,SRC}_BIT to generate mip maps"); // // graphics::generateMipmaps( // // cmd, // // image.image, // // VkExtent2D{image.extent.width, image.extent.height}, // // image.mipLevels); // spdlog::warn("Yea dawg, i ain't written this yet :pray:"); // } else { // vkutil::transitionImage( // cmd, // image.image, // VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, // VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); // } // }); // // destroyBuffer(uploadBuffer); // } void GfxDevice::uploadImageDataSized(const GPUImage& image, const void* pixelData, std::size_t byteSize, std::uint32_t layer) const { auto uploadBuffer = createBuffer(byteSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU); // safety checks assert(uploadBuffer.info.pMappedData); assert(pixelData); assert(byteSize > 0); std::memcpy(uploadBuffer.info.pMappedData, pixelData, byteSize); executor.immediateSubmit([&] (VkCommandBuffer cmd) { vkutil::transitionImage(cmd, image.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); VkBufferImageCopy copyRegion{}; copyRegion.bufferOffset = 0; copyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; copyRegion.imageSubresource.mipLevel = 0; copyRegion.imageSubresource.baseArrayLayer = layer; copyRegion.imageSubresource.layerCount = 1; copyRegion.imageExtent = image.extent; vkCmdCopyBufferToImage(cmd, uploadBuffer.buffer, image.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©Region); vkutil::transitionImage(cmd, image.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); }); destroyBuffer(uploadBuffer); } // GPUImage GfxDevice::loadImageFromFileRaw(const std::filesystem::path& path, VkImageUsageFlags usage, bool mipMap) const { // const auto data = util::loadImage(path); // const bool isHdr = data.hdr && data.hdrPixels; // const bool isLdr = !data.hdr && data.pixels; // // if (!isHdr && !isLdr || data.vkFormat == VK_FORMAT_UNDEFINED || data.byteSize == 0) { // spdlog::error("failed to load image from '{}'", path.string()); // return getImage(errorImageId); // } // // auto image = createImageRaw({ // .format = data.vkFormat, // .usage = usage | // VK_IMAGE_USAGE_TRANSFER_DST_BIT | // (mipMap ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0), // .extent = VkExtent3D{ // .width = (std::uint32_t)data.width, // .height = (std::uint32_t)data.height, // .depth = 1, // }, // .mipMap = mipMap, // }); // // const void* src = isHdr ? (const void*)data.hdrPixels : (const void*)data.pixels; // uploadImageDataSized(image, src, data.byteSize, 0); // // image.debugName = path.string(); // vkutil::addDebugLabel(device, image.image, path.string().c_str()); // return image; // } void GfxDevice::destroyImage(const GPUImage& image) const { vkDestroyImageView(device, image.imageView, nullptr); vmaDestroyImage(allocator, image.image, image.allocation); // TODO: if image has bindless id, update the set }