集成LibKtx

题图是B站一个up 獭可 看着可爱现在这借用一下OwO

这是一篇简单的小记录---集成对ktx文件的支持到我在写的渲染器中,期间遇到的一点小挫折。

1. What and why is KTX

Khronos texture,就是ktx。
ktx是一种纹理容器,他是内部可以是不同的压缩格式。目前比较常用的是ASTC纹理的ktx文件。

首先最直观的感受就是,转化为了ktx后文件变小了,而且清晰度也差不多。

当然最重要的事情是,这种纹理的压缩是能在GPU内实时解算的,意味着我们能直接把数据送到GPU里当texture采样,作为对比,如果使用jpeg格式,我们可能先需要把其用库(stbi?)解压成rgba8888格式,然后才能送进显存中采样。

更好的一点是:你甚至可以把纹理数组、mipmap、CubeMap一股脑的送进一个KTX文件,然后一次性加载出来送到GPU里去,非常方便!

这种格式的加载是需要GPU支持的,但好消息是:这东西支持十分广泛(或者说采用了ASTC纹理的KTX2支持十分广泛)。

对于Android,85%使用GooglePlay的手机都支持ASTC纹理(官方Android指南有写道),对于Apple A8以上的芯片可以完美支持ASTC(在这之前的通用格式是PVRTC,当然之后也通用),对于PC,openGL和Vulkan支持十分的好....DX有自己的DDS就是了

所以为了跨平台性,我选择KTX + ASTC!

2. 集成LibKtx

khronos官方的库 LibKTX提供了方便快捷的加载方式,可以一次性的把所有数据上传到显存中,然后返回给你一个VkImage对象!
真是太方便辣......才怪

在这个库的设计之初竟然没考虑到用外部的Vulkan显存分配器,这也意味着你一旦使用了他的加载方式,就得为即将迎来的显存碎片化,OOM做好准备....
直到23年在社区的推动下外部分配器的嵌入接口才完成了...

mantainer:我和VMA不熟 (真-原话)

于是巨他妈难用的接口诞生了,首先你先得创建一个叫vdi的东西,这是个vulkan设备的大打包盒,放着物理设备,逻辑设备,队列...
然后你得手动给其设置回调函数,把VMA Allocation, VMAMap, VMAUnMap等等函数再狠狠包装一遍....然后你并不能调用VMA CreateImage那套,得用最传统的VMA Allocation,也是VMA官方最不建议用的那种...
并不是我用错了,详情可以去查看libKtx-VKLoadTests这个官方示例,非常,非常挫,我一开始尝试把这套移植到我的框架上,然后发现...这东西直接击穿了我现有的Buffer/Texture管理,并且十分十分不好写。

当然我们可以绕过他的实现手动把数据写到显存里去....有点复杂就是
经过我不懈的寻找下....终于发现SaschaWillems的Vulkan exsample里确实有使用到KTX加载
具体的正确代码去看他那的就好了..我这贴一下我的实现,如果有看不懂的地方就是我的封装了....脑内自动翻译下就好

    layerCount = ktxTexture->arrLen;
    ......    

	if (!isCube) {
		for (uint32_t layer = 0; layer < std::min(layerCount,textureHandler->arrayLayer()); layer++)
		{
			for (uint32_t level = 0; level < std::min(mipLevels, textureHandler->mipLevelCount()); level++)
			{
				ktx_size_t offset;
				KTX_error_code result = ktxTexture_GetImageOffset(texture, level, layer, 0, &offset);
				if (result != KTX_SUCCESS) {
					Logger::instance()->log("KTX Loader", lvl::err, "Get image offset error -> {}. || in arraylayer {}, mipmapLvl {}", ktxErrorString(result), layer, level);
					return ;
				}
				BufferManager::BufferToImage bufferCopyInfo{};
				bufferCopyInfo.baseArrayer = layer;
				bufferCopyInfo.layerCount = 1;
				bufferCopyInfo.width = std::max(1u, texture->baseWidth >> level);
				bufferCopyInfo.height = std::max(1u, texture->baseHeight >> level);
				bufferCopyInfo.depth = 1;
				bufferCopyInfo.bufferOffset = offset;
				bufferCopyInfo.mipmapLevel = level;
				mCopyInfo.push_back(bufferCopyInfo);
			}
		}
	}
	else {
		uint32_t offset = 0;

		if (info.arrayLen > 1) {
			Logger::instance()->log("KTX Loader", lvl::err, "Currently cube map array is not supported, The behaviours followed is unknown.");
			return ;
		}
		for (uint32_t face = 0; face < 6; face++)
		{
			for (uint32_t level = 0; level < mipLevels; level++)
			{
				// Calculate offset into staging buffer for the current mip level and face
				ktx_size_t offset;
				KTX_error_code ret = ktxTexture_GetImageOffset(texture, level, 0, face, &offset);
				if (ret != KTX_SUCCESS) {
					Logger::instance()->log("KTX Loader", lvl::err, "Get image offset error -> {}. || in face {}, mipmapLvl {}", ktxErrorString(ret), face, level);
					return ;
				}
				BufferManager::BufferToImage bufferCopyInfo{};
				bufferCopyInfo.baseArrayer = face;
				bufferCopyInfo.layerCount = 1;
				bufferCopyInfo.width = std::max(1u, texture->baseWidth >> level);
				bufferCopyInfo.height = std::max(1u, texture->baseHeight >> level);
				bufferCopyInfo.depth = 1;
				bufferCopyInfo.bufferOffset = offset;
				bufferCopyInfo.mipmapLevel = level;
				mCopyInfo.push_back(bufferCopyInfo);
			}
		}
	}
    auto totoalDataSize = texture->dataSize;
	auto textureDataPtr = ktxTexture_GetData(texture);
	auto bufferHandler = this->mBufferManager->requestBuffer(Buffer::StageBuffer, totoalDataSize, false, true, textureDataPtr, totoalDataSize);
	this->mBufferManager->copyBufferToImage(bufferHandler, textureHandler);
    //To you:   vkPipelineBarrier -> imageLayoutTransferDst -> vkBufferToImage -> vkPipelineBarrier -> imageLayoutShaderSAmpledReadOnly

简单来说:就是对每个CubeMapface(对于普通图像就是只有一面)的每个数组成员的每个mipmap等级,提取其在ktx data中的offset,然后复制到显存中....

那为什么我会称其为挫折呢...很容易不是
其实我一开始看的是libktx内部的实现,只能说我看不懂....还以为是什么不同的mipmap等级之间还会有会随着elementSize变的padding...同时还在想---ImageMipmap/layer的内存位置不会也有约定吧(后来想想copyImage的subResource应该已经把这些指定好了)
真的在看到libktx的实现的时候我是害怕了,然后疯狂寻求借助官方封装....反正很神奇

额外

目前我司的自研引擎就是用的ktx(貌似ASTC?)作为Android平台上的贴图格式,这东西确实好,就是转换时间特别特别慢。

突然想想国内没几个经过商业项目验证的全平台自研引擎...我司算一个(虽然效果挺搓的但毕竟一开始是针对手机平台开发的...能理解,但理解的不多),里面的很多模块处于一种尚未非常屎山的地步,实现还是能看着学习一些的(比如我最近在看字体渲染

所以做这种自研引擎的引擎开发还是挺有趣的(估计以后也不会有这种机会了)
...只要别让我写UI对接客户端就好QwQ

END