From 812f0d577b1466dfd4223060dea6f8d1fa4c6ed0 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Thu, 6 May 2021 19:23:21 -0700 Subject: [PATCH] VulkanTexture: support format conversion via vkCmdBlitImage. --- .../backend/src/vulkan/VulkanStagePool.cpp | 85 +++++++++++++ filament/backend/src/vulkan/VulkanStagePool.h | 16 +++ filament/backend/src/vulkan/VulkanTexture.cpp | 119 +++++++++++++----- filament/backend/src/vulkan/VulkanTexture.h | 6 + filament/backend/src/vulkan/VulkanUtility.cpp | 93 ++++++++++++++ filament/backend/src/vulkan/VulkanUtility.h | 2 + 6 files changed, 293 insertions(+), 28 deletions(-) diff --git a/filament/backend/src/vulkan/VulkanStagePool.cpp b/filament/backend/src/vulkan/VulkanStagePool.cpp index 5583eac53b..0cd5ecc6d8 100644 --- a/filament/backend/src/vulkan/VulkanStagePool.cpp +++ b/filament/backend/src/vulkan/VulkanStagePool.cpp @@ -17,6 +17,7 @@ #include "VulkanStagePool.h" #include "VulkanConstants.h" +#include "VulkanUtility.h" #include @@ -58,6 +59,51 @@ VulkanStage const* VulkanStagePool::acquireStage(uint32_t numBytes) { return stage; } +VulkanStageImage const* VulkanStagePool::acquireImage(PixelDataFormat format, PixelDataType type, + uint32_t width, uint32_t height) { + const VkFormat vkformat = getVkFormat(format, type); + for (auto image : mFreeImages) { + if (image->format == vkformat && image->width == width && image->height == height) { + mFreeImages.erase(image); + mUsedImages.insert(image); + return image; + } + } + + VulkanStageImage* image = new VulkanStageImage({ + .format = vkformat, + .width = width, + .height = height, + .lastAccessed = mCurrentFrame, + }); + + mUsedImages.insert(image); + + const VkImageCreateInfo imageInfo = { + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .imageType = VK_IMAGE_TYPE_2D, + .format = vkformat, + .extent = { width, height, 1 }, + .mipLevels = 1, + .arrayLayers = 1, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_LINEAR, + .usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT + }; + + const VmaAllocationCreateInfo allocInfo { + .flags = VMA_ALLOCATION_CREATE_MAPPED_BIT, + .usage = VMA_MEMORY_USAGE_CPU_TO_GPU + }; + + const UTILS_UNUSED VkResult result = vmaCreateImage(mContext.allocator, &imageInfo, &allocInfo, + &image->image, &image->memory, nullptr); + + assert_invariant(result == VK_SUCCESS); + + return image; +} + void VulkanStagePool::gc() noexcept { // If this is one of the first few frames, return early to avoid wrapping unsigned integers. if (++mCurrentFrame <= TIME_BEFORE_EVICTION) { @@ -65,6 +111,7 @@ void VulkanStagePool::gc() noexcept { } const uint64_t evictionTime = mCurrentFrame - TIME_BEFORE_EVICTION; + // Destroy buffers that have not been used for several frames. decltype(mFreeStages) freeStages; freeStages.swap(mFreeStages); for (auto pair : freeStages) { @@ -76,6 +123,7 @@ void VulkanStagePool::gc() noexcept { } } + // Reclaim buffers that are no longer being used by any command buffer. decltype(mUsedStages) usedStages; usedStages.swap(mUsedStages); for (auto stage : usedStages) { @@ -86,6 +134,30 @@ void VulkanStagePool::gc() noexcept { mUsedStages.insert(stage); } } + + // Destroy images that have not been used for several frames. + decltype(mFreeImages) freeImages; + freeImages.swap(mFreeImages); + for (auto image : freeImages) { + if (image->lastAccessed < evictionTime) { + vmaDestroyImage(mContext.allocator, image->image, image->memory); + delete image; + } else { + mFreeImages.insert(image); + } + } + + // Reclaim images that are no longer being used by any command buffer. + decltype(mUsedImages) usedImages; + usedImages.swap(mUsedImages); + for (auto image : usedImages) { + if (image->lastAccessed < evictionTime) { + image->lastAccessed = mCurrentFrame; + mFreeImages.insert(image); + } else { + mUsedImages.insert(image); + } + } } void VulkanStagePool::reset() noexcept { @@ -94,11 +166,24 @@ void VulkanStagePool::reset() noexcept { delete stage; } mUsedStages.clear(); + for (auto pair : mFreeStages) { vmaDestroyBuffer(mContext.allocator, pair.second->buffer, pair.second->memory); delete pair.second; } mFreeStages.clear(); + + for (auto image : mUsedImages) { + vmaDestroyImage(mContext.allocator, image->image, image->memory); + delete image; + } + mUsedStages.clear(); + + for (auto image : mFreeImages) { + vmaDestroyImage(mContext.allocator, image->image, image->memory); + delete image; + } + mFreeStages.clear(); } } // namespace filament diff --git a/filament/backend/src/vulkan/VulkanStagePool.h b/filament/backend/src/vulkan/VulkanStagePool.h index 9733202c41..3badfd78d2 100644 --- a/filament/backend/src/vulkan/VulkanStagePool.h +++ b/filament/backend/src/vulkan/VulkanStagePool.h @@ -33,7 +33,17 @@ struct VulkanStage { mutable uint64_t lastAccessed; }; +struct VulkanStageImage { + VkFormat format; + uint32_t width; + uint32_t height; + mutable uint64_t lastAccessed; + VmaAllocation memory; + VkImage image; +}; + // Manages a pool of stages, periodically releasing stages that have been unused for a while. +// This class manages two types of host-mappable staging areas: buffer stages and image stages. class VulkanStagePool { public: explicit VulkanStagePool(VulkanContext& context) noexcept : mContext(context) {} @@ -42,6 +52,9 @@ public: // The stage is automatically released back to the pool after TIME_BEFORE_EVICTION frames. VulkanStage const* acquireStage(uint32_t numBytes); + VulkanStageImage const* acquireImage(PixelDataFormat format, PixelDataType type, + uint32_t width, uint32_t height); + // Evicts old unused stages and bumps the current frame number. void gc() noexcept; @@ -58,6 +71,9 @@ private: // Simple unordered set for stashing a list of in-use stages that can be reclaimed later. std::unordered_set mUsedStages; + std::unordered_set mFreeImages; + std::unordered_set mUsedImages; + // Store the current "time" (really just a frame count) and LRU eviction parameters. uint64_t mCurrentFrame = 0; }; diff --git a/filament/backend/src/vulkan/VulkanTexture.cpp b/filament/backend/src/vulkan/VulkanTexture.cpp index c58eb0f130..ca0623c3de 100644 --- a/filament/backend/src/vulkan/VulkanTexture.cpp +++ b/filament/backend/src/vulkan/VulkanTexture.cpp @@ -236,48 +236,74 @@ void VulkanTexture::update2DImage(const PixelBufferDescriptor& data, uint32_t wi void VulkanTexture::update3DImage(const PixelBufferDescriptor& data, uint32_t width, uint32_t height, uint32_t depth, int miplevel) { assert_invariant(width <= this->width && height <= this->height && depth <= this->depth); - const uint32_t numSrcBytes = data.size; + const PixelBufferDescriptor* hostData = &data; + PixelBufferDescriptor reshapedData; - uint32_t numDstBytes = numSrcBytes; - PixelDataFormat dstFormat = data.format; - void* mapped = nullptr; - VulkanStage const* stage = nullptr; - const void* clientData = data.buffer; - - // Create and populate the staging buffer. + // First, reshape 3-component data into 4-component data. The fourth component is usually + // set to 1 (one exception is when type = HALF). In practice, alpha is just a dummy channel. + // Note that the reshaped data is freed at the end of this method due to the callback. if (data.format == PixelDataFormat::RGB) { + const auto freeFunc = [](void* buffer, size_t size, void* user) { free(buffer); }; + const size_t reshapedSize = 4 * data.size / 3; + const PixelDataFormat reshapedFormat = PixelDataFormat::RGBA; switch (data.type) { case PixelDataType::BYTE: - case PixelDataType::UBYTE: - numDstBytes = 4 * numSrcBytes / 3; - dstFormat = PixelDataFormat::RGBA; - stage = mStagePool.acquireStage(numDstBytes); - vmaMapMemory(mContext.allocator, stage->memory, &mapped); - DataReshaper::reshape(mapped, clientData, numSrcBytes); + case PixelDataType::UBYTE: { + uint8_t* bytes = (uint8_t*) malloc(reshapedSize); + DataReshaper::reshape(bytes, data.buffer, data.size); + PixelBufferDescriptor pbd(bytes, reshapedSize, reshapedFormat, data.type, freeFunc); + reshapedData = std::move(pbd); + hostData = &reshapedData; break; + } case PixelDataType::SHORT: case PixelDataType::USHORT: - case PixelDataType::HALF: - numDstBytes = 4 * numSrcBytes / 3; - dstFormat = PixelDataFormat::RGBA; - stage = mStagePool.acquireStage(numDstBytes); - vmaMapMemory(mContext.allocator, stage->memory, &mapped); - DataReshaper::reshape(mapped, clientData, numSrcBytes); + case PixelDataType::HALF: { + uint8_t* bytes = (uint8_t*) malloc(reshapedSize); + DataReshaper::reshape(bytes, data.buffer, data.size); + PixelBufferDescriptor pbd(bytes, reshapedSize, reshapedFormat, data.type, freeFunc); + reshapedData = std::move(pbd); + hostData = &reshapedData; break; + } + case PixelDataType::INT: + case PixelDataType::UINT: + case PixelDataType::FLOAT: { + uint8_t* bytes = (uint8_t*) malloc(reshapedSize); + DataReshaper::reshape(bytes, data.buffer, data.size); + PixelBufferDescriptor pbd(bytes, reshapedSize, reshapedFormat, data.type, freeFunc); + reshapedData = std::move(pbd); + hostData = &reshapedData; + break; + } + default: break; } - } + } - if (numDstBytes == numSrcBytes) { - stage = mStagePool.acquireStage(numDstBytes); - vmaMapMemory(mContext.allocator, stage->memory, &mapped); - memcpy(mapped, clientData, numSrcBytes); - } + // If format conversion is both required and supported, use vkCmdBlitImage. Otherwise, use + // vkCmdCopyBufferToImage. + const VkFormat hostFormat = backend::getVkFormat(hostData->format, hostData->type); + const VkFormat deviceFormat = getVkFormatLinear(mVkFormat); + + if (hostFormat != deviceFormat && hostFormat != VK_FORMAT_UNDEFINED) { + updateWithBlitImage(*hostData, width, height, depth, miplevel); + } else { + updateWithCopyBuffer(*hostData, width, height, depth, miplevel); + } +} + +void VulkanTexture::updateWithCopyBuffer(const PixelBufferDescriptor& hostData, uint32_t width, + uint32_t height, uint32_t depth, int miplevel) { + void* mapped = nullptr; + VulkanStage const* stage = mStagePool.acquireStage(hostData.size); + vmaMapMemory(mContext.allocator, stage->memory, &mapped); + memcpy(mapped, hostData.buffer, hostData.size); vmaUnmapMemory(mContext.allocator, stage->memory); - vmaFlushAllocation(mContext.allocator, stage->memory, 0, numDstBytes); + vmaFlushAllocation(mContext.allocator, stage->memory, 0, hostData.size); const VkCommandBuffer cmdbuffer = mContext.commands->get().cmdbuffer; transitionImageLayout(cmdbuffer, mTextureImage, VK_IMAGE_LAYOUT_UNDEFINED, @@ -286,7 +312,44 @@ void VulkanTexture::update3DImage(const PixelBufferDescriptor& data, uint32_t wi copyBufferToImage(cmdbuffer, stage->buffer, mTextureImage, width, height, depth, nullptr, miplevel); - transitionImageLayout(cmdbuffer, mTextureImage,VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + transitionImageLayout(cmdbuffer, mTextureImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + getTextureLayout(usage), miplevel, 1, 1, mAspect); +} + +void VulkanTexture::updateWithBlitImage(const PixelBufferDescriptor& hostData, uint32_t width, + uint32_t height, uint32_t depth, int miplevel) { + void* mapped = nullptr; + VulkanStageImage const* stage = mStagePool.acquireImage(hostData.format, hostData.type, + width, height); + vmaMapMemory(mContext.allocator, stage->memory, &mapped); + memcpy(mapped, hostData.buffer, hostData.size); + vmaUnmapMemory(mContext.allocator, stage->memory); + vmaFlushAllocation(mContext.allocator, stage->memory, 0, hostData.size); + + const VkCommandBuffer cmdbuffer = mContext.commands->get().cmdbuffer; + + // TODO: support blit-based format conversion for 3D images and cubemaps. + const int layer = 0; + + const VkOffset3D rect[2] { {0, 0, 0}, {int32_t(width), int32_t(height), 1} }; + + const VkImageBlit blitRegions[1] = {{ + .srcSubresource = { mAspect, 0, 0, 1 }, + .srcOffsets = { rect[0], rect[1] }, + .dstSubresource = { mAspect, uint32_t(miplevel), layer, 1 }, + .dstOffsets = { rect[0], rect[1] } + }}; + + transitionImageLayout(cmdbuffer, stage->image, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, miplevel, 1, 1, mAspect); + + transitionImageLayout(cmdbuffer, mTextureImage, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, miplevel, 1, 1, mAspect); + + vkCmdBlitImage(cmdbuffer, stage->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, mTextureImage, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, blitRegions, VK_FILTER_NEAREST); + + transitionImageLayout(cmdbuffer, mTextureImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, getTextureLayout(usage), miplevel, 1, 1, mAspect); } diff --git a/filament/backend/src/vulkan/VulkanTexture.h b/filament/backend/src/vulkan/VulkanTexture.h index 70a2eaed0c..1d07ea840f 100644 --- a/filament/backend/src/vulkan/VulkanTexture.h +++ b/filament/backend/src/vulkan/VulkanTexture.h @@ -69,6 +69,12 @@ private: uint32_t width, uint32_t height, uint32_t depth, FaceOffsets const* faceOffsets, uint32_t miplevel); + void updateWithCopyBuffer(const PixelBufferDescriptor& hostData, uint32_t width, + uint32_t height, uint32_t depth, int miplevel); + + void updateWithBlitImage(const PixelBufferDescriptor& hostData, uint32_t width, + uint32_t height, uint32_t depth, int miplevel); + const VkFormat mVkFormat; const VkComponentMapping mSwizzle; VkImageViewType mViewType; diff --git a/filament/backend/src/vulkan/VulkanUtility.cpp b/filament/backend/src/vulkan/VulkanUtility.cpp index 27b9eb2083..93759baf28 100644 --- a/filament/backend/src/vulkan/VulkanUtility.cpp +++ b/filament/backend/src/vulkan/VulkanUtility.cpp @@ -232,6 +232,99 @@ VkFormat getVkFormat(TextureFormat format) { } } +// Converts PixelBufferDescriptor format + type pair into a VkFormat. +// +// NOTE: This function only returns formats that support VK_FORMAT_FEATURE_BLIT_SRC_BIT as per +// "Required Format Support" in the Vulkan specification. These are the only formats that can be +// used for format conversion. If the requested format does not support this feature, then +// VK_FORMAT_UNDEFINED is returned. +VkFormat getVkFormat(PixelDataFormat format, PixelDataType type) { + if (type == PixelDataType::USHORT_565) return VK_FORMAT_R5G6B5_UNORM_PACK16; + if (type == PixelDataType::UINT_2_10_10_10_REV) return VK_FORMAT_A2B10G10R10_UNORM_PACK32; + if (type == PixelDataType::UINT_10F_11F_11F_REV) return VK_FORMAT_B10G11R11_UFLOAT_PACK32; + + #define CONVERT(FORMAT, TYPE, VK) \ + if (PixelDataFormat::FORMAT == format && PixelDataType::TYPE == type) return VK_FORMAT_ ## VK; + + CONVERT(R, UBYTE, R8_UNORM); + CONVERT(R, BYTE, R8_SNORM); + CONVERT(R_INTEGER, UBYTE, R8_UINT); + CONVERT(R_INTEGER, BYTE, R8_SINT); + CONVERT(RG, UBYTE, R8G8_UNORM); + CONVERT(RG, BYTE, R8G8_SNORM); + CONVERT(RG_INTEGER, UBYTE, R8G8_UINT); + CONVERT(RG_INTEGER, BYTE, R8G8_SINT); + CONVERT(RGBA, UBYTE, R8G8B8A8_UNORM); + CONVERT(RGBA, BYTE, R8G8B8A8_SNORM); + CONVERT(RGBA_INTEGER, UBYTE, R8G8B8A8_UINT); + CONVERT(RGBA_INTEGER, BYTE, R8G8B8A8_SINT); + CONVERT(R_INTEGER, USHORT, R16_UINT); + CONVERT(R_INTEGER, SHORT, R16_SINT); + CONVERT(R, HALF, R16_SFLOAT); + CONVERT(RG_INTEGER, USHORT, R16G16_UINT); + CONVERT(RG_INTEGER, SHORT, R16G16_SINT); + CONVERT(RG, HALF, R16G16_SFLOAT); + CONVERT(RGBA_INTEGER, USHORT, R16G16B16A16_SINT); + CONVERT(RGBA_INTEGER, SHORT, R16G16B16A16_SINT); + CONVERT(RGBA, HALF, R16G16B16A16_SFLOAT); + CONVERT(R_INTEGER, UINT, R32_UINT); + CONVERT(R_INTEGER, INT, R32_SINT); + CONVERT(R, FLOAT, R32_SFLOAT); + CONVERT(RG_INTEGER, UINT, R32G32_UINT); + CONVERT(RG_INTEGER, INT, R32G32_SINT); + CONVERT(RG, FLOAT, R32G32_SFLOAT); + CONVERT(RGBA_INTEGER, UINT, R32G32B32A32_UINT); + CONVERT(RGBA_INTEGER, INT, R32G32B32A32_SINT); + CONVERT(RGBA, FLOAT, R32G32B32A32_SFLOAT); + #undef CONVERT + + return VK_FORMAT_UNDEFINED; +} + +// Converts an SRGB Vulkan format identifier into its corresponding canonical UNORM format. If the +// given format is not an SRGB format, it is returned unmodified. This function is useful when +// determining if blit-based conversion is needed, since SRGB is orthogonal to the actual bit layout +// of color components. +VkFormat getVkFormatLinear(VkFormat format) { + switch (format) { + case VK_FORMAT_R8_SRGB: return VK_FORMAT_R8_UNORM; + case VK_FORMAT_R8G8_SRGB: return VK_FORMAT_R8G8_UNORM; + case VK_FORMAT_R8G8B8_SRGB: return VK_FORMAT_R8G8B8_UNORM; + case VK_FORMAT_B8G8R8_SRGB: return VK_FORMAT_B8G8R8_UNORM; + case VK_FORMAT_R8G8B8A8_SRGB: return VK_FORMAT_R8G8B8A8_UNORM; + case VK_FORMAT_B8G8R8A8_SRGB: return VK_FORMAT_B8G8R8A8_UNORM; + case VK_FORMAT_A8B8G8R8_SRGB_PACK32: return VK_FORMAT_A8B8G8R8_UNORM_PACK32; + case VK_FORMAT_BC1_RGB_SRGB_BLOCK: return VK_FORMAT_BC1_RGB_UNORM_BLOCK; + case VK_FORMAT_BC1_RGBA_SRGB_BLOCK: return VK_FORMAT_BC1_RGBA_UNORM_BLOCK; + case VK_FORMAT_BC2_SRGB_BLOCK: return VK_FORMAT_BC2_UNORM_BLOCK; + case VK_FORMAT_BC3_SRGB_BLOCK: return VK_FORMAT_BC3_UNORM_BLOCK; + case VK_FORMAT_BC7_SRGB_BLOCK: return VK_FORMAT_BC7_UNORM_BLOCK; + case VK_FORMAT_ETC2_R8G8B8_SRGB_BLOCK: return VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK; + case VK_FORMAT_ETC2_R8G8B8A1_SRGB_BLOCK: return VK_FORMAT_ETC2_R8G8B8A1_UNORM_BLOCK; + case VK_FORMAT_ETC2_R8G8B8A8_SRGB_BLOCK: return VK_FORMAT_ETC2_R8G8B8A8_UNORM_BLOCK; + case VK_FORMAT_ASTC_4x4_SRGB_BLOCK: return VK_FORMAT_ASTC_4x4_UNORM_BLOCK; + case VK_FORMAT_ASTC_5x4_SRGB_BLOCK: return VK_FORMAT_ASTC_5x4_UNORM_BLOCK; + case VK_FORMAT_ASTC_5x5_SRGB_BLOCK: return VK_FORMAT_ASTC_5x5_UNORM_BLOCK; + case VK_FORMAT_ASTC_6x5_SRGB_BLOCK: return VK_FORMAT_ASTC_6x5_UNORM_BLOCK; + case VK_FORMAT_ASTC_6x6_SRGB_BLOCK: return VK_FORMAT_ASTC_6x6_UNORM_BLOCK; + case VK_FORMAT_ASTC_8x5_SRGB_BLOCK: return VK_FORMAT_ASTC_8x5_UNORM_BLOCK; + case VK_FORMAT_ASTC_8x6_SRGB_BLOCK: return VK_FORMAT_ASTC_8x6_UNORM_BLOCK; + case VK_FORMAT_ASTC_8x8_SRGB_BLOCK: return VK_FORMAT_ASTC_8x8_UNORM_BLOCK; + case VK_FORMAT_ASTC_10x5_SRGB_BLOCK: return VK_FORMAT_ASTC_10x5_UNORM_BLOCK; + case VK_FORMAT_ASTC_10x6_SRGB_BLOCK: return VK_FORMAT_ASTC_10x6_UNORM_BLOCK; + case VK_FORMAT_ASTC_10x8_SRGB_BLOCK: return VK_FORMAT_ASTC_10x8_UNORM_BLOCK; + case VK_FORMAT_ASTC_10x10_SRGB_BLOCK: return VK_FORMAT_ASTC_10x10_UNORM_BLOCK; + case VK_FORMAT_ASTC_12x10_SRGB_BLOCK: return VK_FORMAT_ASTC_12x10_UNORM_BLOCK; + case VK_FORMAT_ASTC_12x12_SRGB_BLOCK: return VK_FORMAT_ASTC_12x12_UNORM_BLOCK; + case VK_FORMAT_PVRTC1_2BPP_SRGB_BLOCK_IMG: return VK_FORMAT_PVRTC1_2BPP_UNORM_BLOCK_IMG; + case VK_FORMAT_PVRTC1_4BPP_SRGB_BLOCK_IMG: return VK_FORMAT_PVRTC1_4BPP_UNORM_BLOCK_IMG; + case VK_FORMAT_PVRTC2_2BPP_SRGB_BLOCK_IMG: return VK_FORMAT_PVRTC2_2BPP_UNORM_BLOCK_IMG; + case VK_FORMAT_PVRTC2_4BPP_SRGB_BLOCK_IMG: return VK_FORMAT_PVRTC2_4BPP_UNORM_BLOCK_IMG; + default: return format; + } + return format; +} + // See also FTexture::computeTextureDataSize, which takes a public-facing Texture format rather // than a driver-level Texture format, and can account for a specified byte alignment. uint32_t getBytesPerPixel(TextureFormat format) { diff --git a/filament/backend/src/vulkan/VulkanUtility.h b/filament/backend/src/vulkan/VulkanUtility.h index 78ba00d19e..0d85563334 100644 --- a/filament/backend/src/vulkan/VulkanUtility.h +++ b/filament/backend/src/vulkan/VulkanUtility.h @@ -38,6 +38,8 @@ struct VulkanLayoutTransition { void createSemaphore(VkDevice device, VkSemaphore* semaphore); VkFormat getVkFormat(ElementType type, bool normalized, bool integer); VkFormat getVkFormat(TextureFormat format); +VkFormat getVkFormat(PixelDataFormat format, PixelDataType type); +VkFormat getVkFormatLinear(VkFormat format); uint32_t getBytesPerPixel(TextureFormat format); VkCompareOp getCompareOp(SamplerCompareFunc func); VkBlendFactor getBlendFactor(BlendFunction mode);