I am building a 3d renderer in C++ using Vulkan.hpp. I have a Cubemap class that I am currently using to handle skybox textures. I am in the process of expanding the functionality of this class to also handle point shadow map images. To do this I have created an IntitializeBlankImages function to create blank images to the be written to in the point shadow mapping rendering pipeline. At the end of this function I am calling the TransitionImageLayout function to ensure the images are in the correct format to write to. I now need to update this function to handle this new layout transition. Can you confirm the layouts that are being transitioned are correct for my purposes? Can you also update the TransitionImageLayout function to handle the appropriate image layout transitions?

Here is the code for the Cubemap class:

Cubemap.h:

#pragma once

#include <vulkan/vulkan.hpp>
#include <array>
#include <string>

class Cubemap
{
public:
    Cubemap();
    ~Cubemap();

    void LoadFromFiles(const std::array<std::string, 6>& filenames, vk::Device device, vk::PhysicalDevice physicalDevice, vk::CommandPool commandPool, vk::Queue graphicsQueue);
    void InitializeBlankImages(vk::Device device, vk::PhysicalDevice physicalDevice, vk::CommandPool commandPool, vk::Queue graphicsQueue, int width, int height);
    vk::ImageView GetImageView() const;
    vk::Sampler GetSampler() const;
    void Cleanup();

private:
    vk::Device device;
    vk::Image image;
    vk::DeviceMemory imageMemory;
    vk::ImageView imageView;
    vk::Sampler sampler;
    vk::PhysicalDevice physicalDevice;
    vk::CommandPool commandPool;
    vk::Queue graphicsQueue;
    bool initialized = false;

    void CreateImage(uint32_t width, uint32_t height, uint32_t mipLevels, vk::SampleCountFlagBits numSamples, vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties);
    void TransitionImageLayout(vk::ImageLayout oldLayout, vk::ImageLayout newLayout, vk::ImageSubresourceRange subresourceRange);
    void CopyBufferToImage(vk::Buffer buffer, uint32_t width, uint32_t height, int layerIndex);
    void CreateImageView(vk::Format format, vk::ImageAspectFlags aspectFlags, uint32_t mipLevels);
    void CreateSampler(uint32_t mipLevels);
    vk::CommandBuffer BeginSingleTimeCommands(vk::CommandPool commandPool, vk::Device device);
    void EndSingleTimeCommands(vk::CommandPool commandPool, vk::Queue graphicsQueue, vk::Device device, vk::CommandBuffer commandBuffer);
};


Cubemap.cpp:

#include "Cubemap.h"
#include "stb_image.h"
#include "BufferUtils.h"

Cubemap::Cubemap()
    : device(), image(), imageMemory(), imageView(), sampler()
{
}

Cubemap::~Cubemap()
{
    Cleanup();
}

