Files
MobileGL/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp
T

1759 lines
87 KiB
C++

// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "VkTextureManager.h"
#include "MG_State/GLState/Core.h"
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
#include <Config.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>
namespace MobileGL::MG_Backend::DirectVulkan {
// Compute shaders may legally sample framebuffer-attached textures (the GL feedback-loop rule
// only covers rendering commands; e.g. Flywheel's Hi-Z depth pyramid downsample samples the
// depth attachment of the bound draw framebuffer), so sampled-read barriers must cover the
// compute stage in addition to the graphics stages.
static constexpr VkPipelineStageFlags kSampledReadStages =
VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) {
Int maxDimension = std::max<Int>(baseTexelSize.x(),
std::max<Int>(baseTexelSize.y(), std::max<Int>(baseTexelSize.z(), 1)));
Uint32 mipLevelCount = 1;
while (maxDimension > 1) {
maxDimension = std::max<Int>(maxDimension / 2, 1);
++mipLevelCount;
}
return mipLevelCount;
}
struct TextureFormatInfo {
VkFormat format = VK_FORMAT_UNDEFINED;
Bool expandRgbToRgba = false;
Uint32 componentByteCount = 0;
Array<Uint8, 4> alphaBytes = {0, 0, 0, 0};
};
struct TextureShapeInfo {
VkImageType imageType = VK_IMAGE_TYPE_2D;
VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
VkImageCreateFlags imageFlags = 0;
Uint32 depth = 1;
Uint32 arrayLayers = 1;
};
static Bool IsR11G11B10FFallbackEnabled() {
return MG_Config::Features.VulkanR11G11B10FFallback;
}
static Bool IsMultisampleTextureUploadTarget(TextureUploadTarget target) {
return target == TextureUploadTarget::Texture2DMultisample ||
target == TextureUploadTarget::ProxyTexture2DMultisample ||
target == TextureUploadTarget::Texture2DMultisampleArray ||
target == TextureUploadTarget::ProxyTexture2DMultisampleArray;
}
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
switch (requestedSamples) {
case 1:
outSampleCount = VK_SAMPLE_COUNT_1_BIT;
return true;
case 2:
outSampleCount = VK_SAMPLE_COUNT_2_BIT;
return true;
case 4:
outSampleCount = VK_SAMPLE_COUNT_4_BIT;
return true;
case 8:
outSampleCount = VK_SAMPLE_COUNT_8_BIT;
return true;
case 16:
outSampleCount = VK_SAMPLE_COUNT_16_BIT;
return true;
case 32:
outSampleCount = VK_SAMPLE_COUNT_32_BIT;
return true;
case 64:
outSampleCount = VK_SAMPLE_COUNT_64_BIT;
return true;
default:
return false;
}
}
static Bool IsCubeMapFaceUploadTarget(TextureUploadTarget target) {
return target >= TextureUploadTarget::CubeMapPositiveX &&
target <= TextureUploadTarget::CubeMapNegativeZ;
}
static Uint32 ResolveUploadArrayLayer(TextureUploadTarget target) {
if (!IsCubeMapFaceUploadTarget(target)) {
return 0;
}
return static_cast<Uint32>(target) - static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX);
}
static Bool IsValidSampledImageLayout(VkImageLayout layout) {
switch (layout) {
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
case VK_IMAGE_LAYOUT_GENERAL:
case VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL:
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
return true;
default:
return false;
}
}
static VkImageLayout ResolveSampledReadOnlyLayout(VkImageAspectFlags aspectMask) {
return (aspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0
? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
}
static void GetImageTransitionSourceState(VkImageLayout oldLayout,
VkPipelineStageFlags& outSrcStageMask,
VkAccessFlags& outSrcAccessMask) {
switch (oldLayout) {
case VK_IMAGE_LAYOUT_UNDEFINED:
outSrcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
outSrcAccessMask = 0;
return;
case VK_IMAGE_LAYOUT_GENERAL:
outSrcStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;
outSrcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
return;
case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:
outSrcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
outSrcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
return;
case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL:
outSrcStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
outSrcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
return;
case VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL:
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
outSrcStageMask = kSampledReadStages;
outSrcAccessMask = VK_ACCESS_SHADER_READ_BIT;
return;
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
outSrcStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT;
outSrcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
return;
case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:
outSrcStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT;
outSrcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
return;
default:
MOBILEGL_ASSERT(false, "GetImageTransitionSourceState: unsupported layout=%d", static_cast<Int>(oldLayout));
outSrcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
outSrcAccessMask = 0;
return;
}
}
VkTextureManager::TextureIdentity VkTextureManager::MakeTextureIdentity(
MG_State::GLState::ITextureObject* texture) {
return TextureIdentity{
.texture = texture,
.lifetimeId = texture ? texture->GetLifetimeId() : 0,
};
}
static void GetImageTransitionDestinationState(VkImageLayout newLayout,
VkPipelineStageFlags& outDstStageMask,
VkAccessFlags& outDstAccessMask) {
switch (newLayout) {
case VK_IMAGE_LAYOUT_GENERAL:
outDstStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;
outDstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
return;
case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:
outDstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
outDstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
return;
case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL:
outDstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
outDstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
return;
case VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL:
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
outDstStageMask = kSampledReadStages;
outDstAccessMask = VK_ACCESS_SHADER_READ_BIT;
return;
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
outDstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT;
outDstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
return;
case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:
outDstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT;
outDstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
return;
default:
MOBILEGL_ASSERT(false, "GetImageTransitionDestinationState: unsupported layout=%d", static_cast<Int>(newLayout));
outDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
outDstAccessMask = 0;
return;
}
}
static Bool PreserveTextureContentsOnRecreate(VkDevice device,
VkCommandPool commandPool,
VkQueue graphicsQueue,
const VkTextureManager::TextureResource& oldResource,
VkTextureManager::TextureResource& newResource) {
MOBILEGL_ASSERT(device != VK_NULL_HANDLE, "PreserveTextureContentsOnRecreate: device is null");
MOBILEGL_ASSERT(commandPool != VK_NULL_HANDLE, "PreserveTextureContentsOnRecreate: commandPool is null");
MOBILEGL_ASSERT(graphicsQueue != VK_NULL_HANDLE, "PreserveTextureContentsOnRecreate: graphicsQueue is null");
MOBILEGL_ASSERT(oldResource.image != VK_NULL_HANDLE, "PreserveTextureContentsOnRecreate: old image is null");
MOBILEGL_ASSERT(newResource.image != VK_NULL_HANDLE, "PreserveTextureContentsOnRecreate: new image is null");
const Uint32 preservedMipLevels = std::min(oldResource.mipLevels, newResource.mipLevels);
if (preservedMipLevels == 0 || oldResource.layout == VK_IMAGE_LAYOUT_UNDEFINED) {
return true;
}
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
VK_VERIFY(vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer),
"vkAllocateCommandBuffers(texture preserve)");
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
VK_VERIFY(vkBeginCommandBuffer(commandBuffer, &beginInfo), "vkBeginCommandBuffer(texture preserve)");
Bool ok = VkTextureManager::TransitionImageLayout(
commandBuffer, newResource.image, newResource.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
0, VK_ACCESS_TRANSFER_WRITE_BIT, newResource.aspect, 0, newResource.mipLevels,
newResource.arrayLayers);
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare destination image");
VkImageLayout srcTrackedLayout = oldResource.layout;
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags srcAccessMask = 0;
GetImageTransitionSourceState(srcTrackedLayout, srcStageMask, srcAccessMask);
ok = VkTextureManager::TransitionImageLayout(
commandBuffer, oldResource.image, srcTrackedLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT,
srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, oldResource.aspect, 0, preservedMipLevels,
oldResource.arrayLayers);
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to prepare source image");
Vector<VkImageCopy> copyRegions;
copyRegions.reserve(preservedMipLevels);
for (Uint32 level = 0; level < preservedMipLevels; ++level) {
VkImageCopy copy{};
copy.srcSubresource.aspectMask = oldResource.aspect;
copy.srcSubresource.mipLevel = level;
copy.srcSubresource.baseArrayLayer = 0;
copy.srcSubresource.layerCount = oldResource.arrayLayers;
copy.dstSubresource.aspectMask = newResource.aspect;
copy.dstSubresource.mipLevel = level;
copy.dstSubresource.baseArrayLayer = 0;
copy.dstSubresource.layerCount = newResource.arrayLayers;
copy.extent.width = std::max(oldResource.extent.width >> level, 1u);
copy.extent.height = std::max(oldResource.extent.height >> level, 1u);
copy.extent.depth = std::max(oldResource.depth >> level, 1u);
copyRegions.push_back(copy);
}
vkCmdCopyImage(commandBuffer,
oldResource.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
newResource.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
static_cast<Uint32>(copyRegions.size()), copyRegions.data());
VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags dstAccessMask = 0;
GetImageTransitionDestinationState(oldResource.layout, dstStageMask, dstAccessMask);
ok = VkTextureManager::TransitionImageLayout(
commandBuffer, newResource.image, newResource.layout, oldResource.layout,
VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask,
VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, newResource.aspect, 0, newResource.mipLevels,
newResource.arrayLayers);
MOBILEGL_ASSERT(ok, "PreserveTextureContentsOnRecreate: failed to restore destination layout");
VK_VERIFY(vkEndCommandBuffer(commandBuffer), "vkEndCommandBuffer(texture preserve)");
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;
VkFenceCreateInfo fenceInfo{};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
VkFence fence = VK_NULL_HANDLE;
VK_VERIFY(vkCreateFence(device, &fenceInfo, nullptr, &fence), "vkCreateFence(texture preserve)");
VK_VERIFY(vkQueueSubmit(graphicsQueue, 1, &submitInfo, fence), "vkQueueSubmit(texture preserve)");
VK_VERIFY(vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX), "vkWaitForFences(texture preserve)");
vkDestroyFence(device, fence, nullptr);
vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
return true;
}
static TextureFormatInfo ResolveTextureFormatInfo(TextureInternalFormat format) {
switch (format) {
case TextureInternalFormat::RGB:
case TextureInternalFormat::RGB8:
return {VK_FORMAT_R8G8B8A8_UNORM, true, 1, {0xFF, 0x00, 0x00, 0x00}};
case TextureInternalFormat::SRGB8:
return {VK_FORMAT_R8G8B8A8_SRGB, true, 1, {0xFF, 0x00, 0x00, 0x00}};
case TextureInternalFormat::RGB8Snorm:
return {VK_FORMAT_R8G8B8A8_SNORM, true, 1, {0x7F, 0x00, 0x00, 0x00}};
case TextureInternalFormat::RGB16:
return {VK_FORMAT_R16G16B16A16_UNORM, true, 2, {0xFF, 0xFF, 0x00, 0x00}};
case TextureInternalFormat::RGB16Snorm:
return {VK_FORMAT_R16G16B16A16_SNORM, true, 2, {0xFF, 0x7F, 0x00, 0x00}};
case TextureInternalFormat::RGB16F:
return {VK_FORMAT_R16G16B16A16_SFLOAT, true, 2, {0x00, 0x3C, 0x00, 0x00}};
case TextureInternalFormat::R11FG11FB10F:
if (IsR11G11B10FFallbackEnabled()) {
return {VK_FORMAT_R16G16B16A16_SFLOAT, true, 2, {0x00, 0x3C, 0x00, 0x00}};
}
return {MG_Util::ConvertTextureInternalFormatToVkEnum(format), false, 0, {0, 0, 0, 0}};
case TextureInternalFormat::RGB32F:
return {VK_FORMAT_R32G32B32A32_SFLOAT, true, 4, {0x00, 0x00, 0x80, 0x3F}};
case TextureInternalFormat::RGB8I:
return {VK_FORMAT_R8G8B8A8_SINT, true, 1, {0x01, 0x00, 0x00, 0x00}};
case TextureInternalFormat::RGB8UI:
return {VK_FORMAT_R8G8B8A8_UINT, true, 1, {0x01, 0x00, 0x00, 0x00}};
case TextureInternalFormat::RGB16I:
return {VK_FORMAT_R16G16B16A16_SINT, true, 2, {0x01, 0x00, 0x00, 0x00}};
case TextureInternalFormat::RGB16UI:
return {VK_FORMAT_R16G16B16A16_UINT, true, 2, {0x01, 0x00, 0x00, 0x00}};
case TextureInternalFormat::RGB32I:
return {VK_FORMAT_R32G32B32A32_SINT, true, 4, {0x01, 0x00, 0x00, 0x00}};
case TextureInternalFormat::RGB32UI:
return {VK_FORMAT_R32G32B32A32_UINT, true, 4, {0x01, 0x00, 0x00, 0x00}};
default:
return {MG_Util::ConvertTextureInternalFormatToVkEnum(format), false, 0, {0, 0, 0, 0}};
}
}
static Bool ExpandRgbSourceToRgba(const void* source, SizeT sourceByteSize, const IntVec3& texelSize,
const TextureFormatInfo& formatInfo, Vector<Uint8>& outExpandedData) {
MOBILEGL_ASSERT(source != nullptr, "ExpandRgbSourceToRgba: source is null");
MOBILEGL_ASSERT(formatInfo.expandRgbToRgba, "ExpandRgbSourceToRgba: format does not require RGB expansion");
MOBILEGL_ASSERT(formatInfo.componentByteCount > 0,
"ExpandRgbSourceToRgba: invalid component size for expanded RGB format");
const SizeT depth = static_cast<SizeT>(std::max(texelSize.z(), 1));
const SizeT pixelCount = static_cast<SizeT>(texelSize.x()) * static_cast<SizeT>(texelSize.y()) * depth;
MOBILEGL_ASSERT(pixelCount > 0, "ExpandRgbSourceToRgba: invalid texel size (%d, %d, %d)",
texelSize.x(), texelSize.y(), texelSize.z());
MOBILEGL_ASSERT(sourceByteSize == pixelCount * formatInfo.componentByteCount * 3,
"ExpandRgbSourceToRgba: unexpected source byte size=%zu for pixelCount=%zu componentBytes=%u",
sourceByteSize, pixelCount, formatInfo.componentByteCount);
outExpandedData.resize(pixelCount * formatInfo.componentByteCount * 4);
const auto* src = static_cast<const Uint8*>(source);
auto* dst = outExpandedData.data();
const SizeT srcPixelSize = static_cast<SizeT>(formatInfo.componentByteCount) * 3;
const SizeT dstPixelSize = static_cast<SizeT>(formatInfo.componentByteCount) * 4;
for (SizeT pixel = 0; pixel < pixelCount; ++pixel) {
const SizeT srcOffset = pixel * srcPixelSize;
const SizeT dstOffset = pixel * dstPixelSize;
std::memcpy(dst + dstOffset, src + srcOffset, srcPixelSize);
std::memcpy(dst + dstOffset + srcPixelSize, formatInfo.alphaBytes.data(), formatInfo.componentByteCount);
}
return true;
}
static Bool ShouldDumpTextureUploadStats() {
return MG_Config::Features.TextureUploadStats;
}
static void DumpTextureSyncStats(Int textureId, TextureInternalFormat format, TextureUploadTarget uploadTarget,
Uint32 mipLevelCount, const IntVec3& texelSize, SizeT byteSize,
Bool hasDirtyMipLevel) {
if (!ShouldDumpTextureUploadStats()) {
return;
}
std::fprintf(stderr,
"MOBILEGL_TEXTURE_SYNC_STATS texture=%d format=%s target=%s mips=%u size=%dx%dx%d "
"bytes=%zu dirty=%d\n",
textureId,
MG_Util::ConvertTextureInternalFormatToString(format).c_str(),
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
mipLevelCount,
texelSize.x(),
texelSize.y(),
texelSize.z(),
byteSize,
hasDirtyMipLevel ? 1 : 0);
}
static void DumpTextureUploadStats(Int textureId, TextureUploadTarget target, Uint32 level,
const IntVec3& texelSize, const void* data, SizeT byteSize, Uint32 channels) {
if (channels == 0 && texelSize.x() > 0 && texelSize.y() > 0) {
const SizeT depth = static_cast<SizeT>(std::max(texelSize.z(), 1));
const SizeT pixelCount = static_cast<SizeT>(texelSize.x()) * static_cast<SizeT>(texelSize.y()) * depth;
if (pixelCount > 0 && byteSize % pixelCount == 0) {
channels = static_cast<Uint32>(std::min<SizeT>(byteSize / pixelCount, 4));
}
}
if (!ShouldDumpTextureUploadStats() || data == nullptr || byteSize == 0 || channels == 0) {
return;
}
const auto* bytes = static_cast<const Uint8*>(data);
Uint8 minValue = 255;
Uint8 maxValue = 0;
SizeT nonZero = 0;
Uint64 sum = 0;
Uint64 neighborDiff = 0;
SizeT neighborCount = 0;
for (SizeT i = 0; i < byteSize; ++i) {
minValue = std::min(minValue, bytes[i]);
maxValue = std::max(maxValue, bytes[i]);
nonZero += bytes[i] != 0 ? 1 : 0;
sum += bytes[i];
}
const SizeT width = static_cast<SizeT>(std::max(texelSize.x(), 0));
const SizeT height = static_cast<SizeT>(std::max(texelSize.y(), 0));
const SizeT pixelStride = channels;
if (width > 1 && height > 0 && byteSize >= width * height * pixelStride) {
for (SizeT y = 0; y < height; ++y) {
const SizeT row = y * width * pixelStride;
for (SizeT x = 1; x < width; ++x) {
const SizeT prev = row + (x - 1) * pixelStride;
const SizeT cur = row + x * pixelStride;
for (SizeT c = 0; c < std::min<SizeT>(channels, 3); ++c) {
neighborDiff += static_cast<Uint64>(
std::abs(static_cast<Int>(bytes[cur + c]) - static_cast<Int>(bytes[prev + c])));
++neighborCount;
}
}
}
}
const double avg = byteSize > 0 ? static_cast<double>(sum) / static_cast<double>(byteSize) : 0.0;
const double avgNeighborDiff =
neighborCount > 0 ? static_cast<double>(neighborDiff) / static_cast<double>(neighborCount) : 0.0;
std::fprintf(stderr,
"MOBILEGL_TEXTURE_UPLOAD_STATS texture=%d target=%s level=%u size=%dx%dx%d bytes=%zu "
"channels=%u min=%u max=%u avg=%.2f nonzero=%zu neighborDiff=%.2f first=%u,%u,%u,%u\n",
textureId,
MG_Util::ConvertTextureUploadTargetToString(target).c_str(),
level,
texelSize.x(),
texelSize.y(),
texelSize.z(),
byteSize,
channels,
static_cast<Uint>(minValue),
static_cast<Uint>(maxValue),
avg,
nonZero,
avgNeighborDiff,
byteSize > 0 ? static_cast<Uint>(bytes[0]) : 0u,
byteSize > 1 ? static_cast<Uint>(bytes[1]) : 0u,
byteSize > 2 ? static_cast<Uint>(bytes[2]) : 0u,
byteSize > 3 ? static_cast<Uint>(bytes[3]) : 0u);
}
static VkComponentSwizzle ToVkComponentSwizzle(TextureSwizzleParam swizzle) {
switch (swizzle) {
case TextureSwizzleParam::Red:
return VK_COMPONENT_SWIZZLE_R;
case TextureSwizzleParam::Green:
return VK_COMPONENT_SWIZZLE_G;
case TextureSwizzleParam::Blue:
return VK_COMPONENT_SWIZZLE_B;
case TextureSwizzleParam::Alpha:
return VK_COMPONENT_SWIZZLE_A;
case TextureSwizzleParam::Zero:
return VK_COMPONENT_SWIZZLE_ZERO;
case TextureSwizzleParam::One:
return VK_COMPONENT_SWIZZLE_ONE;
default:
MOBILEGL_ASSERT(false, "ToVkComponentSwizzle: unsupported swizzle=%d", static_cast<Int>(swizzle));
return VK_COMPONENT_SWIZZLE_IDENTITY;
}
}
static VkComponentSwizzle ToVkSampledComponentSwizzle(TextureSwizzleParam swizzle, Bool alphaIsImplicitOne) {
if (alphaIsImplicitOne && swizzle == TextureSwizzleParam::Alpha) {
return VK_COMPONENT_SWIZZLE_ONE;
}
return ToVkComponentSwizzle(swizzle);
}
static VkComponentMapping ResolveSampledViewComponents(const MG_State::GLState::ITextureObject& texture,
const TextureFormatInfo& formatInfo) {
const auto& swizzles = texture.GetAllSwizzleParams();
const Bool alphaIsImplicitOne = formatInfo.expandRgbToRgba;
VkComponentMapping components{
ToVkSampledComponentSwizzle(swizzles.x(), alphaIsImplicitOne),
ToVkSampledComponentSwizzle(swizzles.y(), alphaIsImplicitOne),
ToVkSampledComponentSwizzle(swizzles.z(), alphaIsImplicitOne),
ToVkSampledComponentSwizzle(swizzles.w(), alphaIsImplicitOne),
};
return components;
}
static Bool TryResolveTextureShapeInfo(const MG_State::GLState::ITextureObject& texture,
TextureUploadTarget uploadTarget, const IntVec3& texelSize,
TextureShapeInfo& outShape) {
switch (uploadTarget) {
case TextureUploadTarget::Texture1D:
case TextureUploadTarget::ProxyTexture1D:
outShape = {};
outShape.imageType = VK_IMAGE_TYPE_1D;
outShape.viewType = VK_IMAGE_VIEW_TYPE_1D;
return true;
case TextureUploadTarget::Texture1DArray:
case TextureUploadTarget::ProxyTexture1DArray:
MOBILEGL_ASSERT(texelSize.z() > 0,
"TryResolveTextureShapeInfo: invalid 1D array depth=%d for textureId=%d",
texelSize.z(), texture.GetExternalIndex());
outShape.imageType = VK_IMAGE_TYPE_1D;
outShape.viewType = VK_IMAGE_VIEW_TYPE_1D_ARRAY;
outShape.depth = 1;
outShape.arrayLayers = static_cast<Uint32>(texelSize.z());
return true;
case TextureUploadTarget::Texture2D:
case TextureUploadTarget::ProxyTexture2D:
case TextureUploadTarget::TextureRectangle:
case TextureUploadTarget::ProxyTextureRectangle:
outShape = {};
return true;
case TextureUploadTarget::Texture2DMultisample:
case TextureUploadTarget::ProxyTexture2DMultisample:
outShape = {};
return true;
case TextureUploadTarget::Texture2DArray:
case TextureUploadTarget::ProxyTexture2DArray:
MOBILEGL_ASSERT(texelSize.z() > 0,
"TryResolveTextureShapeInfo: invalid 2D array depth=%d for textureId=%d",
texelSize.z(), texture.GetExternalIndex());
outShape.imageType = VK_IMAGE_TYPE_2D;
outShape.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY;
outShape.depth = 1;
outShape.arrayLayers = static_cast<Uint32>(texelSize.z());
return true;
case TextureUploadTarget::Texture2DMultisampleArray:
case TextureUploadTarget::ProxyTexture2DMultisampleArray:
MOBILEGL_ASSERT(texelSize.z() > 0,
"TryResolveTextureShapeInfo: invalid 2D multisample array depth=%d for textureId=%d",
texelSize.z(), texture.GetExternalIndex());
outShape.imageType = VK_IMAGE_TYPE_2D;
outShape.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY;
outShape.depth = 1;
outShape.arrayLayers = static_cast<Uint32>(texelSize.z());
return true;
case TextureUploadTarget::Texture3D:
case TextureUploadTarget::ProxyTexture3D:
MOBILEGL_ASSERT(texelSize.z() > 0,
"TryResolveTextureShapeInfo: invalid 3D texture depth=%d for textureId=%d",
texelSize.z(), texture.GetExternalIndex());
outShape.imageType = VK_IMAGE_TYPE_3D;
outShape.viewType = VK_IMAGE_VIEW_TYPE_3D;
outShape.depth = static_cast<Uint32>(texelSize.z());
return true;
case TextureUploadTarget::CubeMapPositiveX:
case TextureUploadTarget::CubeMapNegativeX:
case TextureUploadTarget::CubeMapPositiveY:
case TextureUploadTarget::CubeMapNegativeY:
case TextureUploadTarget::CubeMapPositiveZ:
case TextureUploadTarget::CubeMapNegativeZ:
case TextureUploadTarget::ProxyCubeMap:
MOBILEGL_ASSERT(texture.GetTarget() == TextureTarget::TextureCubeMap,
"TryResolveTextureShapeInfo: cube upload target on non-cube textureId=%d target=%s",
texture.GetExternalIndex(),
MG_Util::ConvertTextureTargetToString(texture.GetTarget()).c_str());
MOBILEGL_ASSERT(texelSize.x() == texelSize.y(),
"TryResolveTextureShapeInfo: cube map textureId=%d is not square (%d x %d)",
texture.GetExternalIndex(), texelSize.x(), texelSize.y());
outShape.imageType = VK_IMAGE_TYPE_2D;
outShape.viewType = VK_IMAGE_VIEW_TYPE_CUBE;
outShape.imageFlags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
outShape.depth = 1;
outShape.arrayLayers = 6;
return true;
default:
return false;
}
}
Bool VkTextureManager::Initialize(const InitInfo& initInfo) {
Shutdown();
m_device = initInfo.device;
m_physicalDevice = initInfo.physicalDevice;
m_allocator = initInfo.allocator;
m_commandPool = initInfo.commandPool;
m_graphicsQueue = initInfo.graphicsQueue;
m_currentFrameIndex = 0;
m_deferredReleases.clear();
m_deferredReleases.resize(initInfo.frameCount);
m_deferredViewReleases.clear();
m_deferredViewReleases.resize(initInfo.frameCount);
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_physicalDevice != VK_NULL_HANDLE && m_allocator != nullptr &&
m_commandPool != VK_NULL_HANDLE && m_graphicsQueue != VK_NULL_HANDLE,
"VkTextureManager::Initialize failed: invalid initialization info");
MOBILEGL_ASSERT(initInfo.frameCount > 0,
"VkTextureManager::Initialize failed: frameCount must be > 0");
TextureResource::s_device = m_device;
TextureResource::s_allocator = m_allocator;
return true;
}
void VkTextureManager::Shutdown() {
DestroyDeferredReleases();
m_textureResources.clear();
m_aliveObjects.clear();
m_device = VK_NULL_HANDLE;
m_physicalDevice = VK_NULL_HANDLE;
m_allocator = nullptr;
m_commandPool = VK_NULL_HANDLE;
m_graphicsQueue = VK_NULL_HANDLE;
m_currentFrameIndex = 0;
}
void VkTextureManager::BeginFrame(Uint32 frameIndex) {
MOBILEGL_ASSERT(frameIndex < m_deferredReleases.size(),
"VkTextureManager::BeginFrame invalid frame index %u (size=%zu)",
frameIndex, m_deferredReleases.size());
MOBILEGL_ASSERT(frameIndex < m_deferredViewReleases.size(),
"VkTextureManager::BeginFrame invalid deferred-view frame index %u (size=%zu)",
frameIndex, m_deferredViewReleases.size());
m_currentFrameIndex = frameIndex;
CollectDeferredReleases(frameIndex);
}
void VkTextureManager::EraseTrackedTexture(const TextureIdentity& identity) {
auto resourceIt = m_textureResources.find(identity);
if (resourceIt != m_textureResources.end()) {
DeferResourceRelease(Move(resourceIt->second));
m_textureResources.erase(resourceIt);
}
m_aliveObjects.erase(identity);
}
void VkTextureManager::PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture) {
if (texture == nullptr) {
return;
}
Vector<TextureIdentity> staleAliases;
for (auto it = m_aliveObjects.begin(); it != m_aliveObjects.end(); ++it) {
if (it->first.texture != texture) {
continue;
}
const auto liveTexture = it->second.lock();
if (!liveTexture || liveTexture.get() != texture ||
liveTexture->GetLifetimeId() != it->first.lifetimeId) {
staleAliases.emplace_back(it->first);
}
}
for (const auto& identity : staleAliases) {
EraseTrackedTexture(identity);
}
}
void VkTextureManager::BeginDrawSyncScope() {
m_drawSyncedThisDraw.clear();
m_drawSyncScopeActive = true;
}
void VkTextureManager::EndDrawSyncScope() {
m_drawSyncScopeActive = false;
m_drawSyncedThisDraw.clear();
}
VkTextureManager::TextureResource* VkTextureManager::SyncTextureAndGetDescriptor(MG_State::GLState::ITextureObject& texture) {
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "SyncTextureAndGetDescriptor: m_device == VK_NULL_HANDLE");
const TextureIdentity identity = MakeTextureIdentity(&texture);
// Per-draw memo fast path (see BeginDrawSyncScope): a texture already fully
// synced earlier in this draw cannot have changed since (no GL mutation runs
// mid-SetupDraw), so skip the heavy SyncTexture work and hand back the
// already-synced resource. Layout lives on the resource and is updated by the
// transition path, so the short-circuited resource still reflects the truth.
const Bool memoActive = m_drawSyncScopeActive;
if (memoActive) {
for (const TextureIdentity& synced : m_drawSyncedThisDraw) {
if (synced == identity) {
auto cachedIt = m_textureResources.find(identity);
if (cachedIt != m_textureResources.end() && cachedIt->second.image != VK_NULL_HANDLE) {
return &(cachedIt->second);
}
break; // resource unexpectedly gone -> fall through to a full sync
}
}
}
auto aliveIt = m_aliveObjects.find(identity);
if (aliveIt != m_aliveObjects.end() && aliveIt->second.expired()) {
EraseTrackedTexture(aliveIt->first);
aliveIt = m_aliveObjects.end();
}
// Only (re)register and prune when this (texture, lifetime) pair is new: stale
// aliases can only come into existence through an address reuse, which by
// construction introduces a new identity. Doing this unconditionally made every
// sampled-texture sync scan the entire alive-texture map per draw.
if (aliveIt == m_aliveObjects.end()) {
const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex());
if (liveTexture && liveTexture.get() == &texture) {
m_aliveObjects[identity] = WeakPtr<MG_State::GLState::ITextureObject>(liveTexture);
PruneStaleTextureAliases(&texture);
}
}
auto it = m_textureResources.find(identity);
if (it == m_textureResources.end()) {
TextureResource initial{};
auto [insertIt, _] = m_textureResources.emplace(identity, Move(initial));
it = insertIt;
}
if (!SyncTexture(texture, it->second)) {
MGLOG_D("%s: Syncing texture %d failed", __func__, texture.GetExternalIndex());
return nullptr;
}
if (memoActive) {
Bool recorded = false;
for (const TextureIdentity& synced : m_drawSyncedThisDraw) {
if (synced == identity) {
recorded = true;
break;
}
}
if (!recorded) {
m_drawSyncedThisDraw.push_back(identity);
}
}
return &(it->second);
}
VkImageView VkTextureManager::GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
return VK_NULL_HANDLE;
}
if (resource->perMipViews.size() != resource->mipLevels) {
resource->perMipViews.resize(resource->mipLevels, VK_NULL_HANDLE);
}
VkImageView& perMipView = resource->perMipViews[mipLevel];
if (perMipView != VK_NULL_HANDLE) {
return perMipView;
}
perMipView = CreateImageView(resource->image, resource->format, resource->aspect, resource->viewType,
mipLevel, 1, 0, resource->arrayLayers);
if (perMipView == VK_NULL_HANDLE) {
MGLOG_D("%s: CreateImageView failed for textureId=%d mipLevel=%u", __func__, texture.GetExternalIndex(), mipLevel);
return VK_NULL_HANDLE;
}
return perMipView;
}
VkImageView VkTextureManager::GetOrCreateAttachmentViewAtMipLevel(MG_State::GLState::ITextureObject& texture,
Uint32 mipLevel, Uint32 baseArrayLayer,
Uint32 layerCount,
VkImageViewType viewType) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
return VK_NULL_HANDLE;
}
if (layerCount == 0 || baseArrayLayer >= resource->arrayLayers ||
baseArrayLayer + layerCount > resource->arrayLayers) {
MGLOG_D("%s: invalid layer span [%u, %u) for textureId=%d arrayLayers=%u",
__func__, baseArrayLayer, baseArrayLayer + layerCount, texture.GetExternalIndex(),
resource->arrayLayers);
return VK_NULL_HANDLE;
}
if (baseArrayLayer == 0 && layerCount == resource->arrayLayers && viewType == resource->viewType) {
return GetOrCreateViewAtMipLevel(texture, mipLevel);
}
const TextureResource::AttachmentViewKey key{
.mipLevel = mipLevel,
.baseArrayLayer = baseArrayLayer,
.layerCount = layerCount,
.viewType = viewType,
};
auto it = resource->attachmentViews.find(key);
if (it == resource->attachmentViews.end()) {
it = resource->attachmentViews.emplace(key, VK_NULL_HANDLE).first;
}
VkImageView& attachmentView = it->second;
if (attachmentView != VK_NULL_HANDLE) {
return attachmentView;
}
attachmentView = CreateImageView(resource->image, resource->format, resource->aspect, viewType,
mipLevel, 1, baseArrayLayer, layerCount);
if (attachmentView == VK_NULL_HANDLE) {
MGLOG_D("%s: CreateImageView failed for textureId=%d mipLevel=%u baseArrayLayer=%u layerCount=%u viewType=%d",
__func__, texture.GetExternalIndex(), mipLevel, baseArrayLayer, layerCount, static_cast<Int>(viewType));
resource->attachmentViews.erase(it);
return VK_NULL_HANDLE;
}
return attachmentView;
}
VkImageView VkTextureManager::GetOrCreateSampledViewAtMipLevel(MG_State::GLState::ITextureObject& texture,
Uint32 mipLevel) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
return VK_NULL_HANDLE;
}
if (resource->perMipSampledViews.size() != resource->mipLevels) {
resource->perMipSampledViews.resize(resource->mipLevels, VK_NULL_HANDLE);
}
VkImageView& perMipSampledView = resource->perMipSampledViews[mipLevel];
if (perMipSampledView != VK_NULL_HANDLE) {
return perMipSampledView;
}
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
const VkImageAspectFlags sampledAspect = ResolveSampledImageViewAspectMask(resource->aspect);
perMipSampledView = CreateImageView(resource->image, resource->format, sampledAspect, resource->viewType,
mipLevel, 1, 0, resource->arrayLayers, &sampledComponents);
if (perMipSampledView == VK_NULL_HANDLE) {
MGLOG_D("%s: CreateImageView failed for textureId=%d mipLevel=%u", __func__, texture.GetExternalIndex(),
mipLevel);
return VK_NULL_HANDLE;
}
return perMipSampledView;
}
void VkTextureManager::UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout) {
MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null");
auto it = m_textureResources.find(MakeTextureIdentity(texture));
MOBILEGL_ASSERT(it != m_textureResources.end(),
"UpdateTrackedImageLayout: textureId=%d has no tracked resource", texture->GetExternalIndex());
MOBILEGL_ASSERT(it->second.image != VK_NULL_HANDLE,
"UpdateTrackedImageLayout: textureId=%d has null image", texture->GetExternalIndex());
it->second.layout = newLayout;
}
void VkTextureManager::UpdateTrackedImageLayoutAfterAttachmentWrite(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject* texture,
Uint32 writtenMipLevel,
VkImageLayout newLayout) {
MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayoutAfterAttachmentWrite: texture is null");
auto it = m_textureResources.find(MakeTextureIdentity(texture));
MOBILEGL_ASSERT(it != m_textureResources.end(),
"UpdateTrackedImageLayoutAfterAttachmentWrite: textureId=%d has no tracked resource",
texture->GetExternalIndex());
auto& resource = it->second;
MOBILEGL_ASSERT(resource.image != VK_NULL_HANDLE,
"UpdateTrackedImageLayoutAfterAttachmentWrite: textureId=%d has null image",
texture->GetExternalIndex());
MOBILEGL_ASSERT(writtenMipLevel < resource.mipLevels,
"UpdateTrackedImageLayoutAfterAttachmentWrite: textureId=%d mipLevel=%u out of range %u",
texture->GetExternalIndex(), writtenMipLevel, resource.mipLevels);
if (resource.layout != newLayout && resource.mipLevels > 1) {
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags srcAccessMask = 0;
GetImageTransitionSourceState(resource.layout, srcStageMask, srcAccessMask);
VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags dstAccessMask = 0;
GetImageTransitionDestinationState(newLayout, dstStageMask, dstAccessMask);
if (writtenMipLevel > 0) {
VkImageLayout lowerMipLayout = resource.layout;
const Bool lowerTransitioned = TransitionImageLayout(
commandBuffer, resource.image, lowerMipLayout, newLayout,
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
resource.aspect, 0, writtenMipLevel, resource.arrayLayers);
MOBILEGL_ASSERT(lowerTransitioned,
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition lower mip levels for textureId=%d",
texture->GetExternalIndex());
}
const Uint32 upperBaseMipLevel = writtenMipLevel + 1;
if (upperBaseMipLevel < resource.mipLevels) {
VkImageLayout upperMipLayout = resource.layout;
const Bool upperTransitioned = TransitionImageLayout(
commandBuffer, resource.image, upperMipLayout, newLayout,
srcStageMask, dstStageMask, srcAccessMask, dstAccessMask,
resource.aspect, upperBaseMipLevel, resource.mipLevels - upperBaseMipLevel,
resource.arrayLayers);
MOBILEGL_ASSERT(upperTransitioned,
"UpdateTrackedImageLayoutAfterAttachmentWrite: failed to transition upper mip levels for textureId=%d",
texture->GetExternalIndex());
}
}
resource.layout = newLayout;
}
Bool VkTextureManager::TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr) {
return false;
}
if (IsValidSampledImageLayout(resource->layout)) {
return true;
}
if (resource->layout == VK_IMAGE_LAYOUT_UNDEFINED) {
MGLOG_W("TransitionTextureForSampling: textureId=%d is still in VK_IMAGE_LAYOUT_UNDEFINED before sampling",
texture.GetExternalIndex());
}
VkImageLayout targetLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags srcAccessMask = 0;
if ((resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) {
MOBILEGL_ASSERT(resource->layout == VK_IMAGE_LAYOUT_UNDEFINED ||
resource->layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
"TransitionTextureForSampling: unsupported color layout=%d for textureId=%d",
static_cast<Int>(resource->layout), texture.GetExternalIndex());
if (resource->layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) {
srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
}
targetLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
} else if ((resource->aspect & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) != 0) {
MOBILEGL_ASSERT(resource->layout == VK_IMAGE_LAYOUT_UNDEFINED ||
resource->layout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
"TransitionTextureForSampling: unsupported depth/stencil layout=%d for textureId=%d",
static_cast<Int>(resource->layout), texture.GetExternalIndex());
if (resource->layout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) {
srcStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
}
targetLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL;
} else {
MOBILEGL_ASSERT(false, "TransitionTextureForSampling: unsupported aspect mask=0x%x for textureId=%d",
static_cast<Uint32>(resource->aspect), texture.GetExternalIndex());
}
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
kSampledReadStages, srcAccessMask,
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels,
resource->arrayLayers);
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
return ok;
}
Bool VkTextureManager::TransitionTextureForStorageImage(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture) {
TextureResource* resource = SyncTextureAndGetDescriptor(texture);
if (resource == nullptr) {
return false;
}
if (resource->sampleCount != VK_SAMPLE_COUNT_1_BIT) {
MGLOG_D("TransitionTextureForStorageImage: multisample textureId=%d is not exposed as a storage image",
texture.GetExternalIndex());
return false;
}
if (resource->layout == VK_IMAGE_LAYOUT_GENERAL) {
return true;
}
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;
VkAccessFlags srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_MEMORY_READ_BIT;
GetImageTransitionSourceState(resource->layout, srcStageMask, srcAccessMask);
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout,
VK_IMAGE_LAYOUT_GENERAL, srcStageMask,
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, srcAccessMask,
VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
resource->aspect, 0, resource->mipLevels, resource->arrayLayers);
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
texture.GetExternalIndex());
return ok;
}
Bool VkTextureManager::TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image,
VkImageLayout& trackedLayout, VkImageLayout newLayout,
VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask,
VkAccessFlags srcAccessMask, VkAccessFlags dstAccessMask,
VkImageAspectFlags aspectMask, Uint32 baseMipLevel, Uint32 levelCount,
Uint32 layerCount) {
MOBILEGL_ASSERT(image != VK_NULL_HANDLE, "TransitionImageLayout: m_image == VK_NULL_HANDLE");
MOBILEGL_ASSERT(!((dstAccessMask & VK_ACCESS_TRANSFER_READ_BIT) != 0 &&
(dstStageMask & VK_PIPELINE_STAGE_TRANSFER_BIT) == 0),
"TransitionImageLayout: invalid dstAccess/dstStage pair (dstAccess=0x%x, dstStage=0x%x, oldLayout=%d, newLayout=%d)",
static_cast<Uint32>(dstAccessMask), static_cast<Uint32>(dstStageMask), static_cast<Int>(trackedLayout),
static_cast<Int>(newLayout));
if (trackedLayout == newLayout) {
return true;
}
VkImageMemoryBarrier barrier{};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.srcAccessMask = srcAccessMask;
barrier.dstAccessMask = dstAccessMask;
barrier.oldLayout = trackedLayout;
barrier.newLayout = newLayout;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image;
barrier.subresourceRange.aspectMask = aspectMask;
barrier.subresourceRange.baseMipLevel = baseMipLevel;
barrier.subresourceRange.levelCount = levelCount;
barrier.subresourceRange.baseArrayLayer = 0;
barrier.subresourceRange.layerCount = layerCount;
vkCmdPipelineBarrier(commandBuffer, srcStageMask, dstStageMask, 0, 0, nullptr, 0, nullptr, 1, &barrier);
trackedLayout = newLayout;
return true;
}
SizeT VkTextureManager::CollectGarbage() {
m_gcCounter++;
if (m_gcCounter != 0) {
return 0;
}
Vector<MG_State::GLState::ITextureObject*> expiredTextures;
expiredTextures.reserve(m_aliveObjects.size());
for (auto it = m_aliveObjects.begin(); it != m_aliveObjects.end(); ++it) {
if (it->second.expired()) {
expiredTextures.emplace_back(it->first.texture);
}
}
for (auto* texture : expiredTextures) {
PruneStaleTextureAliases(texture);
}
return expiredTextures.size();
}
Bool VkTextureManager::SyncTexture(MG_State::GLState::ITextureObject &texture,
TextureResource &outResource) {
// Cross-draw fast path: if the resource is already built and neither the texture's
// pixel content (bumped in MarkStorageDirty) nor its params changed since the last
// sync, there is nothing to re-check or re-upload - skip CheckMipmapCompleteness,
// SyncTextureResource, SyncTextureViews and the per-level dirty scan. Layout is
// maintained separately by the transition path, so the resource still reflects truth.
const Uint64 syncingContentVersion = texture.GetContentVersion();
const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture);
const Uint32 syncingMipLevelCount =
syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u;
if (outResource.image != VK_NULL_HANDLE &&
outResource.syncedContentVersion == syncingContentVersion &&
outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() &&
outResource.syncedMipLevelCount == syncingMipLevelCount) {
return true;
}
TextureUploadTarget uploadTarget = TextureUploadTarget::Unknown;
IntVec3 texelSize{0, 0, 0};
SizeT byteSize = 0;
Uint32 mipLevelCount = 0;
if (!CheckMipmapCompleteness(texture, uploadTarget, texelSize, byteSize, mipLevelCount)) {
MGLOG_D("%s: mipmap not complete", __func__);
return false;
}
auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
if (!mipTexture) {
MGLOG_D("%s: not TextureObjectMipmap", __func__);
return false;
}
if (!SyncTextureResource(texture, uploadTarget, texelSize, byteSize, mipLevelCount, outResource)) {
MGLOG_D("%s: SyncTextureResource failed", __func__);
return false;
}
if (!SyncTextureViews(texture, outResource)) {
MGLOG_D("%s: SyncTextureViews failed", __func__);
return false;
}
Vector<TextureUploadTarget> dirtyTargets;
if (outResource.viewType == VK_IMAGE_VIEW_TYPE_CUBE) {
dirtyTargets = mipTexture->GetUploadTargets();
} else {
dirtyTargets.push_back(uploadTarget);
}
Bool hasDirtyMipLevel = false;
for (const TextureUploadTarget target : dirtyTargets) {
const Uint32 targetMipLevelCount = std::min(mipLevelCount, GetUploadMipLevelCount(*mipTexture, target));
for (Uint32 level = 0; level < targetMipLevelCount; ++level) {
if (mipTexture->IsStorageDirty(target, level)) {
hasDirtyMipLevel = true;
break;
}
}
if (hasDirtyMipLevel) {
break;
}
}
DumpTextureSyncStats(texture.GetExternalIndex(), texture.GetFormat(), uploadTarget, mipLevelCount,
texelSize, byteSize, hasDirtyMipLevel);
if (!hasDirtyMipLevel) {
outResource.syncedContentVersion = syncingContentVersion;
outResource.syncedMipLevelCount = syncingMipLevelCount;
return true;
}
if (!UploadDirtyMipLevels(*mipTexture, uploadTarget, outResource)) {
MGLOG_D("%s: UploadDirtyMipLevels failed", __func__);
return false;
}
outResource.syncedContentVersion = syncingContentVersion;
outResource.syncedMipLevelCount = syncingMipLevelCount;
return true;
}
Bool VkTextureManager::SyncTextureResource(const MG_State::GLState::ITextureObject &texture,
TextureUploadTarget uploadTarget,
const IntVec3 &texelSize, SizeT byteSize, Uint32 mipLevels,
TextureResource &resource) {
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
const VkFormat format = formatInfo.format;
if (format == VK_FORMAT_UNDEFINED) {
MGLOG_D("%s: format == VK_FORMAT_UNDEFINED", __func__);
return false;
}
if (texelSize.x() <= 0 || texelSize.y() <= 0 /*|| byteSize == 0*/) {
MGLOG_D("%s: texelSize or byteSize is zero", __func__);
return false;
}
if (mipLevels == 0) {
MGLOG_D("%s: no mip levels", __func__);
return false;
}
const Bool isMultisampleTexture = IsMultisampleTextureUploadTarget(uploadTarget);
const Uint32 backingMipLevels =
isMultisampleTexture ? 1u : std::max(mipLevels, ComputeFullMipLevelCount(texelSize));
TextureShapeInfo shapeInfo{};
const Bool supportedShape = TryResolveTextureShapeInfo(texture, uploadTarget, texelSize, shapeInfo);
MOBILEGL_ASSERT(supportedShape,
"SyncTextureResource: unsupported uploadTarget=%s textureTarget=%s textureId=%d size=(%d,%d,%d) "
"mipLevels=%u vkViewType=%d",
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
MG_Util::ConvertTextureTargetToString(texture.GetTarget()).c_str(),
texture.GetExternalIndex(), texelSize.x(), texelSize.y(), texelSize.z(), mipLevels,
static_cast<Int>(MG_Util::ConvertTextureUploadTargetToVkEnum(uploadTarget)));
if (!supportedShape) {
MGLOG_D("%s: not Texture2D, unsupported", __func__);
return false;
}
VkSampleCountFlagBits resolvedSampleCount = VK_SAMPLE_COUNT_1_BIT;
if (isMultisampleTexture &&
!TryResolveSampleCountFlagBits(texture.GetSamples(), resolvedSampleCount)) {
MGLOG_D("%s: unsupported multisample count=%d for textureId=%d target=%s", __func__,
texture.GetSamples(), texture.GetExternalIndex(),
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str());
return false;
}
const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format &&
resource.extent.width == static_cast<Uint32>(texelSize.x()) &&
resource.extent.height == static_cast<Uint32>(texelSize.y()) &&
resource.depth == shapeInfo.depth &&
resource.arrayLayers == shapeInfo.arrayLayers &&
resource.viewType == shapeInfo.viewType &&
resource.sampleCount == resolvedSampleCount &&
resource.mipLevels == backingMipLevels;
if (compatible) {
if (resource.perMipViews.size() != backingMipLevels) {
resource.perMipViews.resize(backingMipLevels, VK_NULL_HANDLE);
}
if (resource.perMipSampledViews.size() != backingMipLevels) {
resource.perMipSampledViews.resize(backingMipLevels, VK_NULL_HANDLE);
}
return true;
}
const Bool preserveExistingContent =
resource.image != VK_NULL_HANDLE &&
resource.format == format &&
resource.extent.width == static_cast<Uint32>(texelSize.x()) &&
resource.extent.height == static_cast<Uint32>(texelSize.y()) &&
resource.depth == shapeInfo.depth &&
resource.arrayLayers == shapeInfo.arrayLayers &&
resource.viewType == shapeInfo.viewType &&
resource.sampleCount == resolvedSampleCount &&
resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT &&
resource.mipLevels < backingMipLevels &&
resource.layout != VK_IMAGE_LAYOUT_UNDEFINED;
std::unique_ptr<TextureResource> preservedResource;
if (preserveExistingContent) {
preservedResource = std::make_unique<TextureResource>(Move(resource));
} else {
DeferResourceRelease(Move(resource));
}
auto aspect = GetAspectMaskForFormat(format);
VkImageCreateInfo imageInfo{};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.flags = shapeInfo.imageFlags;
imageInfo.imageType = shapeInfo.imageType;
imageInfo.extent.width = static_cast<Uint32>(texelSize.x());
imageInfo.extent.height = static_cast<Uint32>(texelSize.y());
imageInfo.extent.depth = shapeInfo.depth;
imageInfo.mipLevels = backingMipLevels;
imageInfo.arrayLayers = shapeInfo.arrayLayers;
imageInfo.format = format;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties);
const Bool supportsStorageImage =
!isMultisampleTexture &&
(aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 &&
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT |
(supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) |
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) |
(((aspect & VK_IMAGE_ASPECT_DEPTH_BIT) || (aspect & VK_IMAGE_ASPECT_STENCIL_BIT)) ?
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT :
0);
if (!isMultisampleTexture) {
imageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
}
imageInfo.samples = resolvedSampleCount;
if (isMultisampleTexture) {
VkImageFormatProperties imageFormatProperties{};
const VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
imageInfo.flags, &imageFormatProperties);
if (imageFormatResult != VK_SUCCESS ||
(imageFormatProperties.sampleCounts & resolvedSampleCount) == 0) {
MGLOG_D("%s: sampleCount=%d is unsupported for textureId=%d target=%s format=%d usage=0x%x",
__func__, texture.GetSamples(), texture.GetExternalIndex(),
MG_Util::ConvertTextureUploadTargetToString(uploadTarget).c_str(),
static_cast<Int>(format), static_cast<Uint32>(imageInfo.usage));
return false;
}
}
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VmaAllocationCreateInfo allocationInfo{};
allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
VK_VERIFY(vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &resource.image, &resource.allocation, nullptr),
"vmaCreateImage(texture)");
++m_textureImageEpoch; // a new attachment image invalidates cached render passes
resource.layout = VK_IMAGE_LAYOUT_UNDEFINED;
resource.extent = {static_cast<Uint32>(texelSize.x()), static_cast<Uint32>(texelSize.y())};
resource.depth = shapeInfo.depth;
resource.arrayLayers = shapeInfo.arrayLayers;
resource.mipLevels = backingMipLevels;
resource.perMipViews.assign(backingMipLevels, VK_NULL_HANDLE);
resource.perMipSampledViews.assign(backingMipLevels, VK_NULL_HANDLE);
resource.sampledBaseMipLevel = 0;
resource.sampledLevelCount = mipLevels;
resource.format = format;
resource.aspect = aspect;
resource.viewType = shapeInfo.viewType;
resource.sampleCount = resolvedSampleCount;
resource.syncedTextureParamsVersion = 0;
if (preservedResource) {
const Bool preserved = PreserveTextureContentsOnRecreate(
m_device, m_commandPool, m_graphicsQueue, *preservedResource, resource);
MOBILEGL_ASSERT(preserved,
"SyncTextureResource: failed to preserve texture contents while growing mip chain");
DeferResourceRelease(Move(*preservedResource));
}
return true;
}
void VkTextureManager::DeferResourceRelease(TextureResource&& resource) {
if (resource.image == VK_NULL_HANDLE && resource.fullView == VK_NULL_HANDLE &&
resource.sampledView == VK_NULL_HANDLE &&
resource.perMipViews.empty() && resource.perMipSampledViews.empty()) {
return;
}
if (m_deferredReleases.empty()) {
resource.Reset();
return;
}
MOBILEGL_ASSERT(m_currentFrameIndex < m_deferredReleases.size(),
"VkTextureManager::DeferResourceRelease invalid current frame index %u (size=%zu)",
m_currentFrameIndex, m_deferredReleases.size());
m_deferredReleases[m_currentFrameIndex].push_back(Move(resource));
}
void VkTextureManager::CollectDeferredReleases(Uint32 frameIndex) {
MOBILEGL_ASSERT(frameIndex < m_deferredReleases.size(),
"VkTextureManager::CollectDeferredReleases invalid frame index %u (size=%zu)",
frameIndex, m_deferredReleases.size());
m_deferredReleases[frameIndex].clear();
MOBILEGL_ASSERT(frameIndex < m_deferredViewReleases.size(),
"VkTextureManager::CollectDeferredReleases invalid deferred-view frame index %u (size=%zu)",
frameIndex, m_deferredViewReleases.size());
for (const VkImageView view : m_deferredViewReleases[frameIndex]) {
if (view != VK_NULL_HANDLE) {
vkDestroyImageView(m_device, view, nullptr);
}
}
m_deferredViewReleases[frameIndex].clear();
}
void VkTextureManager::DestroyDeferredReleases() {
for (auto& deferredReleases : m_deferredReleases) {
deferredReleases.clear();
}
m_deferredReleases.clear();
for (auto& deferredViews : m_deferredViewReleases) {
for (const VkImageView view : deferredViews) {
if (view != VK_NULL_HANDLE) {
vkDestroyImageView(m_device, view, nullptr);
}
}
deferredViews.clear();
}
m_deferredViewReleases.clear();
}
void VkTextureManager::DeferViewRelease(VkImageView view) {
if (view == VK_NULL_HANDLE) {
return;
}
if (m_deferredViewReleases.empty()) {
vkDestroyImageView(m_device, view, nullptr);
return;
}
MOBILEGL_ASSERT(m_currentFrameIndex < m_deferredViewReleases.size(),
"VkTextureManager::DeferViewRelease invalid current frame index %u (size=%zu)",
m_currentFrameIndex, m_deferredViewReleases.size());
m_deferredViewReleases[m_currentFrameIndex].push_back(view);
}
Bool VkTextureManager::SyncTextureViews(const MG_State::GLState::ITextureObject& texture, TextureResource& resource) {
MOBILEGL_ASSERT(resource.image != VK_NULL_HANDLE, "SyncTextureViews: image == VK_NULL_HANDLE");
Uint32 baseMipLevel = 0;
Uint32 levelCount = 1;
ResolveViewMipRange(texture, resource.mipLevels, baseMipLevel, levelCount);
const Bool needsRecreate =
resource.fullView == VK_NULL_HANDLE ||
resource.sampledView == VK_NULL_HANDLE ||
resource.sampledBaseMipLevel != baseMipLevel ||
resource.sampledLevelCount != levelCount ||
resource.syncedTextureParamsVersion != texture.GetTextureParamsVersion();
if (!needsRecreate) {
return true;
}
if (resource.fullView != VK_NULL_HANDLE) {
DeferViewRelease(resource.fullView);
resource.fullView = VK_NULL_HANDLE;
}
if (resource.sampledView != VK_NULL_HANDLE) {
DeferViewRelease(resource.sampledView);
resource.sampledView = VK_NULL_HANDLE;
}
for (auto& sampledView : resource.perMipSampledViews) {
if (sampledView != VK_NULL_HANDLE) {
DeferViewRelease(sampledView);
sampledView = VK_NULL_HANDLE;
}
}
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat());
const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo);
resource.fullView = CreateImageView(resource.image, resource.format, resource.aspect, resource.viewType,
baseMipLevel, levelCount, 0, resource.arrayLayers, &sampledComponents);
if (resource.fullView == VK_NULL_HANDLE) {
return false;
}
const VkImageAspectFlags sampledAspect = ResolveSampledImageViewAspectMask(resource.aspect);
resource.sampledView = CreateImageView(resource.image, resource.format, sampledAspect, resource.viewType,
baseMipLevel, levelCount, 0, resource.arrayLayers, &sampledComponents);
if (resource.sampledView == VK_NULL_HANDLE) {
return false;
}
resource.sampledBaseMipLevel = baseMipLevel;
resource.sampledLevelCount = levelCount;
resource.syncedTextureParamsVersion = texture.GetTextureParamsVersion();
return true;
}
VkImageView VkTextureManager::CreateImageView(VkImage image, VkFormat format, VkImageAspectFlags aspect,
VkImageViewType viewType, Uint32 baseMipLevel, Uint32 levelCount,
Uint32 baseArrayLayer,
Uint32 layerCount,
const VkComponentMapping* components) const {
VkImageViewCreateInfo viewInfo{};
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = image;
viewInfo.viewType = viewType;
viewInfo.format = format;
viewInfo.components = components != nullptr ?
*components :
VkComponentMapping{VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G,
VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A};
viewInfo.subresourceRange.aspectMask = aspect;
viewInfo.subresourceRange.baseMipLevel = baseMipLevel;
viewInfo.subresourceRange.levelCount = levelCount;
viewInfo.subresourceRange.baseArrayLayer = baseArrayLayer;
viewInfo.subresourceRange.layerCount = layerCount;
VkImageView view = VK_NULL_HANDLE;
VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &view), "vkCreateImageView(texture)");
return view;
}
Bool VkTextureManager::UploadDirtyMipLevels(MG_State::GLState::TextureObjectMipmap &mipmapTexture,
TextureUploadTarget uploadTarget,
TextureResource &outResource) {
struct UploadItem {
TextureUploadTarget target = TextureUploadTarget::Unknown;
Uint32 level = 0;
Uint32 baseArrayLayer = 0;
SizeT uploadByteSize = 0;
IntVec3 texelSize = {0, 0, 0};
const void* source = nullptr;
Vector<Uint8> expandedData;
VkDeviceSize offset = 0;
};
Vector<UploadItem> uploadItems;
Vector<TextureUploadTarget> targets;
if (outResource.viewType == VK_IMAGE_VIEW_TYPE_CUBE) {
targets = mipmapTexture.GetUploadTargets();
} else {
targets.push_back(uploadTarget);
}
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(mipmapTexture.GetFormat());
VkDeviceSize stagingSize = 0;
for (const TextureUploadTarget target : targets) {
const Uint32 definedMipLevels = GetUploadMipLevelCount(mipmapTexture, target);
MOBILEGL_ASSERT(definedMipLevels <= outResource.mipLevels,
"UploadDirtyMipLevels: defined mip level count %u exceeds backing mip level count %u for textureId=%d target=%s",
definedMipLevels, outResource.mipLevels, mipmapTexture.GetExternalIndex(),
MG_Util::ConvertTextureUploadTargetToString(target).c_str());
for (Uint32 level = 0; level < definedMipLevels; ++level) {
if (!mipmapTexture.IsStorageDirty(target, level)) {
continue;
}
const auto texelSize = mipmapTexture.GetMipmapTexelSize(target, level);
const auto byteSize = mipmapTexture.GetMipmapByteSize(target, level);
if (texelSize.x() <= 0 || texelSize.y() <= 0 || byteSize == 0) {
mipmapTexture.MarkStorageDirty(target, level, false);
continue;
}
const void* source = mipmapTexture.MapMipmapData(target, level);
if (source == nullptr) {
MGLOG_D("%s: MapmipmapData failed at target %s level %d", __func__,
MG_Util::ConvertTextureUploadTargetToString(target).c_str(), level);
return false;
}
UploadItem uploadItem{};
uploadItem.target = target;
uploadItem.level = level;
uploadItem.baseArrayLayer = ResolveUploadArrayLayer(target);
uploadItem.texelSize = texelSize;
uploadItem.source = source;
uploadItem.offset = stagingSize;
uploadItem.uploadByteSize = byteSize;
if (formatInfo.expandRgbToRgba) {
const Bool expanded = ExpandRgbSourceToRgba(source, byteSize, texelSize, formatInfo,
uploadItem.expandedData);
MOBILEGL_ASSERT(expanded,
"UploadDirtyMipLevels: failed to expand RGB textureId=%d target=%s level=%u to RGBA staging data",
mipmapTexture.GetExternalIndex(),
MG_Util::ConvertTextureUploadTargetToString(target).c_str(), level);
uploadItem.uploadByteSize = uploadItem.expandedData.size();
}
uploadItems.push_back(Move(uploadItem));
if (!uploadItems.back().expandedData.empty()) {
uploadItems.back().source = uploadItems.back().expandedData.data();
}
DumpTextureUploadStats(mipmapTexture.GetExternalIndex(), uploadItems.back().target,
uploadItems.back().level, uploadItems.back().texelSize,
uploadItems.back().source, uploadItems.back().uploadByteSize,
formatInfo.expandRgbToRgba ? 4u : 0u);
stagingSize += static_cast<VkDeviceSize>(uploadItems.back().uploadByteSize);
}
}
if (uploadItems.empty()) {
return true;
}
VkBuffer stagingBuffer = VK_NULL_HANDLE;
VmaAllocation stagingAllocation = nullptr;
VkBufferCreateInfo bufferInfo{};
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufferInfo.size = stagingSize;
bufferInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VmaAllocationCreateInfo stagingAllocationInfo{};
stagingAllocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST;
stagingAllocationInfo.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
stagingAllocationInfo.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
VK_VERIFY(vmaCreateBuffer(m_allocator, &bufferInfo, &stagingAllocationInfo, &stagingBuffer, &stagingAllocation, nullptr),
"vmaCreateBuffer(staging texture)");
void* mapped = nullptr;
VK_VERIFY(vmaMapMemory(m_allocator, stagingAllocation, &mapped), "vmaMapMemory(staging texture)");
for (const auto& item : uploadItems) {
std::memcpy(static_cast<Uint8*>(mapped) + item.offset, item.source, item.uploadByteSize);
}
vmaUnmapMemory(m_allocator, stagingAllocation);
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = m_commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
VK_VERIFY(vkAllocateCommandBuffers(m_device, &allocInfo, &commandBuffer), "vkAllocateCommandBuffers(texture)");
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
VK_VERIFY(vkBeginCommandBuffer(commandBuffer, &beginInfo), "vkBeginCommandBuffer(texture)");
const VkImageAspectFlags aspectMask = GetAspectMaskForFormat(outResource.format);
VkPipelineStageFlags uploadSrcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkAccessFlags uploadSrcAccessMask = 0;
GetImageTransitionSourceState(outResource.layout, uploadSrcStageMask, uploadSrcAccessMask);
Bool ok = TransitionImageLayout(commandBuffer, outResource.image,
outResource.layout,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
uploadSrcStageMask,
VK_PIPELINE_STAGE_TRANSFER_BIT,
uploadSrcAccessMask,
VK_ACCESS_TRANSFER_WRITE_BIT,
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
MOBILEGL_ASSERT(ok, "TransitionImageLayout to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL failed");
for (const auto& item : uploadItems) {
VkBufferImageCopy copy{};
copy.bufferOffset = item.offset;
copy.bufferRowLength = 0;
copy.bufferImageHeight = 0;
copy.imageSubresource.aspectMask = aspectMask;
copy.imageSubresource.mipLevel = item.level;
copy.imageSubresource.baseArrayLayer = item.baseArrayLayer;
copy.imageSubresource.layerCount = 1;
copy.imageOffset = {0, 0, 0};
copy.imageExtent = {static_cast<Uint32>(item.texelSize.x()), static_cast<Uint32>(item.texelSize.y()),
item.texelSize.z() > 0 ? static_cast<Uint32>(item.texelSize.z()) : 1u};
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, &copy);
}
const VkImageLayout finalLayout = ResolveSampledReadOnlyLayout(aspectMask);
VkImageLayout uploadLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
ok = TransitionImageLayout(commandBuffer, outResource.image,
uploadLayout,
finalLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT,
kSampledReadStages,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_SHADER_READ_BIT,
aspectMask, 0, outResource.mipLevels, outResource.arrayLayers);
MOBILEGL_ASSERT(ok, "TransitionImageLayout to sampled read-only layout failed");
outResource.layout = finalLayout;
VK_VERIFY(vkEndCommandBuffer(commandBuffer), "vkEndCommandBuffer(texture)");
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;
VkFenceCreateInfo fenceInfo{};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
VkFence uploadFence = VK_NULL_HANDLE;
VK_VERIFY(vkCreateFence(m_device, &fenceInfo, nullptr, &uploadFence), "vkCreateFence(texture upload)");
VK_VERIFY(vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, uploadFence), "vkQueueSubmit(texture)");
VK_VERIFY(vkWaitForFences(m_device, 1, &uploadFence, VK_TRUE, UINT64_MAX), "vkWaitForFences(texture upload)");
vkDestroyFence(m_device, uploadFence, nullptr);
vkFreeCommandBuffers(m_device, m_commandPool, 1, &commandBuffer);
vmaDestroyBuffer(m_allocator, stagingBuffer, stagingAllocation);
if (!ok) {
MGLOG_D("%s: texture upload cmd failed", __func__);
return false;
}
for (const auto& item : uploadItems) {
mipmapTexture.MarkStorageDirty(item.target, item.level, false);
}
outResource.layout = finalLayout;
return true;
}
Bool VkTextureManager::CheckMipmapCompleteness(const MG_State::GLState::ITextureObject& texture,
TextureUploadTarget& outTarget,
IntVec3& outTexelSize,
SizeT& outByteSize,
Uint32& outMipLevelCount) {
const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
if (!mipTexture) {
MGLOG_D("%s: not TextureObjectMipmap", __func__);
return false;
}
const auto& targets = texture.GetUploadTargets();
if (targets.empty()) {
MGLOG_D("%s: upload target empty", __func__);
return false;
}
for (const auto target : targets) {
const Uint32 mipLevelCount = GetUploadMipLevelCount(*mipTexture, target);
if (mipLevelCount == 0) {
MGLOG_D("%s: mipLevelCount == 0", __func__);
continue;
}
// Backing VkImage allocation still uses storage mip 0 as the physical image extent.
// GL_TEXTURE_BASE_LEVEL / MAX_LEVEL are applied later when building the sampled view.
const auto storageBaseTexelSize = mipTexture->GetMipmapTexelSize(target, 0);
const auto storageBaseByteSize = mipTexture->GetMipmapByteSize(target, 0);
if (storageBaseTexelSize.x() <= 0 || storageBaseTexelSize.y() <= 0 /*|| storageBaseByteSize == 0*/) {
continue;
}
outTarget = target;
outTexelSize = storageBaseTexelSize;
outByteSize = storageBaseByteSize;
outMipLevelCount = mipLevelCount;
return true;
}
MGLOG_D("%s: no valid target or mipmap", __func__);
return false;
}
Uint32 VkTextureManager::GetUploadMipLevelCount(const MG_State::GLState::TextureObjectMipmap& texture,
TextureUploadTarget target) {
const Uint totalLevelCount = texture.GetMipmapLevelCount();
if (totalLevelCount == 0) {
return 0;
}
Uint32 validLevelCount = 0;
for (Uint level = 0; level < totalLevelCount; ++level) {
const auto size = texture.GetMipmapTexelSize(target, level);
const auto byteSize = texture.GetMipmapByteSize(target, level);
if (size.x() <= 0 || size.y() <= 0 /*|| byteSize == 0*/) {
break;
}
++validLevelCount;
}
return validLevelCount;
}
void VkTextureManager::ResolveViewMipRange(const MG_State::GLState::ITextureObject& texture, Uint32 mipLevels,
Uint32& outBaseMipLevel, Uint32& outLevelCount) {
MOBILEGL_ASSERT(mipLevels > 0, "ResolveViewMipRange: mipLevels must be > 0");
Uint32 definedMipLevels = mipLevels;
if (const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture)) {
const auto& targets = texture.GetUploadTargets();
for (const auto target : targets) {
const Uint32 uploadMipLevels = GetUploadMipLevelCount(*mipTexture, target);
if (uploadMipLevels == 0) {
continue;
}
definedMipLevels = std::min(mipLevels, uploadMipLevels);
break;
}
}
MOBILEGL_ASSERT(definedMipLevels > 0, "ResolveViewMipRange: texture has no defined mip levels");
const auto& levelRange = texture.GetLevelRange();
const Uint32 maxAvailableMipLevel = definedMipLevels - 1;
const Uint32 requestedBaseMipLevel = std::min(static_cast<Uint32>(levelRange.x()), maxAvailableMipLevel);
Uint32 requestedMaxMipLevel = std::min(static_cast<Uint32>(levelRange.y()), maxAvailableMipLevel);
if (requestedMaxMipLevel < requestedBaseMipLevel) {
requestedMaxMipLevel = requestedBaseMipLevel;
}
outBaseMipLevel = requestedBaseMipLevel;
outLevelCount = requestedMaxMipLevel - requestedBaseMipLevel + 1;
}
VkImageAspectFlags VkTextureManager::GetAspectMaskForFormat(VkFormat format) {
switch (format) {
case VK_FORMAT_D16_UNORM:
case VK_FORMAT_X8_D24_UNORM_PACK32:
case VK_FORMAT_D32_SFLOAT:
return VK_IMAGE_ASPECT_DEPTH_BIT;
case VK_FORMAT_S8_UINT:
return VK_IMAGE_ASPECT_STENCIL_BIT;
case VK_FORMAT_D16_UNORM_S8_UINT:
case VK_FORMAT_D24_UNORM_S8_UINT:
case VK_FORMAT_D32_SFLOAT_S8_UINT:
return VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
default:
return VK_IMAGE_ASPECT_COLOR_BIT;
}
}
VkImageAspectFlags VkTextureManager::ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect) {
if ((imageAspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) {
return VK_IMAGE_ASPECT_COLOR_BIT;
}
if ((imageAspect & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) {
return VK_IMAGE_ASPECT_DEPTH_BIT;
}
if ((imageAspect & VK_IMAGE_ASPECT_STENCIL_BIT) != 0) {
return VK_IMAGE_ASPECT_STENCIL_BIT;
}
return imageAspect;
}
} // namespace MobileGL::MG_Backend::DirectVulkan