无法从压缩纹理数据创建图像 (S3TC)

Unable to create image from compressed texture data (S3TC)

我一直在尝试在 Vulkan 中使用 S3TC (BC/DXT) 压缩加载压缩图像,但到目前为止我运气不佳。

以下是 Vulkan 规范关于压缩图像的说明:

https://www.khronos.org/registry/dataformat/specs/1.1/dataformat.1.1.html#S3TC:

Compressed texture images stored using the S3TC compressed image formats are represented as a collection of 4×4 texel blocks, where each block contains 64 or 128 bits of texel data. The image is encoded as a normal 2D raster image in which each 4×4 block is treated as a single pixel.

https://www.khronos.org/registry/vulkan/specs/1.0/xhtml/vkspec.html#resources-images:

For images created with linear tiling, rowPitch, arrayPitch and depthPitch describe the layout of the subresource in linear memory. For uncompressed formats, rowPitch is the number of bytes between texels with the same x coordinate in adjacent rows (y coordinates differ by one). arrayPitch is the number of bytes between texels with the same x and y coordinate in adjacent array layers of the image (array layer values differ by one). depthPitch is the number of bytes between texels with the same x and y coordinate in adjacent slices of a 3D image (z coordinates differ by one). Expressed as an addressing formula, the starting byte of a texel in the subresource has address:

// (x,y,z,layer) are in texel coordinates

address(x,y,z,layer) = layerarrayPitch + zdepthPitch + yrowPitch + xtexelSize + offset

对于压缩格式,rowPitch 是相邻行中压缩块之间的字节数。 arrayPitch 是相邻阵列层中块之间的字节数。 depthPitch 是 3D 图像相邻切片中块之间的字节数。

// (x,y,z,layer) are in block coordinates

address(x,y,z,layer) = layerarrayPitch + zdepthPitch + yrowPitch + xblockSize + offset;

对于未创建为数组的图像,未定义 arrayPitch。 depthPitch 仅为 3D 图像定义。

对于颜色格式,VkImageSubresource 的 aspectMask 成员必须是 VK_IMAGE_ASPECT_COLOR_BIT。对于 depth/stencil 格式,纵横比必须是 VK_IMAGE_ASPECT_DEPTH_BIT 或 VK_IMAGE_ASPECT_STENCIL_BIT。在分别存储深度和模板方面的实现中,查询每个子资源布局将 return 不同的偏移量和大小,表示用于该方面的内存区域。在存储深度和模板方面交错的实现中,相同的偏移量和大小被 returned 并表示交错的内存分配。

我的图像是普通的二维图像(0 层,1 个 mipmap),所以没有 arrayPitchdepthPitch。由于硬件直接支持 S3TC 压缩,因此应该可以在不先解压缩的情况下使用图像数据。在 OpenGL 中,这可以使用 glCompressedTexImage2D 完成,这在过去对我有用。

在 OpenGL 中我使用 GL_COMPRESSED_RGBA_S3TC_DXT1_EXT 作为图像格式,对于 Vulkan 我使用 VK_FORMAT_BC1_RGBA_UNORM_BLOCK,这应该是等价的。 这是我映射图像数据的代码:

auto dds = load_dds("img.dds");
auto *srcData = static_cast<uint8_t*>(dds.data());
auto *destData = static_cast<uint8_t*>(vkImageMapPtr); // Pointer to mapped memory of VkImage
destData += layout.offset(); // layout = VkImageLayout of the image
assert((w %4) == 0);
assert((h %4) == 0);
assert(blockSize == 8); // S3TC BC1
auto wBlocks = w /4;
auto hBlocks = h /4;
for(auto y=decltype(hBlocks){0};y<hBlocks;++y)
{
    auto *rowDest = destData +y *layout.rowPitch(); // rowPitch is 0
    auto *rowSrc = srcData +y *(wBlocks *blockSize);
    for(auto x=decltype(wBlocks){0};x<wBlocks;++x)
    {
        auto *pxDest = rowDest +x *blockSize;
        auto *pxSrc = rowSrc +x *blockSize; // 4x4 image block
        memcpy(pxDest,pxSrc,blockSize); // 64Bit per block
    }
}