void Cubemap::LoadFromFiles(const std::array<std::string, 6>& filenames, vk::Device device, vk::PhysicalDevice physicalDevice, vk::CommandPool commandPool, vk::Queue graphicsQueue)
{
    this->device = device;
    this->physicalDevice = physicalDevice;
    this->commandPool = commandPool;
    this->graphicsQueue = graphicsQueue;
    this->initialized = true;

    int width, height, channels;
    stbi_uc* pixels = stbi_load(filenames[0].c_str(), &width, &height, &channels, STBI_rgb_alpha);

    if (!pixels)
    {
        throw std::runtime_error("Failed to load cubemap face!");
    }

    uint32_t mipLevels = 1;

    vk::DeviceSize imageSize = width * height * 4;
    std::vector<vk::Buffer> stagingBuffers(6);
    std::vector<vk::DeviceMemory> stagingBufferMemories(6);

    for (int i = 0; i < 6; ++i)
    {
        stbi_uc* facePixels = stbi_load(filenames[i].c_str(), &width, &height, &channels, STBI_rgb_alpha);
        if (!facePixels)
        {
            throw std::runtime_error("Failed to load cubemap face!");
        }

        BufferUtils::CreateBuffer(device, physicalDevice, imageSize, vk::BufferUsageFlagBits::eTransferSrc, vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, stagingBuffers[i], stagingBufferMemories[i]);

        void* bufferData;
        device.mapMemory(stagingBufferMemories[i], 0, imageSize, {}, &bufferData);
        memcpy(bufferData, facePixels, static_cast<size_t>(imageSize));
        device.unmapMemory(stagingBufferMemories[i]);
        stbi_image_free(facePixels);
    }

    CreateImage(width, height, mipLevels, vk::SampleCountFlagBits::e1, vk::Format::eR8G8B8A8Srgb, vk::ImageTiling::eOptimal,
        vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled, vk::MemoryPropertyFlagBits::eDeviceLocal);

    CreateImageView(vk::Format::eR8G8B8A8Srgb, vk::ImageAspectFlagBits::eColor, mipLevels);
    CreateSampler(mipLevels);

    vk::ImageSubresourceRange subresourceRange{};
    subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor;
    subresourceRange.baseMipLevel = 0;
    subresourceRange.levelCount = mipLevels;
    subresourceRange.baseArrayLayer = 0;
    subresourceRange.layerCount = 6;

    TransitionImageLayout(vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal, subresourceRange);

    for (int i = 0; i < 6; ++i)
    {
        CopyBufferToImage(stagingBuffers[i], width, height, i);
    }

    TransitionImageLayout(vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal, subresourceRange);

    for (int i = 0; i < 6; ++i)
    {
        device.destroyBuffer(stagingBuffers[i], nullptr);
        device.freeMemory(stagingBufferMemories[i], nullptr);
    }
}

// New method to initialize blank images for shadows
void Cubemap::InitializeBlankImages(vk::Device device, vk::PhysicalDevice physicalDevice, vk::CommandPool commandPool, vk::Queue graphicsQueue, int width, int height)
{
    this->device = device;
    this->physicalDevice = physicalDevice;
    this->commandPool = commandPool;
    this->graphicsQueue = graphicsQueue;
    this->initialized = true;

    //uint32_t width = width; // Standard size for shadow maps, adjust as needed
    //uint32_t height = height;
    uint32_t mipLevels = 1;

    CreateImage(width, height, mipLevels, vk::SampleCountFlagBits::e1, vk::Format::eD32Sfloat, vk::ImageTiling::eOptimal,
        vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eSampled, vk::MemoryPropertyFlagBits::eDeviceLocal);

    CreateImageView(vk::Format::eD32Sfloat, vk::ImageAspectFlagBits::eDepth, mipLevels);
    CreateSampler(mipLevels);

    vk::ImageSubresourceRange subresourceRange{};
    subresourceRange.aspectMask = vk::ImageAspectFlagBits::eDepth;
    subresourceRange.baseMipLevel = 0;
    subresourceRange.levelCount = mipLevels;
    subresourceRange.baseArrayLayer = 0;
    subresourceRange.layerCount = 6;

    TransitionImageLayout(vk::ImageLayout::eUndefined, vk::ImageLayout::eDepthStencilAttachmentOptimal, subresourceRange);
}

void Cubemap::Cleanup()
{
    if (!initialized) {
        return;
    }

    if (sampler) {
        device.destroySampler(sampler, nullptr);
        sampler = nullptr;
    }

    if (imageView) {
        device.destroyImageView(imageView, nullptr);
        imageView = nullptr;
    }

    if (image) {
        device.destroyImage(image, nullptr);
        image = nullptr;
    }

    if (imageMemory) {
        device.freeMemory(imageMemory, nullptr);
        imageMemory = nullptr;
    }

    initialized = false;
}

vk::ImageView Cubemap::GetImageView() const
{
    return imageView;
}

vk::Sampler Cubemap::GetSampler() const
{
    return sampler;
}

