/* -*- tab-width: 4; -*- */ /* vi: set sw=2 ts=4 expandtab: */ /* * Copyright 2018-2020 Mark Callow. * SPDX-License-Identifier: Apache-2.0 */ /** * @internal * @file * @~English * * @brief Functions for instantiating Vulkan textures from KTX files. * * @author Mark Callow, github.com/MarkCallow */ #ifdef _WIN32 #define _CRT_SECURE_NO_WARNINGS #endif #include #include #include #include #include "vk_funcs.h" // Must be included before ktxvulkan.h. #include "ktxvulkan.h" #include "ktxint.h" #include "unused.h" #include "texture1.h" #include "texture2.h" #include "vk_format.h" /* * N.B. See comment at top of ./glloader.c regarding references to * "non0class" members in Doxygen comments. */ // Macro to check and display Vulkan return results. // Use when the only possible errors are caused by invalid usage by this loader. #if defined(_DEBUG) #define VK_CHECK_RESULT(f) \ { \ VkResult res = (f); \ if (res != VK_SUCCESS) \ { \ /* XXX Find an errorString function. */ \ fprintf(stderr, "Fatal error in ktxLoadVkTexture*: " \ "VkResult is \"%d\" in %s at line %d\n", \ res, __FILE__, __LINE__); \ assert(res == VK_SUCCESS); \ } \ } #else #define VK_CHECK_RESULT(f) ((void)f) #endif #define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0])) #define DEFAULT_FENCE_TIMEOUT 100000000000 #define VK_FLAGS_NONE 0 static void setImageLayout( ktxVulkanFunctions vkFuncs, VkCommandBuffer cmdBuffer, VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout, VkImageSubresourceRange subresourceRange); static void generateMipmaps(ktxVulkanTexture* vkTexture, ktxVulkanDeviceInfo* vdi, VkFilter filter, VkImageLayout initialLayout); /** * @defgroup ktx_vkloader Vulkan Texture Image Loader * @brief Create texture images on a Vulkan device. * @{ */ /** * @example vkload.cpp * This shows how to create and load a Vulkan image using the Vulkan texture * image loading functions. */ /** * @memberof ktxVulkanDeviceInfo * @~English * @brief Create a ktxVulkanDeviceInfo object. * * Allocates CPU memory for a ktxVulkanDeviceInfo object then calls * ktxVulkanDeviceInfo\_Construct(). See it for documentation of the * parameters. * * @return a pointer to the constructed ktxVulkanDeviceInfo. * * @sa ktxVulkanDeviceInfo\_Construct() * @sa ktxVulkanDeviceInfo\_Destroy() */ ktxVulkanDeviceInfo* ktxVulkanDeviceInfo_Create(VkPhysicalDevice physicalDevice, VkDevice device, VkQueue queue, VkCommandPool cmdPool, const VkAllocationCallbacks* pAllocator) { return ktxVulkanDeviceInfo_CreateEx(VK_NULL_HANDLE, physicalDevice, device, queue, cmdPool, pAllocator, NULL); } /** * @memberof ktxVulkanDeviceInfo * @~English * @brief Create a ktxVulkanDeviceInfo object. * * Allocates CPU memory for a ktxVulkanDeviceInfo object then calls * ktxVulkanDeviceInfo\_Construct(). See it for documentation of the * parameters. * * @return a pointer to the constructed ktxVulkanDeviceInfo. * * @sa ktxVulkanDeviceInfo\_Construct() * @sa ktxVulkanDeviceInfo\_Destroy() */ ktxVulkanDeviceInfo* ktxVulkanDeviceInfo_CreateEx(VkInstance instance, VkPhysicalDevice physicalDevice, VkDevice device, VkQueue queue, VkCommandPool cmdPool, const VkAllocationCallbacks* pAllocator, const ktxVulkanFunctions* pFuncs) { ktxVulkanDeviceInfo* newvdi; newvdi = (ktxVulkanDeviceInfo*)malloc(sizeof(ktxVulkanDeviceInfo)); if (newvdi != NULL) { if (ktxVulkanDeviceInfo_ConstructEx(newvdi, instance, physicalDevice, device, queue, cmdPool, pAllocator, pFuncs) != KTX_SUCCESS) { free(newvdi); newvdi = 0; } } return newvdi; } /** * @memberof ktxVulkanDeviceInfo * @~English * @brief Construct a ktxVulkanDeviceInfo object. * * Records the device information, allocates a command buffer that will be * used to transfer image data to the Vulkan device and retrieves the physical * device memory properties for ease of use when allocating device memory for * the images. * * If @c VK_IMAGE_TILING_OPTIMAL will be passed to ktxTexture\_VkUploadEx(), the * family of the @a queue parameter must support transfers. This is true if * any of @c VK_QUEUE_GRAPHICS_BIT, @c VK_QUEUE_COMPUTE_BIT or * @c VK_QUEUE_TRANSFER_BIT is set in the @c queueFlags property of the queue's * @c VkQueueFamilyProperties. If protected memory is being used, i.e * @c queueFlags has the @c VK_QUEUE_PROTECTED_BIT set, then * @c VK_IMAGE_TILING_OPTIMAL must be passed to ktxTexture\_VkUploadEx(). * * VkImages created in @ref ktxTexture::ktxTexture\_VkUploadEx() * "ktxTexture_VkUploadEx()" will have @c VK_SHARING_MODE_EXCLUSIVE set. * Thus the resulting image will be usable only with queues of the same family * as @a queue. * * Pass a valid ktxVulkanDeviceInfo\* to any Vulkan KTX image loading * function to provide it with the information. * * @returns KTX\_SUCCESS on success, other KTX\_\* enum values on error. * * @exception KTX_NOT_FOUND A dynamically loaded Vulkan function * required by the loader was not found. * * @exception KTX_OUT_OF_MEMORY A command buffer could not be allocated. * * @sa ktxVulkanDeviceInfo\_Destruct() * * @param This pointer to the ktxVulkanDeviceInfo object to * initialize. * @param physicalDevice handle of the Vulkan physical device. * @param device handle of the Vulkan logical device. * @param queue handle of the Vulkan queue. * @param cmdPool handle of the Vulkan command pool. * @param pAllocator pointer to the allocator to use for the image * memory. If NULL, the default allocator will be used. * */ KTX_error_code ktxVulkanDeviceInfo_Construct(ktxVulkanDeviceInfo* This, VkPhysicalDevice physicalDevice, VkDevice device, VkQueue queue, VkCommandPool cmdPool, const VkAllocationCallbacks* pAllocator) { return ktxVulkanDeviceInfo_ConstructEx(This, VK_NULL_HANDLE, physicalDevice, device, queue, cmdPool, pAllocator, NULL); } /** * @memberof ktxVulkanDeviceInfo * @copydoc ktxVulkanDeviceInfo_Construct * * @~English * @param instance handle of the Vulkan instance. If @c VK_NULL_HANDLE, * which is not recommended, the function will attempt * to initialize the instance-level functions via the * platform's standard dynamic library symbol loading * mechanisms. * @param pFunctions pointer to the struct of functions to use for vulkan * operations. Can be NULL in which case the function * will retrieve the proc addresses itself. */ KTX_error_code ktxVulkanDeviceInfo_ConstructEx(ktxVulkanDeviceInfo* This, VkInstance instance, VkPhysicalDevice physicalDevice, VkDevice device, VkQueue queue, VkCommandPool cmdPool, const VkAllocationCallbacks* pAllocator, const ktxVulkanFunctions* pFunctions) { #define LOAD_EXT_FUNC(member, fun) \ do { \ if ((member).fun == NULL) { \ (member).fun = (PFN_##fun)ktxLoadVulkanFunction(#fun); \ if ((member).fun == NULL) { \ return KTX_NOT_FOUND; \ }\ } \ } while (0) #define LOAD_INST_FUNC(member, instance, fun) \ do { \ if ((member).fun == NULL) { \ (member).fun = (PFN_##fun)((member).vkGetInstanceProcAddr)((instance), #fun); \ if ((member).fun == NULL) { \ return KTX_NOT_FOUND; \ }\ } \ } while (0) #define LOAD_DEVICE_FUNC(member, device, fun) \ do { \ if ((member).fun == NULL) { \ (member).fun = (PFN_##fun)((member).vkGetDeviceProcAddr)((device), #fun); \ if ((member).fun == NULL) { \ return KTX_NOT_FOUND; \ }\ } \ } while (0) This->instance = instance; This->physicalDevice = physicalDevice; This->device = device; This->queue = queue; This->cmdPool = cmdPool; This->pAllocator = pAllocator; ktxVulkanFunctions funcs; memset(&funcs, 0, sizeof(ktxVulkanFunctions)); if (pFunctions != NULL) funcs = *pFunctions; if (instance == VK_NULL_HANDLE || pFunctions == NULL) { // This is the old behavior, where no functions where specified. We // take a shot at loading them dynamically. // The reason we check this here, is that ktxLoadVulkanFunction doesn't // give us a proper error code. ktx_error_code_e kresult = ktxLoadVulkanLibrary(); if (kresult != KTX_SUCCESS) return kresult; // If pFunctions are null, then we try to load the minimum number of // required functions. if (pFunctions == NULL) { LOAD_EXT_FUNC(funcs, vkGetInstanceProcAddr); } // If we have no instance, we will need to bind the instance-level // functions, as we won't be able to load them through // vkGetInstanceProcAddr, and we need to load vkGetDeviceProcAddr. if (instance == VK_NULL_HANDLE) { LOAD_EXT_FUNC(funcs, vkGetDeviceProcAddr); // These require an instance, which we don't have. LOAD_EXT_FUNC(funcs, vkGetPhysicalDeviceFormatProperties); LOAD_EXT_FUNC(funcs, vkGetPhysicalDeviceImageFormatProperties); LOAD_EXT_FUNC(funcs, vkGetPhysicalDeviceMemoryProperties); } } // We now load the rest of the functions in. LOAD_INST_FUNC(funcs, instance, vkGetPhysicalDeviceFormatProperties); LOAD_INST_FUNC(funcs, instance, vkGetPhysicalDeviceMemoryProperties); LOAD_INST_FUNC(funcs, instance, vkGetPhysicalDeviceImageFormatProperties); // We now get to device functions. LOAD_INST_FUNC(funcs, instance, vkGetDeviceProcAddr); LOAD_DEVICE_FUNC(funcs, device, vkAllocateCommandBuffers); LOAD_DEVICE_FUNC(funcs, device, vkFreeCommandBuffers); LOAD_DEVICE_FUNC(funcs, device, vkAllocateMemory); LOAD_DEVICE_FUNC(funcs, device, vkFreeMemory); LOAD_DEVICE_FUNC(funcs, device, vkBeginCommandBuffer); LOAD_DEVICE_FUNC(funcs, device, vkEndCommandBuffer); LOAD_DEVICE_FUNC(funcs, device, vkBindBufferMemory); LOAD_DEVICE_FUNC(funcs, device, vkBindImageMemory); LOAD_DEVICE_FUNC(funcs, device, vkCmdBlitImage); LOAD_DEVICE_FUNC(funcs, device, vkCmdCopyBufferToImage); LOAD_DEVICE_FUNC(funcs, device, vkCmdPipelineBarrier); LOAD_DEVICE_FUNC(funcs, device, vkCreateImage); LOAD_DEVICE_FUNC(funcs, device, vkDestroyImage); LOAD_DEVICE_FUNC(funcs, device, vkCreateBuffer); LOAD_DEVICE_FUNC(funcs, device, vkDestroyBuffer); LOAD_DEVICE_FUNC(funcs, device, vkCreateFence); LOAD_DEVICE_FUNC(funcs, device, vkDestroyFence); LOAD_DEVICE_FUNC(funcs, device, vkWaitForFences); LOAD_DEVICE_FUNC(funcs, device, vkMapMemory); LOAD_DEVICE_FUNC(funcs, device, vkUnmapMemory); LOAD_DEVICE_FUNC(funcs, device, vkQueueSubmit); LOAD_DEVICE_FUNC(funcs, device, vkQueueWaitIdle); LOAD_DEVICE_FUNC(funcs, device, vkGetBufferMemoryRequirements); LOAD_DEVICE_FUNC(funcs, device, vkGetImageMemoryRequirements); LOAD_DEVICE_FUNC(funcs, device, vkGetImageSubresourceLayout); This->vkFuncs = funcs; VkCommandBufferAllocateInfo cmdBufInfo = { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; VkResult result; This->vkFuncs.vkGetPhysicalDeviceMemoryProperties(physicalDevice, &This->deviceMemoryProperties); // Use a separate command buffer for texture loading. Needed for // submitting image barriers and converting tilings. cmdBufInfo.commandPool = cmdPool; cmdBufInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cmdBufInfo.commandBufferCount = 1; result = This->vkFuncs.vkAllocateCommandBuffers(device, &cmdBufInfo, &This->cmdBuffer); if (result != VK_SUCCESS) { return KTX_OUT_OF_MEMORY; // TODO: Consider an equivalent to pGlError } return KTX_SUCCESS; #undef LOAD_EXT_FUNC #undef LOAD_INST_FUNC #undef LOAD_DEVICE_FUNC } /** * @memberof ktxVulkanDeviceInfo * @~English * @brief Destruct a ktxVulkanDeviceInfo object. * * Frees the command buffer. * * @param This pointer to the ktxVulkanDeviceInfo to destruct. */ void ktxVulkanDeviceInfo_Destruct(ktxVulkanDeviceInfo* This) { This->vkFuncs.vkFreeCommandBuffers(This->device, This->cmdPool, 1, &This->cmdBuffer); } /** * @memberof ktxVulkanDeviceInfo * @~English * @brief Destroy a ktxVulkanDeviceInfo object. * * Calls ktxVulkanDeviceInfo\_Destruct() then frees the ktxVulkanDeviceInfo. * * @param This pointer to the ktxVulkanDeviceInfo to destroy. */ void ktxVulkanDeviceInfo_Destroy(ktxVulkanDeviceInfo* This) { assert(This != NULL); ktxVulkanDeviceInfo_Destruct(This); free(This); } /* Get appropriate memory type index for a memory allocation. */ static uint32_t ktxVulkanDeviceInfo_getMemoryType(ktxVulkanDeviceInfo* This, uint32_t typeBits, VkFlags properties) { for (uint32_t i = 0; i < 32; i++) { if ((typeBits & 1) == 1) { if ((This->deviceMemoryProperties.memoryTypes[i].propertyFlags & properties) == properties) { return i; } } typeBits >>= 1; } // XXX : throw error return 0; } //====================================================================== // ReadImages callbacks //====================================================================== typedef struct user_cbdata_optimal { VkBufferImageCopy* region; // Specify destination region in final image. VkDeviceSize offset; // Offset of current level in staging buffer ktx_uint32_t numFaces; ktx_uint32_t numLayers; // The following are used only by optimalTilingPadCallback ktx_uint8_t* dest; // Pointer to mapped staging buffer. ktx_uint32_t elementSize; ktx_uint32_t numDimensions; #if defined(_DEBUG) VkBufferImageCopy* regionsArrayEnd; #endif } user_cbdata_optimal; /** * @internal * @~English * @brief Callback for optimally tiled textures with no source row padding. * * Images must be preloaded into the staging buffer. Each iteration, i.e. * the value of @p faceLodSize must be for a complete mip level, regardless of * texture type. This should be used only with @c ktx_Texture_IterateLevels. * * Sets up a region to copy the data from the staging buffer to the final * image. * * @note @p pixels is not used. * * @copydetails PFNKTXITERCB */ static KTX_error_code optimalTilingCallback(int miplevel, int face, int width, int height, int depth, ktx_uint64_t faceLodSize, void* pixels, void* userdata) { user_cbdata_optimal* ud = (user_cbdata_optimal*)userdata; UNUSED(pixels); // Set up copy to destination region in final image #if defined(_DEBUG) assert(ud->region < ud->regionsArrayEnd); #endif ud->region->bufferOffset = ud->offset; ud->offset += faceLodSize; // These 2 are expressed in texels. ud->region->bufferRowLength = 0; ud->region->bufferImageHeight = 0; ud->region->imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; ud->region->imageSubresource.mipLevel = miplevel; ud->region->imageSubresource.baseArrayLayer = face; ud->region->imageSubresource.layerCount = ud->numLayers * ud->numFaces; ud->region->imageOffset.x = 0; ud->region->imageOffset.y = 0; ud->region->imageOffset.z = 0; ud->region->imageExtent.width = width; ud->region->imageExtent.height = height; ud->region->imageExtent.depth = depth; ud->region += 1; return KTX_SUCCESS; } uint32_t lcm4(uint32_t a); /** * @internal * @~English * @brief Callback for optimally tiled textures with possible source row * padding. * * Copies data to the staging buffer removing row padding, if necessary. * Increments the offset for destination of the next copy increasing it to an * appropriate common multiple of the element size and 4 to comply with Vulkan * valid usage. Finally sets up a region to copy the face/lod from the staging * buffer to the final image. * * This longer method is needed because row padding is different between * KTX (pad to 4) and Vulkan (none). Also region->bufferOffset, i.e. the start * of each image, has to be a multiple of 4 and also a multiple of the * element size. * * This should be used with @c ktx_Texture_IterateFaceLevels or * @c ktx_Texture_IterateLoadLevelFaces. Face-level iteration has been * selected to minimize the buffering needed between reading the file and * copying the data into the staging buffer. Obviously when * @c ktx_Texture_IterateFaceLevels is being used, this is a moot point. * * @copydetails PFNKTXITERCB */ KTX_error_code optimalTilingPadCallback(int miplevel, int face, int width, int height, int depth, ktx_uint64_t faceLodSize, void* pixels, void* userdata) { user_cbdata_optimal* ud = (user_cbdata_optimal*)userdata; ktx_uint32_t rowPitch = width * ud->elementSize; // Set bufferOffset in destination region in final image #if defined(_DEBUG) assert(ud->region < ud->regionsArrayEnd); #endif ud->region->bufferOffset = ud->offset; // Copy data into staging buffer if (_KTX_PAD_UNPACK_ALIGN_LEN(rowPitch) == 0) { // No padding. Can copy in bulk. memcpy(ud->dest + ud->offset, pixels, faceLodSize); ud->offset += faceLodSize; } else { // Must remove padding. Copy a row at a time. ktx_uint32_t image, imageIterations; ktx_int32_t row; ktx_uint32_t paddedRowPitch; if (ud->numDimensions == 3) imageIterations = depth; else if (ud->numLayers > 1) imageIterations = ud->numLayers * ud->numFaces; else imageIterations = 1; rowPitch = paddedRowPitch = width * ud->elementSize; paddedRowPitch = _KTX_PAD_UNPACK_ALIGN(paddedRowPitch); for (image = 0; image < imageIterations; image++) { for (row = 0; row < height; row++) { memcpy(ud->dest + ud->offset, pixels, rowPitch); ud->offset += rowPitch; pixels = (ktx_uint8_t*)pixels + paddedRowPitch; } } } // Round to needed multiples for next region, if necessary. if (ud->offset % ud->elementSize != 0 || ud->offset % 4 != 0) { // Only elementSizes of 1,2 and 3 will bring us here. assert(ud->elementSize < 4 && ud->elementSize > 0); ktx_uint32_t lcm = lcm4(ud->elementSize); ud->offset = _KTX_PADN(lcm, ud->offset); } // These 2 are expressed in texels; not suitable for dealing with padding. ud->region->bufferRowLength = 0; ud->region->bufferImageHeight = 0; ud->region->imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; ud->region->imageSubresource.mipLevel = miplevel; ud->region->imageSubresource.baseArrayLayer = face; ud->region->imageSubresource.layerCount = ud->numLayers * ud->numFaces; ud->region->imageOffset.x = 0; ud->region->imageOffset.y = 0; ud->region->imageOffset.z = 0; ud->region->imageExtent.width = width; ud->region->imageExtent.height = height; ud->region->imageExtent.depth = depth; ud->region += 1; return KTX_SUCCESS; } typedef struct user_cbdata_linear { ktxVulkanFunctions vkFuncs; VkImage destImage; VkDevice device; uint8_t* dest; // Pointer to mapped Image memory ktxTexture* texture; } user_cbdata_linear; /** * @internal * @~English * @brief Callback for linear tiled textures with no source row padding. * * Copy the image data into the mapped Vulkan image. */ KTX_error_code linearTilingCallback(int miplevel, int face, int width, int height, int depth, ktx_uint64_t faceLodSize, void* pixels, void* userdata) { user_cbdata_linear* ud = (user_cbdata_linear*)userdata; VkSubresourceLayout subResLayout; #if !defined(_MSC_VER) || _MSC_VER >= 1920 VkImageSubresource subRes = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .mipLevel = miplevel, .arrayLayer = face }; #else VkImageSubresource subRes = {0}; subRes.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; subRes.mipLevel = miplevel; subRes.arrayLayer = face; #endif UNUSED(width); UNUSED(height); UNUSED(depth); // Get sub resources layout. Includes row pitch, size, // offsets, etc. ud->vkFuncs.vkGetImageSubresourceLayout(ud->device, ud->destImage, &subRes, &subResLayout); // Copies all images of the miplevel (for array & 3d) or a single face. memcpy(ud->dest + subResLayout.offset, pixels, faceLodSize); return KTX_SUCCESS; } /** * @internal * @~English * @brief Callback for linear tiled textures with possible source row * padding. * * Need to use this long method as row padding is different * between KTX (pad to 4) and Vulkan (none). * * In theory this should work for the no-padding case too but it is much * clearer and a bit faster to use the simple callback above. It also avoids * potential Vulkan implementation bugs. * * I have seen weird subResLayout results with a BC2_UNORM texture in the only * real Vulkan implementation I have available (Mesa). The reported row & image * strides appears to be for an R8G8B8A8_UNORM of the same texel size. */ KTX_error_code linearTilingPadCallback(int miplevel, int face, int width, int height, int depth, ktx_uint64_t faceLodSize, void* pixels, void* userdata) { user_cbdata_linear* ud = (user_cbdata_linear*)userdata; VkDeviceSize offset; ktx_size_t imageSize = 0; VkDeviceSize imagePitch = 0; ktx_uint32_t srcRowPitch; ktx_uint32_t rowIterations; ktx_uint32_t imageIterations; ktx_uint32_t row, image; ktx_uint8_t* pSrc; ktx_size_t copySize; VkSubresourceLayout subResLayout; #if !defined(_MSC_VER) || _MSC_VER >= 1920 VkImageSubresource subRes = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .mipLevel = miplevel, .arrayLayer = face }; #else VkImageSubresource subRes = {0}; subRes.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; subRes.mipLevel = miplevel; subRes.arrayLayer = face; #endif UNUSED(width); // Get sub resources layout. Includes row pitch, size, // offsets, etc. ud->vkFuncs.vkGetImageSubresourceLayout(ud->device, ud->destImage, &subRes, &subResLayout); srcRowPitch = ktxTexture_GetRowPitch(ud->texture, miplevel); if (subResLayout.rowPitch != srcRowPitch) rowIterations = height; else rowIterations = 1; imageIterations = 1; // Arrays, including cube map arrays, or 3D textures // Note from the Vulkan spec: // * arrayPitch is undefined for images that were not // created as arrays. // * depthPitch is defined only for 3D images. if (ud->texture->numLayers > 1 || ud->texture->numDimensions == 3) { imageSize = ktxTexture_GetImageSize(ud->texture, miplevel); if (ud->texture->numLayers > 1) { imagePitch = subResLayout.arrayPitch; if (imagePitch != imageSize) imageIterations = ud->texture->numLayers * ud->texture->numFaces; } else { imagePitch = subResLayout.depthPitch; if (imagePitch != imageSize) imageIterations = depth; } assert(imageSize <= imagePitch); } if (rowIterations > 1) { // Copy the minimum of srcRowPitch, the GL_UNPACK_ALIGNMENT padded size, // and subResLayout.rowPitch. if (subResLayout.rowPitch < srcRowPitch) copySize = subResLayout.rowPitch; else copySize = srcRowPitch; } else if (imageIterations > 1) copySize = faceLodSize / imageIterations; else copySize = faceLodSize; offset = subResLayout.offset; // Copy image data to destImage via its mapped memory. for (image = 0; image < imageIterations; image++) { pSrc = (ktx_uint8_t*)pixels + imageSize * image; for (row = 0; row < rowIterations; row++) { memcpy(ud->dest + offset, pSrc, copySize); offset += subResLayout.rowPitch; pSrc += srcRowPitch; } offset += imagePitch; } return KTX_SUCCESS; } /** * @memberof ktxTexture * @~English * @brief Create a Vulkan image object from a ktxTexture object. * * Creates a VkImage with @c VkFormat etc. matching the KTX data and uploads * the images. Mipmaps will be generated if the @c ktxTexture's * @c generateMipmaps flag is set. Returns the handles of the created objects * and information about the texture in the @c ktxVulkanTexture pointed at by * @p vkTexture. * * The created VkImage will have @c VK_SHARING_MODE_EXCLUSIVE set thus the * resulting image will be usable only with queues of the same family as * the @c queue in the ktxVulkanDeviceInfo pointed to by @a vdi. * * @p usageFlags and thus acceptable usage of the created image may be * augmented as follows: * - with @c VK_IMAGE_USAGE_TRANSFER_DST_BIT if @p tiling is * @c VK_IMAGE_TILING_OPTIMAL * - with VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT * if @c generateMipmaps is set in the @c ktxTexture. * * Most Vulkan implementations support @c VK_IMAGE_TILING_LINEAR only for a very * limited number of formats and features. Generally @c VK_IMAGE_TILING_OPTIMAL * is preferred. The latter requires a staging buffer so will use more memory * during loading. * * If a pointer to a set of suballocator callbacks is provided, they * will be used instead of manual allocation of VkDeviceMemory. A 64 bit uint * that references the suballocated page(s) is returned on memory procurement * and saved in the @c allocationId field of the structure pointed to by @a vkTexture. * * @param[in] This pointer to the ktxTexture from which to upload. * @param [in] vdi pointer to a ktxVulkanDeviceInfo structure providing * information about the Vulkan device onto which to * load the texture. * @param [in,out] vkTexture pointer to a ktxVulkanTexture structure into which * the function writes information about the created * VkImage. * @param [in] tiling type of tiling to use in the destination image * on the Vulkan device. * @param [in] usageFlags a set of VkImageUsageFlags bits indicating the * intended usage of the destination image. * @param [in] finalLayout a VkImageLayout value indicating the desired * final layout of the created image. * @param [in] subAllocatorCallbacks pointer to a set of suballocator callbacks * that wrap around suballocator calls: alloc, * bindbuffer, bindimage, map, unmap and free. * They use a uint64_t stored in the @c allocationId * field of the structure pointed at by @a vkTexture * to reference allocated page(s). * * @return KTX_SUCCESS on success, other KTX_* enum values on error. * * @exception KTX_INVALID_VALUE An incomplete set of callbacks are provided in * subAllocatorCallbacks. * @exception KTX_INVALID_VALUE @p This, @p vdi or @p vkTexture is @c NULL. * @exception KTX_INVALID_OPERATION The ktxTexture contains neither images nor * an active stream from which to read them. * @exception KTX_INVALID_OPERATION The combination of the ktxTexture's format, * @p tiling and @p usageFlags is not supported * by the physical device. * @exception KTX_INVALID_OPERATION Requested mipmap generation is not supported * by the physical device for the combination * of the ktxTexture's format and @p tiling. * @exception KTX_INVALID_OPERATION Number of mip levels or array layers exceeds * the maximums supported for the ktxTexture's * format and @p tiling. * @exception KTX_OUT_OF_MEMORY Sufficient memory could not be allocated on * either the CPU or the Vulkan device. * @exception KTX_UNSUPPORTED_FEATURE Attempting to sparsely bind KTX textures * for the time being will report this error. * * @sa @ref ktxVulkanDeviceInfo::ktxVulkanDeviceInfo\_Construct "ktxVulkanDeviceInfo_Construct()" */ KTX_error_code ktxTexture_VkUploadEx_WithSuballocator(ktxTexture* This, ktxVulkanDeviceInfo* vdi, ktxVulkanTexture* vkTexture, VkImageTiling tiling, VkImageUsageFlags usageFlags, VkImageLayout finalLayout, ktxVulkanTexture_subAllocatorCallbacks* subAllocatorCallbacks) { KTX_error_code kResult; VkFilter blitFilter = VK_FILTER_LINEAR; VkFormat vkFormat; VkImageType imageType; VkImageViewType viewType; VkImageCreateFlags createFlags = 0; VkImageFormatProperties imageFormatProperties; VkResult vResult; VkCommandBufferBeginInfo cmdBufBeginInfo = { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, .pNext = NULL }; VkImageCreateInfo imageCreateInfo = { .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .pNext = NULL }; VkMemoryAllocateInfo memAllocInfo = { .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, .pNext = NULL, .allocationSize = 0, .memoryTypeIndex = 0 }; VkMemoryRequirements memReqs; ktx_uint32_t numImageLayers, numImageLevels; ktx_uint32_t elementSize = ktxTexture_GetElementSize(This); ktx_bool_t canUseFasterPath; ktx_bool_t useSuballocator = false; if (subAllocatorCallbacks) { if (subAllocatorCallbacks->allocMemFuncPtr && subAllocatorCallbacks->bindBufferFuncPtr && subAllocatorCallbacks->bindImageFuncPtr && subAllocatorCallbacks->memoryMapFuncPtr && subAllocatorCallbacks->memoryUnmapFuncPtr && subAllocatorCallbacks->freeMemFuncPtr) useSuballocator = true; else return KTX_INVALID_VALUE; } if (!vdi || !This || !vkTexture) { return KTX_INVALID_VALUE; } if (!This->pData && !ktxTexture_isActiveStream(This)) { /* Nothing to upload. */ return KTX_INVALID_OPERATION; } /* _ktxCheckHeader should have caught this. */ assert(This->numFaces == 6 ? This->numDimensions == 2 : VK_TRUE); numImageLayers = This->numLayers; if (This->isCubemap) { numImageLayers *= 6; createFlags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; } assert(This->numDimensions >= 1 && This->numDimensions <= 3); switch (This->numDimensions) { case 1: imageType = VK_IMAGE_TYPE_1D; viewType = This->isArray ? VK_IMAGE_VIEW_TYPE_1D_ARRAY : VK_IMAGE_VIEW_TYPE_1D; break; case 2: default: // To keep compilers happy. imageType = VK_IMAGE_TYPE_2D; if (This->isCubemap) viewType = This->isArray ? VK_IMAGE_VIEW_TYPE_CUBE_ARRAY : VK_IMAGE_VIEW_TYPE_CUBE; else viewType = This->isArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; break; case 3: imageType = VK_IMAGE_TYPE_3D; /* 3D array textures not supported in Vulkan. Attempts to create or * load them should have been trapped long before this. */ assert(!This->isArray); viewType = VK_IMAGE_VIEW_TYPE_3D; break; } vkFormat = ktxTexture_GetVkFormat(This); if (vkFormat == VK_FORMAT_UNDEFINED) { return KTX_INVALID_OPERATION; } /* Get device properties for the requested image format */ if (tiling == VK_IMAGE_TILING_OPTIMAL) { // Ensure we can copy from staging buffer to image. usageFlags |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; } if (This->generateMipmaps) { // Ensure we can blit between levels. usageFlags |= (VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); } vResult = vdi->vkFuncs.vkGetPhysicalDeviceImageFormatProperties(vdi->physicalDevice, vkFormat, imageType, tiling, usageFlags, createFlags, &imageFormatProperties); if (vResult == VK_ERROR_FORMAT_NOT_SUPPORTED) { return KTX_INVALID_OPERATION; } if (This->numLayers > imageFormatProperties.maxArrayLayers) { return KTX_INVALID_OPERATION; } if (This->generateMipmaps) { uint32_t max_dim; VkFormatProperties formatProperties; VkFormatFeatureFlags formatFeatureFlags; VkFormatFeatureFlags neededFeatures = VK_FORMAT_FEATURE_BLIT_DST_BIT | VK_FORMAT_FEATURE_BLIT_SRC_BIT; vdi->vkFuncs.vkGetPhysicalDeviceFormatProperties(vdi->physicalDevice, vkFormat, &formatProperties); assert(vResult == VK_SUCCESS); if (tiling == VK_IMAGE_TILING_OPTIMAL) formatFeatureFlags = formatProperties.optimalTilingFeatures; else formatFeatureFlags = formatProperties.linearTilingFeatures; if ((formatFeatureFlags & neededFeatures) != neededFeatures) return KTX_INVALID_OPERATION; if (formatFeatureFlags & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) blitFilter = VK_FILTER_LINEAR; else blitFilter = VK_FILTER_NEAREST; // XXX INVALID_OP? max_dim = MAX(MAX(This->baseWidth, This->baseHeight), This->baseDepth); numImageLevels = (uint32_t)floor(log2(max_dim)) + 1; } else { numImageLevels = This->numLevels; } if (numImageLevels > imageFormatProperties.maxMipLevels) { return KTX_INVALID_OPERATION; } if (This->classId == ktxTexture2_c) { canUseFasterPath = KTX_TRUE; } else { ktx_uint32_t actualRowPitch = ktxTexture_GetRowPitch(This, 0); ktx_uint32_t tightRowPitch = elementSize * This->baseWidth; // If the texture's images do not have any row padding, we can use a // faster path. Only uncompressed textures might have padding. // // The first test in the if will match compressed textures, because // they all have a block size that is a multiple of 4, as well as // a class of uncompressed textures that will never need padding. // // The second test matches textures whose level 0 has no padding. Any // texture whose block size is not a multiple of 4 will need padding // at some miplevel even if level 0 does not. So, if more than 1 level // exists, we must use the slower path. // // Note all elementSizes > 4 Will be a multiple of 4, so only // elementSizes of 1, 2 & 3 are a concern here. if (elementSize % 4 == 0 /* There'll be no padding at any level. */ /* There is no padding at level 0 and no other levels. */ || (This->numLevels == 1 && actualRowPitch == tightRowPitch)) canUseFasterPath = KTX_TRUE; else canUseFasterPath = KTX_FALSE; } vkTexture->width = This->baseWidth; vkTexture->height = This->baseHeight; vkTexture->depth = This->baseDepth; vkTexture->imageLayout = finalLayout; vkTexture->imageFormat = vkFormat; vkTexture->levelCount = numImageLevels; vkTexture->layerCount = numImageLayers; vkTexture->viewType = viewType; vkTexture->vkDestroyImage = vdi->vkFuncs.vkDestroyImage; vkTexture->vkFreeMemory = vdi->vkFuncs.vkFreeMemory; VK_CHECK_RESULT( vdi->vkFuncs.vkBeginCommandBuffer(vdi->cmdBuffer, &cmdBufBeginInfo) ); if (tiling == VK_IMAGE_TILING_OPTIMAL) { // Create a host-visible staging buffer that contains the raw image data VkBuffer stagingBuffer; VkDeviceMemory stagingMemory = VK_NULL_HANDLE; VkBufferImageCopy* copyRegions; VkDeviceSize textureSize; VkBufferCreateInfo bufferCreateInfo = { .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .pNext = NULL }; VkImageSubresourceRange subresourceRange; VkFence copyFence; VkFenceCreateInfo fenceCreateInfo = { .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, .pNext = NULL, .flags = VK_FLAGS_NONE }; VkSubmitInfo submitInfo = { .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, .pNext = NULL }; ktx_uint8_t* pMappedStagingBuffer; ktx_uint32_t numCopyRegions; user_cbdata_optimal cbData; textureSize = ktxTexture_GetDataSizeUncompressed(This); bufferCreateInfo.size = textureSize; if (canUseFasterPath) { /* * Because all array layers and faces are the same size they can * be copied in a single operation so there'll be 1 copy per mip * level. */ numCopyRegions = This->numLevels; } else { /* * Have to copy all images individually into the staging * buffer so we can place them at correct multiples of * elementSize and 4 and also need a copy region per image * in case they end up with padding between them. */ numCopyRegions = This->isArray ? This->numLevels : This->numLevels * This->numFaces; /* * Add extra space to allow for possible padding described * above. A bit ad-hoc but it's only a small amount of * memory. */ bufferCreateInfo.size += numCopyRegions * elementSize * 4; } copyRegions = (VkBufferImageCopy*)malloc(sizeof(VkBufferImageCopy) * numCopyRegions); if (copyRegions == NULL) { return KTX_OUT_OF_MEMORY; } // This buffer is used as a transfer source for the buffer copy bufferCreateInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; bufferCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; VK_CHECK_RESULT( vdi->vkFuncs.vkCreateBuffer(vdi->device, &bufferCreateInfo, vdi->pAllocator, &stagingBuffer)); // Get memory requirements for the staging buffer (alignment, // memory type bits) vdi->vkFuncs.vkGetBufferMemoryRequirements(vdi->device, stagingBuffer, &memReqs); memAllocInfo.allocationSize = memReqs.size; // Get memory type index for a host visible buffer memAllocInfo.memoryTypeIndex = ktxVulkanDeviceInfo_getMemoryType( vdi, memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT ); uint64_t stagingAllocId = 0ull; if (!useSuballocator) { vResult = vdi->vkFuncs.vkAllocateMemory(vdi->device, &memAllocInfo, vdi->pAllocator, &stagingMemory); if (vResult != VK_SUCCESS) { free(copyRegions); return KTX_OUT_OF_MEMORY; } VK_CHECK_RESULT( vdi->vkFuncs.vkBindBufferMemory(vdi->device, stagingBuffer, stagingMemory, 0)); VK_CHECK_RESULT( vdi->vkFuncs.vkMapMemory(vdi->device, stagingMemory, 0, memReqs.size, 0, (void**)&pMappedStagingBuffer)); } else { uint64_t numPages = 0ull; stagingAllocId = subAllocatorCallbacks->allocMemFuncPtr(&memAllocInfo, &memReqs, &numPages); if (stagingAllocId == 0ull) { free(copyRegions); return KTX_OUT_OF_MEMORY; } if (numPages > 1ull) { // Sparse binding of KTX textures is unsupported for the moment free(copyRegions); return KTX_UNSUPPORTED_FEATURE; } VK_CHECK_RESULT( subAllocatorCallbacks->bindBufferFuncPtr(stagingBuffer, stagingAllocId)); VK_CHECK_RESULT( subAllocatorCallbacks->memoryMapFuncPtr(stagingAllocId, 0ull, &memReqs.size, (void**)&pMappedStagingBuffer)); } cbData.offset = 0; cbData.region = copyRegions; cbData.numFaces = This->numFaces; cbData.numLayers = This->numLayers; cbData.dest = pMappedStagingBuffer; cbData.elementSize = elementSize; cbData.numDimensions = This->numDimensions; #if defined(_DEBUG) cbData.regionsArrayEnd = copyRegions + numCopyRegions; #endif if (canUseFasterPath) { // Bulk load the data to the staging buffer and iterate // over levels. if (This->pData) { // Image data has already been loaded. Copy to staging // buffer. assert(This->dataSize <= memAllocInfo.allocationSize); memcpy(pMappedStagingBuffer, This->pData, This->dataSize); } else { /* Load the image data directly into the staging buffer. */ /* The strange cast quiets an Xcode warning when building * for the Generic iOS Device where size_t is 32-bit even * when building for arm64. */ kResult = ktxTexture_LoadImageData(This, pMappedStagingBuffer, (ktx_size_t)memAllocInfo.allocationSize); if (kResult != KTX_SUCCESS) return kResult; } // Iterate over mip levels to set up the copy regions. kResult = ktxTexture_IterateLevels(This, optimalTilingCallback, &cbData); // XXX Check for possible errors. } else { // Iterate over face-levels with callback that copies the // face-levels to Vulkan-valid offsets in the staging buffer while // removing padding. Using face-levels minimizes pre-staging-buffer // buffering, in the event the data is not already loaded. if (This->pData) { kResult = ktxTexture_IterateLevelFaces( This, optimalTilingPadCallback, &cbData); } else { kResult = ktxTexture_IterateLoadLevelFaces( This, optimalTilingPadCallback, &cbData); // XXX Check for possible errors. } } if (!useSuballocator) vdi->vkFuncs.vkUnmapMemory(vdi->device, stagingMemory); else subAllocatorCallbacks->memoryUnmapFuncPtr(stagingAllocId, 0ull); // Create optimal tiled target image imageCreateInfo.imageType = imageType; imageCreateInfo.flags = createFlags; imageCreateInfo.format = vkFormat; // numImageLevels ensures enough levels for generateMipmaps. imageCreateInfo.mipLevels = numImageLevels; imageCreateInfo.arrayLayers = numImageLayers; imageCreateInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageCreateInfo.tiling = VK_IMAGE_TILING_OPTIMAL; imageCreateInfo.usage = usageFlags; imageCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; imageCreateInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; imageCreateInfo.extent.width = vkTexture->width; imageCreateInfo.extent.height = vkTexture->height; imageCreateInfo.extent.depth = vkTexture->depth; VK_CHECK_RESULT( vdi->vkFuncs.vkCreateImage(vdi->device, &imageCreateInfo, vdi->pAllocator, &vkTexture->image)); vdi->vkFuncs.vkGetImageMemoryRequirements(vdi->device, vkTexture->image, &memReqs); memAllocInfo.allocationSize = memReqs.size; memAllocInfo.memoryTypeIndex = ktxVulkanDeviceInfo_getMemoryType( vdi, memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); if (!useSuballocator) { VK_CHECK_RESULT( vdi->vkFuncs.vkAllocateMemory(vdi->device, &memAllocInfo, vdi->pAllocator, &vkTexture->deviceMemory)); VK_CHECK_RESULT( vdi->vkFuncs.vkBindImageMemory(vdi->device, vkTexture->image, vkTexture->deviceMemory, 0)); } else { uint64_t numPages = 0ull; vkTexture->allocationId = subAllocatorCallbacks->allocMemFuncPtr(&memAllocInfo, &memReqs, &numPages); if (vkTexture->allocationId == 0ull) { return KTX_OUT_OF_MEMORY; } if(numPages > 1ull) { // Sparse binding of KTX textures is unsupported for the moment return KTX_UNSUPPORTED_FEATURE; } VK_CHECK_RESULT( subAllocatorCallbacks->bindImageFuncPtr(vkTexture->image, vkTexture->allocationId)); } subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; subresourceRange.baseMipLevel = 0; subresourceRange.levelCount = This->numLevels; subresourceRange.baseArrayLayer = 0; subresourceRange.layerCount = numImageLayers; // Image barrier to transition, possibly only the base level, image // layout to TRANSFER_DST_OPTIMAL so it can be used as the copy // destination. setImageLayout( vdi->vkFuncs, vdi->cmdBuffer, vkTexture->image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, subresourceRange); // Copy mip levels from staging buffer vdi->vkFuncs.vkCmdCopyBufferToImage( vdi->cmdBuffer, stagingBuffer, vkTexture->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, numCopyRegions, copyRegions ); free(copyRegions); if (This->generateMipmaps) { generateMipmaps(vkTexture, vdi, blitFilter, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); } else { // Transition image layout to finalLayout after all mip levels // have been copied. // In this case numImageLevels == This->numLevels //subresourceRange.levelCount = numImageLevels; setImageLayout( vdi->vkFuncs, vdi->cmdBuffer, vkTexture->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, finalLayout, subresourceRange); } // Submit command buffer containing copy and image layout commands VK_CHECK_RESULT( vdi->vkFuncs.vkEndCommandBuffer(vdi->cmdBuffer)); // Create a fence to make sure that the copies have finished before // continuing VK_CHECK_RESULT( vdi->vkFuncs.vkCreateFence(vdi->device, &fenceCreateInfo, vdi->pAllocator, ©Fence)); submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &vdi->cmdBuffer; VK_CHECK_RESULT( vdi->vkFuncs.vkQueueSubmit(vdi->queue, 1, &submitInfo, copyFence)); VK_CHECK_RESULT( vdi->vkFuncs.vkWaitForFences(vdi->device, 1, ©Fence, VK_TRUE, DEFAULT_FENCE_TIMEOUT)); vdi->vkFuncs.vkDestroyFence(vdi->device, copyFence, vdi->pAllocator); // Clean up staging resources if (!useSuballocator) vdi->vkFuncs.vkFreeMemory(vdi->device, stagingMemory, vdi->pAllocator); else subAllocatorCallbacks->freeMemFuncPtr(stagingAllocId); vdi->vkFuncs.vkDestroyBuffer(vdi->device, stagingBuffer, vdi->pAllocator); } else { VkImage mappableImage; VkDeviceMemory mappableMemory = VK_NULL_HANDLE; VkFence nullFence = { VK_NULL_HANDLE }; VkSubmitInfo submitInfo = { .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, .pNext = NULL }; user_cbdata_linear cbData; PFNKTXITERCB callback; imageCreateInfo.imageType = imageType; imageCreateInfo.flags = createFlags; imageCreateInfo.format = vkFormat; imageCreateInfo.extent.width = vkTexture->width; imageCreateInfo.extent.height = vkTexture->height; imageCreateInfo.extent.depth = vkTexture->depth; // numImageLevels ensures enough levels for generateMipmaps. imageCreateInfo.mipLevels = numImageLevels; imageCreateInfo.arrayLayers = numImageLayers; imageCreateInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageCreateInfo.tiling = VK_IMAGE_TILING_LINEAR; imageCreateInfo.usage = usageFlags; imageCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; imageCreateInfo.initialLayout = VK_IMAGE_LAYOUT_PREINITIALIZED; // Load mip map level 0 to linear tiling image VK_CHECK_RESULT( vdi->vkFuncs.vkCreateImage(vdi->device, &imageCreateInfo, vdi->pAllocator, &mappableImage)); // Get memory requirements for this image // like size and alignment vdi->vkFuncs.vkGetImageMemoryRequirements(vdi->device, mappableImage, &memReqs); // Set memory allocation size to required memory size memAllocInfo.allocationSize = memReqs.size; // Get memory type that can be mapped to host memory memAllocInfo.memoryTypeIndex = ktxVulkanDeviceInfo_getMemoryType( vdi, memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); // Allocate host memory if (!useSuballocator) { vResult = vdi->vkFuncs.vkAllocateMemory(vdi->device, &memAllocInfo, vdi->pAllocator, &mappableMemory); if (vResult != VK_SUCCESS) { return KTX_OUT_OF_MEMORY; } VK_CHECK_RESULT( vdi->vkFuncs.vkBindImageMemory(vdi->device, mappableImage, mappableMemory, 0)); } else { uint64_t numPages = 0ull; vkTexture->allocationId = subAllocatorCallbacks->allocMemFuncPtr(&memAllocInfo, &memReqs, &numPages); if (vkTexture->allocationId == 0ull) { return KTX_OUT_OF_MEMORY; } if (numPages > 1ull) { // Sparse binding of KTX textures is unsupported for the moment return KTX_UNSUPPORTED_FEATURE; } VK_CHECK_RESULT( subAllocatorCallbacks->bindImageFuncPtr(mappableImage, vkTexture->allocationId)); } cbData.vkFuncs = vdi->vkFuncs; cbData.destImage = mappableImage; cbData.device = vdi->device; cbData.texture = This; callback = canUseFasterPath ? linearTilingCallback : linearTilingPadCallback; // Map image memory if (!useSuballocator) { VK_CHECK_RESULT( vdi->vkFuncs.vkMapMemory(vdi->device, mappableMemory, 0, memReqs.size, 0, (void**)&cbData.dest)); } else { VK_CHECK_RESULT( subAllocatorCallbacks->memoryMapFuncPtr(vkTexture->allocationId, 0ull, &memReqs.size, (void**)&cbData.dest)); } // Iterate over images to copy texture data into mapped image memory. if (ktxTexture_isActiveStream(This)) { kResult = ktxTexture_IterateLoadLevelFaces(This, callback, &cbData); } else { kResult = ktxTexture_IterateLevelFaces(This, callback, &cbData); } // XXX Check for possible errors if (!useSuballocator) vdi->vkFuncs.vkUnmapMemory(vdi->device, mappableMemory); else subAllocatorCallbacks->memoryUnmapFuncPtr(vkTexture->allocationId, 0ull); // Linear tiled images can be directly used as textures. vkTexture->image = mappableImage; if (!useSuballocator) vkTexture->deviceMemory = mappableMemory; if (This->generateMipmaps) { generateMipmaps(vkTexture, vdi, blitFilter, VK_IMAGE_LAYOUT_PREINITIALIZED); } else { VkImageSubresourceRange subresourceRange; subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; subresourceRange.baseMipLevel = 0; subresourceRange.levelCount = numImageLevels; subresourceRange.baseArrayLayer = 0; subresourceRange.layerCount = numImageLayers; // Transition image layout to finalLayout. setImageLayout( vdi->vkFuncs, vdi->cmdBuffer, vkTexture->image, VK_IMAGE_LAYOUT_PREINITIALIZED, finalLayout, subresourceRange); } // Submit command buffer containing image layout commands VK_CHECK_RESULT(vdi->vkFuncs.vkEndCommandBuffer(vdi->cmdBuffer)); submitInfo.waitSemaphoreCount = 0; submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &vdi->cmdBuffer; VK_CHECK_RESULT(vdi->vkFuncs.vkQueueSubmit(vdi->queue, 1, &submitInfo, nullFence)); VK_CHECK_RESULT(vdi->vkFuncs.vkQueueWaitIdle(vdi->queue)); } return KTX_SUCCESS; } /** @memberof ktxTexture * @~English * @brief Create a Vulkan image object from a ktxTexture object. * * Calls ktxTexture\_VkUploadEx\_WithSuballocator() with no supplied * suballocator callbacks. Use that for complete control. */ KTX_error_code ktxTexture_VkUploadEx(ktxTexture* This, ktxVulkanDeviceInfo* vdi, ktxVulkanTexture* vkTexture, VkImageTiling tiling, VkImageUsageFlags usageFlags, VkImageLayout finalLayout) { return ktxTexture_VkUploadEx_WithSuballocator(This, vdi, vkTexture, tiling, usageFlags, finalLayout, NULL); } /** * @memberof ktxTexture * @~English * @brief Create a Vulkan image object from a ktxTexture object. * * Calls ktxTexture\_VkUploadEx() with the most commonly used options: * @c VK_IMAGE_TILING_OPTIMAL, * @c VK_IMAGE_USAGE_SAMPLED_BIT and * @c VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL. Use that for complete * control. */ KTX_error_code ktxTexture_VkUpload(ktxTexture* texture, ktxVulkanDeviceInfo* vdi, ktxVulkanTexture *vkTexture) { return ktxTexture_VkUploadEx(ktxTexture(texture), vdi, vkTexture, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); } /** @memberof ktxTexture1 * @~English * @brief Create a Vulkan image object from a ktxTexture1 object. * * This simply calls @ref ktxTexture::ktxTexture\_VkUploadEx_WithSuballocator * "ktxTexture_VkUploadEx_WithSuballocator()" * * @copydetails ktxTexture::ktxTexture_VkUploadEx_WithSuballocator */ KTX_error_code ktxTexture1_VkUploadEx_WithSuballocator(ktxTexture1* This, ktxVulkanDeviceInfo* vdi, ktxVulkanTexture* vkTexture, VkImageTiling tiling, VkImageUsageFlags usageFlags, VkImageLayout finalLayout, ktxVulkanTexture_subAllocatorCallbacks* subAllocatorCallbacks) { return ktxTexture_VkUploadEx_WithSuballocator(ktxTexture(This), vdi, vkTexture, tiling, usageFlags, finalLayout, subAllocatorCallbacks); } /** @memberof ktxTexture1 * @~English * @brief Create a Vulkan image object from a ktxTexture1 object. * * This simply calls @ref ktxTexture::ktxTexture\_VkUploadEx "ktxTexture_VkUploadEx()". */ KTX_error_code ktxTexture1_VkUploadEx(ktxTexture1* This, ktxVulkanDeviceInfo* vdi, ktxVulkanTexture* vkTexture, VkImageTiling tiling, VkImageUsageFlags usageFlags, VkImageLayout finalLayout) { return ktxTexture_VkUploadEx(ktxTexture(This), vdi, vkTexture, tiling, usageFlags, finalLayout); } /** @memberof ktxTexture1 * @~English * @brief Create a Vulkan image object from a ktxTexture1 object. * * Calls @ref ktxTexture::ktxTexture\_VkUploadEx "ktxTexture_VkUploadEx()" with * the most commonly used options: @c VK_IMAGE_TILING_OPTIMAL, * @c VK_IMAGE_USAGE_SAMPLED_BIT and * @c VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL. Use that for complete control. */ KTX_error_code ktxTexture1_VkUpload(ktxTexture1* texture, ktxVulkanDeviceInfo* vdi, ktxVulkanTexture *vkTexture) { return ktxTexture_VkUploadEx(ktxTexture(texture), vdi, vkTexture, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); } /** @memberof ktxTexture2 * @~English * @brief Create a Vulkan image object from a ktxTexture2 object. * * This simplly calls @ref ktxTexture::ktxTexture_VkUploadEx_WithSuballocator * "ktxTexture_VkUploadEx_WithSuballocator()". * * @copydetails ktxTexture::ktxTexture_VkUploadEx_WithSuballocator */ KTX_error_code ktxTexture2_VkUploadEx_WithSuballocator(ktxTexture2* This, ktxVulkanDeviceInfo* vdi, ktxVulkanTexture* vkTexture, VkImageTiling tiling, VkImageUsageFlags usageFlags, VkImageLayout finalLayout, ktxVulkanTexture_subAllocatorCallbacks* subAllocatorCallbacks) { return ktxTexture_VkUploadEx_WithSuballocator(ktxTexture(This), vdi, vkTexture, tiling, usageFlags, finalLayout, subAllocatorCallbacks); } /** @memberof ktxTexture2 * @~English * @brief Create a Vulkan image object from a ktxTexture2 object. * * This simply calls @ref ktxTexture::ktxTexture\_VkUploadEx "ktxTexture_VkUploadEx()". */ KTX_error_code ktxTexture2_VkUploadEx(ktxTexture2* This, ktxVulkanDeviceInfo* vdi, ktxVulkanTexture* vkTexture, VkImageTiling tiling, VkImageUsageFlags usageFlags, VkImageLayout finalLayout) { return ktxTexture_VkUploadEx(ktxTexture(This), vdi, vkTexture, tiling, usageFlags, finalLayout); } /** @memberof ktxTexture2 * @~English * @brief Create a Vulkan image object from a ktxTexture2 object. * * Calls @ref ktxTexture::ktxTexture\_VkUploadEx "ktxTexture_VkUploadEx()" with * the most commonly used options: @c VK_IMAGE_TILING_OPTIMAL, * @c VK_IMAGE_USAGE_SAMPLED_BIT and * @c VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL. Use that for complete control. */ KTX_error_code ktxTexture2_VkUpload(ktxTexture2* This, ktxVulkanDeviceInfo* vdi, ktxVulkanTexture *vkTexture) { return ktxTexture_VkUploadEx(ktxTexture(This), vdi, vkTexture, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); } /** @memberof ktxTexture1 * @~English * @brief Return the VkFormat enum of a ktxTexture1 object. * * @return The VkFormat of the texture object. May return VK_FORMAT_UNDEFINED if * there is no mapping from the GL internalformat and format. */ VkFormat ktxTexture1_GetVkFormat(ktxTexture1* This) { VkFormat vkFormat; vkFormat = vkGetFormatFromOpenGLInternalFormat(This->glInternalformat); if (vkFormat == VK_FORMAT_UNDEFINED) { vkFormat = vkGetFormatFromOpenGLFormat(This->glFormat, This->glType); } return vkFormat; } /** @memberof ktxTexture2 * @~English * @brief Return the VkFormat enum of a ktxTexture2 object. * * @return The VkFormat of the texture object. */ VkFormat ktxTexture2_GetVkFormat(ktxTexture2* This) { return This->vkFormat; } /** @memberof ktxTexture * @~English * @brief Return the VkFormat enum of a ktxTexture object. * * In ordert to ensure that the Vulkan uploader is not linked into an application unless explicitly called, * this is not a virtual function. It determines the texture type then dispatches to the correct function. * @sa @ref ktxTexture1::ktxTexture1_GetVkFormat "ktxTexture1_GetVkFormat()" * @sa @ref ktxTexture2::ktxTexture2_GetVkFormat "ktxTexture2_GetVkFormat()" */ VkFormat ktxTexture_GetVkFormat(ktxTexture* This) { if (This->classId == ktxTexture2_c) return ktxTexture2_GetVkFormat((ktxTexture2*)This); else return ktxTexture1_GetVkFormat((ktxTexture1*)This); } //====================================================================== // Utilities //====================================================================== /** * @internal * @~English * @brief Create an image memory barrier for changing the layout of an image. * * The barrier is placed in the passed command buffer. See the Vulkan spec. * chapter 11.4 "Image Layout" for details. */ static void setImageLayout( ktxVulkanFunctions vkFuncs, VkCommandBuffer cmdBuffer, VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout, VkImageSubresourceRange subresourceRange) { // Create an image barrier object VkImageMemoryBarrier imageMemoryBarrier = { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .pNext = NULL, // Some default values .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED }; imageMemoryBarrier.oldLayout = oldLayout; imageMemoryBarrier.newLayout = newLayout; imageMemoryBarrier.image = image; imageMemoryBarrier.subresourceRange = subresourceRange; // Source layouts (old) // The source access mask controls actions to be finished on the old // layout before it will be transitioned to the new layout. switch (oldLayout) { case VK_IMAGE_LAYOUT_UNDEFINED: // Image layout is undefined (or does not matter). // Only valid as initial layout. No flags required. imageMemoryBarrier.srcAccessMask = 0; break; case VK_IMAGE_LAYOUT_PREINITIALIZED: // Image is preinitialized. // Only valid as initial layout for linear images; preserves memory // contents. Make sure host writes have finished. imageMemoryBarrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT; break; case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: // Image is a color attachment. // Make sure writes to the color buffer have finished imageMemoryBarrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; break; case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: // Image is a depth/stencil attachment. // Make sure any writes to the depth/stencil buffer have finished. imageMemoryBarrier.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; break; case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: // Image is a transfer source. // Make sure any reads from the image have finished imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; break; case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: // Image is a transfer destination. // Make sure any writes to the image have finished. imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; break; case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: // Image is read by a shader. // Make sure any shader reads from the image have finished imageMemoryBarrier.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; break; default: /* Value not used by callers, so not supported. */ assert(KTX_FALSE); } // Target layouts (new) // The destination access mask controls the dependency for the new image // layout. switch (newLayout) { case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: // Image will be used as a transfer destination. // Make sure any writes to the image have finished. imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; break; case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: // Image will be used as a transfer source. // Make sure any reads from and writes to the image have finished. imageMemoryBarrier.srcAccessMask |= VK_ACCESS_TRANSFER_READ_BIT; imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; break; case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: // Image will be used as a color attachment. // Make sure any writes to the color buffer have finished. imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; imageMemoryBarrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; break; case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: // Image layout will be used as a depth/stencil attachment. // Make sure any writes to depth/stencil buffer have finished. imageMemoryBarrier.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; break; case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: // Image will be read in a shader (sampler, input attachment). // Make sure any writes to the image have finished. if (imageMemoryBarrier.srcAccessMask == 0) { imageMemoryBarrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; } imageMemoryBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; break; default: /* Value not used by callers, so not supported. */ assert(KTX_FALSE); } // Put barrier on top of pipeline. VkPipelineStageFlags srcStageFlags = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT; VkPipelineStageFlags destStageFlags = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT; // Add the barrier to the passed command buffer vkFuncs.vkCmdPipelineBarrier( cmdBuffer, srcStageFlags, destStageFlags, 0, 0, NULL, 0, NULL, 1, &imageMemoryBarrier); } /** @internal * @~English * @brief Generate mipmaps from base using @c VkCmdBlitImage. * * Mipmaps are generated by blitting level n from level n-1 as it should * be faster than the alternative of blitting all levels from the base level. * * After generation, the image is transitioned to the layout indicated by * @c vkTexture->imageLayout. * * @param[in] vkTexture pointer to an object with information about the * image for which to generate mipmaps. * @param[in] vdi pointer to an object with information about the * Vulkan device and command buffer to use. * @param[in] blitFilter the type of filter to use in the @c VkCmdBlitImage. * @param[in] initialLayout the layout of the image on entry to the function. */ static void generateMipmaps(ktxVulkanTexture* vkTexture, ktxVulkanDeviceInfo* vdi, VkFilter blitFilter, VkImageLayout initialLayout) { VkImageSubresourceRange subresourceRange; memset(&subresourceRange, 0, sizeof(subresourceRange)); subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; subresourceRange.baseMipLevel = 0; subresourceRange.levelCount = 1; subresourceRange.baseArrayLayer = 0; subresourceRange.layerCount = vkTexture->layerCount; // Transition base level to SRC_OPTIMAL for blitting. setImageLayout( vdi->vkFuncs, vdi->cmdBuffer, vkTexture->image, initialLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, subresourceRange); // Generate the mip chain // ---------------------- // Blit level n from level n-1. for (uint32_t i = 1; i < vkTexture->levelCount; i++) { VkImageBlit imageBlit; memset(&imageBlit, 0, sizeof(imageBlit)); // Source imageBlit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; imageBlit.srcSubresource.layerCount = vkTexture->layerCount; imageBlit.srcSubresource.mipLevel = i-1; imageBlit.srcOffsets[1].x = MAX(1, vkTexture->width >> (i - 1)); imageBlit.srcOffsets[1].y = MAX(1, vkTexture->height >> (i - 1)); imageBlit.srcOffsets[1].z = MAX(1, vkTexture->depth >> (i - 1));; // Destination imageBlit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; imageBlit.dstSubresource.layerCount = 1; imageBlit.dstSubresource.mipLevel = i; imageBlit.dstOffsets[1].x = MAX(1, vkTexture->width >> i); imageBlit.dstOffsets[1].y = MAX(1, vkTexture->height >> i); imageBlit.dstOffsets[1].z = MAX(1, vkTexture->depth >> i); VkImageSubresourceRange mipSubRange; memset(&mipSubRange, 0, sizeof(mipSubRange)); mipSubRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; mipSubRange.baseMipLevel = i; mipSubRange.levelCount = 1; mipSubRange.layerCount = vkTexture->layerCount; // Transiton current mip level to transfer dest setImageLayout( vdi->vkFuncs, vdi->cmdBuffer, vkTexture->image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, mipSubRange); // Blit from previous level vdi->vkFuncs.vkCmdBlitImage( vdi->cmdBuffer, vkTexture->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, vkTexture->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &imageBlit, blitFilter); // Transiton current mip level to transfer source for read in // next iteration. setImageLayout( vdi->vkFuncs, vdi->cmdBuffer, vkTexture->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, mipSubRange); } // After the loop, all mip layers are in TRANSFER_SRC layout. // Transition all to final layout. subresourceRange.levelCount = vkTexture->levelCount; setImageLayout( vdi->vkFuncs, vdi->cmdBuffer, vkTexture->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, vkTexture->imageLayout, subresourceRange); } //====================================================================== // ktxVulkanTexture utilities //====================================================================== /** * @memberof ktxVulkanTexture * @~English * @brief Destructor for the object returned when loading a texture image. * * Frees the Vulkan resources created when the texture image was loaded. * If a complete set of suballocator callbacks * are provided, it will use the free call from those instead. * * @param vkTexture pointer to the ktxVulkanTexture to be destructed. * @param device handle to the Vulkan logical device to which the texture was * loaded. * @param pAllocator pointer to the allocator used during loading. * @param subAllocatorCallbacks pointer to a structure of suballocator callbacks. * Pass NULL if a suballocator was not used. * @return KTX_SUCCESS on success, KTX_INVALID_VALUE if the * supplied subAllocatorCallbacks structure is incomplete. */ ktx_error_code_e ktxVulkanTexture_Destruct_WithSuballocator(ktxVulkanTexture* vkTexture, VkDevice device, const VkAllocationCallbacks* pAllocator, ktxVulkanTexture_subAllocatorCallbacks* subAllocatorCallbacks) { ktx_bool_t useSuballocator = false; if (subAllocatorCallbacks) { if (subAllocatorCallbacks->allocMemFuncPtr && subAllocatorCallbacks->bindBufferFuncPtr && subAllocatorCallbacks->bindImageFuncPtr && subAllocatorCallbacks->memoryMapFuncPtr && subAllocatorCallbacks->memoryUnmapFuncPtr && subAllocatorCallbacks->freeMemFuncPtr) useSuballocator = true; else return KTX_INVALID_VALUE; } vkTexture->vkDestroyImage(device, vkTexture->image, pAllocator); if (useSuballocator) subAllocatorCallbacks->freeMemFuncPtr(vkTexture->allocationId); else vkTexture->vkFreeMemory(device, vkTexture->deviceMemory, pAllocator); return KTX_SUCCESS; } /** * @memberof ktxVulkanTexture * @~English * @brief Destructor for the object returned when loading a texture image. * * Calls ktxVulkanTexture_Destruct_WithSuballocator() without a * set of suballocator callbacks. * * @sa ktxVulkanTexture_Destruct_WithSuballocator() for details and use that for complete * control. */ void ktxVulkanTexture_Destruct(ktxVulkanTexture* vkTexture, VkDevice device, const VkAllocationCallbacks* pAllocator) { (void)ktxVulkanTexture_Destruct_WithSuballocator(vkTexture, device, pAllocator, NULL); } /** @} */