下面是初始化图像的代码:

vk::Device device = ...; // Initialization
vk::AllocationCallbacks allocatorCallbacks = ...; // Initialization
[...] // Load the dds data
uint32_t width = dds.width();
uint32_t height = dds.height();
auto format = dds.format(); // = vk::Format::eBc1RgbaUnormBlock;

vk::Extent3D extent(width,height,1);

vk::ImageCreateInfo imageInfo(
    vk::ImageCreateFlagBits(0),
    vk::ImageType::e2D,format,
    extent,1,1,
    vk::SampleCountFlagBits::e1,
    vk::ImageTiling::eLinear,
    vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eColorAttachment,
    vk::SharingMode::eExclusive,
    0,nullptr,
    vk::ImageLayout::eUndefined
);

vk::Image img = nullptr;
device.createImage(&imageInfo,&allocatorCallbacks,&img);

vk::MemoryRequirements memRequirements;
device.getImageMemoryRequirements(img,&memRequirements);
uint32_t typeIndex = 0;
get_memory_type(memRequirements.memoryTypeBits(),vk::MemoryPropertyFlagBits::eHostVisible,typeIndex); // -> typeIndex is set to 1
auto szMem = memRequirements.size();
vk::MemoryAllocateInfo memAlloc(szMem,typeIndex);
vk::DeviceMemory mem;
device.allocateMemory(&memAlloc,&allocatorCallbacks,&mem); // Note: Using the default allocation (nullptr) doesn't change anything
device.bindImageMemory(img,mem,0);

uint32_t mipLevel = 0;
vk::ImageSubresource resource(
    vk::ImageAspectFlagBits::eColor,
    mipLevel,
    0
);
vk::SubresourceLayout layout;
device.getImageSubresourceLayout(img,&resource,&layout);

auto *srcData = device.mapMemory(mem,0,szMem,vk::MemoryMapFlagBits(0));
[...] // Map the dds-data (See code from first post)
device.unmapMemory(mem);

代码运行没有问题,但是生成的图像不正确。这是源图像:

这是结果:

我确定问题出在我发布的第一个代码片段中,但是,如果没有,我已经从 Vulkan SDK 中编写了一个三角形演示的小改编,它产生了同样的结果。可以下载here。包含源代码,我从三角形演示中更改的是 tri.c 中的 "demo_prepare_texture_image" 函数(第 803 至 903 行)和 "dds.cpp" 和 "dds.h" 文件。 "dds.cpp" 包含用于加载 dds 和映射图像内存的代码。

我正在使用 gli 加载 dds 数据(应该是 "work perfectly with Vulkan"),它也包含在上面的下载中。要构建项目,必须将 Vulkan SDK 包含目录添加到 "tri" 项目,并且必须更改 dds 的路径(tri.c,第 809 行)。

源图像(项目中的"x64/Debug/test.dds")使用DXT1 压缩。我也在不同的硬件上进行了测试,结果相同。

initializing/mapping 压缩图像的任何示例代码也会有很大帮助。

你的问题其实很简单——在demo_prepare_textures函数中,第一行,有一个变量tex_format,设置为VK_FORMAT_B8G8R8A8_UNORM(就是这样在原始样本中)。这最终用于创建 VkImageView。如果你只是将其更改为 VK_FORMAT_BC1_RGBA_UNORM_BLOCK,它会在三角形上正确显示纹理。

顺便说一句 - 您可以使用 Vulkan SDK 安装附带的 RenderDoc 来验证您的纹理是否已正确加载。对其进行捕获,然后查看 TextureViewer 选项卡,Inputs 选项卡显示您的纹理看起来与磁盘上的纹理相同,即使格式不正确也是如此。