void Cubemap::CreateImage(uint32_t width, uint32_t height, uint32_t mipLevels, vk::SampleCountFlagBits numSamples, vk::Format format, vk::ImageTiling tiling, vk::ImageUsageFlags usage, vk::MemoryPropertyFlags properties)
{
    vk::ImageCreateInfo imageInfo{};
    imageInfo.imageType = vk::ImageType::e2D;
    imageInfo.extent.width = width;
    imageInfo.extent.height = height;
    imageInfo.extent.depth = 1;
    imageInfo.mipLevels = mipLevels;
    imageInfo.arrayLayers = 6;
    imageInfo.format = format;
    imageInfo.tiling = tiling;
    imageInfo.initialLayout = vk::ImageLayout::eUndefined;
    imageInfo.usage = usage;
    imageInfo.sharingMode = vk::SharingMode::eExclusive;
    imageInfo.samples = numSamples;
    imageInfo.flags = vk::ImageCreateFlagBits::eCubeCompatible;

    if (device.createImage(&imageInfo, nullptr, &image) != vk::Result::eSuccess)
    {
        throw std::runtime_error("Failed to create cubemap image!");
    }

    vk::MemoryRequirements memRequirements;
    device.getImageMemoryRequirements(image, &memRequirements);

    vk::MemoryAllocateInfo allocInfo{};
    allocInfo.allocationSize = memRequirements.size;
    allocInfo.memoryTypeIndex = BufferUtils::FindMemoryType(physicalDevice, memRequirements.memoryTypeBits, properties);

    if (device.allocateMemory(&allocInfo, nullptr, &imageMemory) != vk::Result::eSuccess)
    {
        throw std::runtime_error("Failed to allocate cubemap image memory!");
    }

    device.bindImageMemory(image, imageMemory, 0);
}

void Cubemap::TransitionImageLayout(vk::ImageLayout oldLayout, vk::ImageLayout newLayout, vk::ImageSubresourceRange subresourceRange)
{
    vk::CommandBuffer commandBuffer = BeginSingleTimeCommands(commandPool, device);

    vk::ImageMemoryBarrier barrier{};
    barrier.oldLayout = oldLayout;
    barrier.newLayout = newLayout;
    barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
    barrier.image = image;
    barrier.subresourceRange = subresourceRange;

    vk::PipelineStageFlags sourceStage;
    vk::PipelineStageFlags destinationStage;

    if (oldLayout == vk::ImageLayout::eUndefined && newLayout == vk::ImageLayout::eTransferDstOptimal) {
        barrier.srcAccessMask = {};
        barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite;

        sourceStage = vk::PipelineStageFlagBits::eTopOfPipe;
        destinationStage = vk::PipelineStageFlagBits::eTransfer;
    }
    else if (oldLayout == vk::ImageLayout::eTransferDstOptimal && newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) {
        barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
        barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;

        sourceStage = vk::PipelineStageFlagBits::eTransfer;
        destinationStage = vk::PipelineStageFlagBits::eFragmentShader;
    }
    else {
        throw std::invalid_argument("Unsupported layout transition!");
    }

    commandBuffer.pipelineBarrier(
        sourceStage, destinationStage,
        {},
        0, nullptr,
        0, nullptr,
        1, &barrier
    );

    EndSingleTimeCommands(commandPool, graphicsQueue, device, commandBuffer);
}

void Cubemap::CreateImageView(vk::Format format, vk::ImageAspectFlags aspectFlags, uint32_t mipLevels)
{
    vk::ImageViewCreateInfo viewInfo{};
    viewInfo.image = image;
    viewInfo.viewType = vk::ImageViewType::eCube;
    viewInfo.format = format;
    viewInfo.subresourceRange.aspectMask = aspectFlags;
    viewInfo.subresourceRange.baseMipLevel = 0;
    viewInfo.subresourceRange.levelCount = mipLevels;
    viewInfo.subresourceRange.baseArrayLayer = 0;
    viewInfo.subresourceRange.layerCount = 6;

    if (device.createImageView(&viewInfo, nullptr, &imageView) != vk::Result::eSuccess)
    {
        throw std::runtime_error("Failed to create cubemap image view!");
    }
}

void Cubemap::CreateSampler(uint32_t mipLevels)
{
    vk::SamplerCreateInfo samplerInfo{};
    samplerInfo.magFilter = vk::Filter::eLinear;
    samplerInfo.minFilter = vk::Filter::eLinear;
    samplerInfo.addressModeU = vk::SamplerAddressMode::eClampToEdge;
    samplerInfo.addressModeV = vk::SamplerAddressMode::eClampToEdge;
    samplerInfo.addressModeW = vk::SamplerAddressMode::eClampToEdge;
    samplerInfo.anisotropyEnable = VK_TRUE;
    samplerInfo.maxAnisotropy = 16;
    samplerInfo.borderColor = vk::BorderColor::eFloatOpaqueWhite;
    samplerInfo.unnormalizedCoordinates = VK_FALSE;
    samplerInfo.compareEnable = VK_FALSE;
    samplerInfo.compareOp = vk::CompareOp::eAlways;
    samplerInfo.mipmapMode = vk::SamplerMipmapMode::eLinear;
    samplerInfo.mipLodBias = 0.0f;
    samplerInfo.minLod = 0.0f;
    samplerInfo.maxLod = static_cast<float>(mipLevels);

    if (device.createSampler(&samplerInfo, nullptr, &sampler) != vk::Result::eSuccess)
    {
        throw std::runtime_error("Failed to create cubemap sampler!");
    }
}

void Cubemap::CopyBufferToImage(vk::Buffer buffer, uint32_t width, uint32_t height, int layerIndex)
{
    vk::CommandBuffer commandBuffer = BeginSingleTimeCommands(commandPool, device);

    vk::BufferImageCopy region{};
    region.bufferOffset = 0;
    region.bufferRowLength = 0;
    region.bufferImageHeight = 0;
    region.imageSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
    region.imageSubresource.mipLevel = 0;
    region.imageSubresource.baseArrayLayer = layerIndex;
    region.imageSubresource.layerCount = 1;
    region.imageOffset = vk::Offset3D{ 0, 0, 0 };
    region.imageExtent = vk::Extent3D{ width, height, 1 };

    commandBuffer.copyBufferToImage(
        buffer,
        image,
        vk::ImageLayout::eTransferDstOptimal,
        1,
        &region
    );

    EndSingleTimeCommands(commandPool, graphicsQueue, device, commandBuffer);
}

vk::CommandBuffer Cubemap::BeginSingleTimeCommands(vk::CommandPool commandPool, vk::Device device)
{
    vk::CommandBufferAllocateInfo allocInfo{};
    allocInfo.level = vk::CommandBufferLevel::ePrimary;
    allocInfo.commandPool = commandPool;
    allocInfo.commandBufferCount = 1;

    vk::CommandBuffer commandBuffer;
    device.allocateCommandBuffers(&allocInfo, &commandBuffer);

    vk::CommandBufferBeginInfo beginInfo{};
    beginInfo.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit;

    commandBuffer.begin(beginInfo);

    return commandBuffer;
}

void Cubemap::EndSingleTimeCommands(vk::CommandPool commandPool, vk::Queue graphicsQueue, vk::Device device, vk::CommandBuffer commandBuffer)
{
    commandBuffer.end();

    vk::SubmitInfo submitInfo{};
    submitInfo.commandBufferCount = 1;
    submitInfo.pCommandBuffers = &commandBuffer;

    graphicsQueue.submit(submitInfo, nullptr);
    graphicsQueue.waitIdle();

    device.freeCommandBuffers(commandPool, 1, &commandBuffer);
}