From 9390a8ddfea29ce2ad663a092fd95165655d10fd Mon Sep 17 00:00:00 2001 From: Kawe Mazidjatari <48657826+Mauler125@users.noreply.github.com> Date: Sat, 28 Dec 2024 00:31:01 +0100 Subject: [PATCH] Add support for DDS texture arrays The game uses mipmapped DDS texture arrays for particle effect textures, added support for these kind of textures so they can be packaged into RPaks. --- src/assets/texture.cpp | 221 +++++++++++++++++++++++------------------ src/public/texture.h | 8 +- 2 files changed, 133 insertions(+), 96 deletions(-) diff --git a/src/assets/texture.cpp b/src/assets/texture.cpp index 0891633..df3f1c4 100644 --- a/src/assets/texture.cpp +++ b/src/assets/texture.cpp @@ -24,7 +24,7 @@ static void Texture_InternalAddTexture(CPakFileBuilder* const pak, const PakGuid size_t streamedOptSize; } mipSizes{}; - std::vector mips; + std::vector> textureArray; bool isStreamable = false; // does this texture require streaming? true if total size of mip levels would exceed 64KiB. can be forced to false. bool isStreamableOpt = false; // can this texture use optional starpaks? can only be set if pak is version v8 @@ -42,6 +42,7 @@ static void Texture_InternalAddTexture(CPakFileBuilder* const pak, const PakGuid input.Read(ddsh); DXGI_FORMAT dxgiFormat = DXGI_FORMAT_UNKNOWN; + uint8_t arraySize = 1; // Go to the end of the DX10 header if it exists. if (ddsh.ddspf.dwFourCC == '01XD') @@ -50,6 +51,7 @@ static void Texture_InternalAddTexture(CPakFileBuilder* const pak, const PakGuid input.Read(ddsh_dx10); dxgiFormat = ddsh_dx10.dxgiFormat; + arraySize = static_cast(ddsh_dx10.arraySize); isDX10 = true; } else { @@ -72,6 +74,12 @@ static void Texture_InternalAddTexture(CPakFileBuilder* const pak, const PakGuid hdr->height = static_cast(ddsh.dwHeight); Log("-> dimensions: %ux%u\n", ddsh.dwWidth, ddsh.dwHeight); + hdr->arraySize = arraySize; + textureArray.resize(arraySize); + + for (auto& mips : textureArray) + mips.resize(ddsh.dwMipMapCount); + /*MIPMAP HANDLING*/ // set streamable boolean based on if we have disabled it, also don't stream if we have only one mip if (!forceDisableStreaming && ddsh.dwMipMapCount > 1) @@ -80,85 +88,95 @@ static void Texture_InternalAddTexture(CPakFileBuilder* const pak, const PakGuid if (isStreamable && pak->GetVersion() >= 8) isStreamableOpt = true; - mips.resize(ddsh.dwMipMapCount); size_t mipOffset = isDX10 ? 0x94 : 0x80; // add header length unsigned int streamedMipCount = 0; - for (unsigned int mipLevel = 0; mipLevel < ddsh.dwMipMapCount; mipLevel++) + for (auto& mips : textureArray) { - // subtracts 1 so skip mips w/h at 1, gets added back when setting in mipLevel_t - uint16_t mipWidth = 0; - if (hdr->width >> mipLevel > 1) - mipWidth = (hdr->width >> mipLevel) - 1; - - uint16_t mipHeight = 0; - if (hdr->height >> mipLevel > 1) - mipHeight = (hdr->height >> mipLevel) - 1; - - const auto& bytesPerPixel = s_pBytesPerPixel[hdr->imgFormat]; - - const uint8_t x = bytesPerPixel.first; - const uint8_t y = bytesPerPixel.second; - - const uint32_t bppWidth = (y + mipWidth) >> (y >> 1); - const uint32_t bppHeight = (y + mipHeight) >> (y >> 1); - - const size_t slicePitch = x * bppWidth * bppHeight; - const size_t alignedSize = IALIGN16(slicePitch); - - mipLevel_t& mipMap = mips[mipLevel]; - - mipMap.mipOffset = mipOffset; - mipMap.mipSize = slicePitch; - mipMap.mipSizeAligned = alignedSize; - mipMap.mipWidth = static_cast(mipWidth + 1); - mipMap.mipHeight = static_cast(mipHeight + 1); - mipMap.mipLevel = static_cast(mipLevel + 1); - mipMap.mipType = mipType_e::INVALID; - - hdr->dataSize += static_cast(alignedSize); // all mips are aligned to 16 bytes within rpak/starpak - mipOffset += slicePitch; // add size for the next mip's offset - - // important: - // - we cannot have more than 4 streamed mip levels; the engine can - // only store up to 4 streamable mip handles per texture, anything - // else must be stored as a permanent mip! - // - // - there must always be at least 1 permanent mip level, regardless - // of its size. not adhering to this rule will result in a failure - // in ID3D11Device::CreateTexture2D during the runtime. we make - // sure that the smallest mip is always permanent (static) here. - if ((streamedMipCount < MAX_STREAMED_TEXTURE_MIPS) && (mipLevel != (ddsh.dwMipMapCount - 1))) + for (unsigned int mipLevel = 0; mipLevel < mips.size(); mipLevel++) { - // if opt streamable textures are enabled, check if this mip is supposed to be opt streamed - if (isStreamableOpt && mipMap.mipSizeAligned > MAX_STREAM_MIP_SIZE) + // subtracts 1 so skip mips w/h at 1, gets added back when setting in mipLevel_t + uint16_t mipWidth = 0; + if (hdr->width >> mipLevel > 1) + mipWidth = (hdr->width >> mipLevel) - 1; + + uint16_t mipHeight = 0; + if (hdr->height >> mipLevel > 1) + mipHeight = (hdr->height >> mipLevel) - 1; + + const auto& bytesPerPixel = s_pBytesPerPixel[hdr->imgFormat]; + + const uint8_t x = bytesPerPixel.x; + const uint8_t y = bytesPerPixel.y; + + const uint32_t bppWidth = (y + mipWidth) >> (y >> 1); + const uint32_t bppHeight = (y + mipHeight) >> (y >> 1); + + const uint32_t slicePitch = x * bppWidth * bppHeight; + const uint32_t alignedSize = IALIGN16(slicePitch); + + mipLevel_t& mipMap = mips[mipLevel]; + + mipMap.mipOffset = mipOffset; + mipMap.mipSize = slicePitch; + mipMap.mipSizeAligned = alignedSize; + mipMap.mipWidth = static_cast(mipWidth + 1); + mipMap.mipHeight = static_cast(mipHeight + 1); + mipMap.mipLevel = static_cast(mipLevel + 1); + mipMap.mipType = mipType_e::INVALID; + + hdr->dataSize += alignedSize; // all mips are aligned to 16 bytes within rpak/starpak + mipOffset += slicePitch; // add size for the next mip's offset + + // important: + // - texture arrays cannot be streamed. + // + // - we cannot have more than 4 streamed mip levels; the engine can + // only store up to 4 streamable mip handles per texture, anything + // else must be stored as a permanent mip! + // + // - there must always be at least 1 permanent mip level, regardless + // of its size. not adhering to this rule will result in a failure + // in ID3D11Device::CreateTexture2D during the runtime. we make + // sure that the smallest mip is always permanent (static) here. + if (arraySize == 1 && (streamedMipCount < MAX_STREAMED_TEXTURE_MIPS) && (mipLevel != (ddsh.dwMipMapCount - 1))) { - mipSizes.streamedOptSize += mipMap.mipSizeAligned; // only reason this is done is to create the data buffers - hdr->optStreamedMipLevels++; // add a streamed mip level + // if opt streamable textures are enabled, check if this mip is supposed to be opt streamed + if (isStreamableOpt && alignedSize > MAX_STREAM_MIP_SIZE) + { + mipSizes.streamedOptSize += alignedSize; // only reason this is done is to create the data buffers + hdr->optStreamedMipLevels++; // add a streamed mip level - mipMap.mipType = mipType_e::STREAMED_OPT; - } + mipMap.mipType = mipType_e::STREAMED_OPT; + } - // if streamable textures are enabled, check if this mip is supposed to be streamed - else if (isStreamable && mipMap.mipSizeAligned > MAX_PERM_MIP_SIZE) - { - mipSizes.streamedSize += mipMap.mipSizeAligned; // only reason this is done is to create the data buffers - hdr->streamedMipLevels++; // add a streamed mip level + // if streamable textures are enabled, check if this mip is supposed to be streamed + else if (isStreamable && alignedSize > MAX_PERM_MIP_SIZE) + { + mipSizes.streamedSize += alignedSize; // only reason this is done is to create the data buffers + hdr->streamedMipLevels++; // add a streamed mip level + + mipMap.mipType = mipType_e::STREAMED; + } - mipMap.mipType = mipType_e::STREAMED; + streamedMipCount++; } - streamedMipCount++; - } + // texture was not streamed, make it permanent. + if (mipMap.mipType == mipType_e::INVALID) + { + mipSizes.staticSize += alignedSize; - // texture was not streamed, make it permanent. - if (mipMap.mipType == mipType_e::INVALID) - { - mipSizes.staticSize += mipMap.mipSizeAligned; - hdr->mipLevels++; + // if it is larger, then we are processing the subsequent in the + // array. and we shouldn't count mip levels from all subsequent + // textures here. The mip levels get applied on all textures in + // the array. + if (hdr->mipLevels < ddsh.dwMipMapCount) + hdr->mipLevels++; - mipMap.mipType = mipType_e::STATIC; + mipMap.mipType = mipType_e::STATIC; + } } } @@ -185,39 +203,52 @@ static void Texture_InternalAddTexture(CPakFileBuilder* const pak, const PakGuid char* const streamedbuf = new char[mipSizes.streamedSize]; char* const optstreamedbuf = new char[mipSizes.streamedOptSize]; - char* pCurrentPosStatic = dataChunk.data; - char* pCurrentPosStreamed = streamedbuf; - char* pCurrentPosStreamedOpt = optstreamedbuf; - - uint8_t mipLevel = static_cast(mips.size()); - - while (mipLevel > 0) + //for (const auto& mips : textureArray) + for (size_t i = 0; i < textureArray.size(); i++) { - const mipLevel_t& mipMap = mips.at(mipLevel - 1); - input.SeekGet(mipMap.mipOffset); + const auto& mips = textureArray[i]; - switch (mipMap.mipType) + char* pCurrentPosStatic = dataChunk.data; + char* pCurrentPosStreamed = streamedbuf; + char* pCurrentPosStreamedOpt = optstreamedbuf; + + for (auto mipIter = mips.rbegin(); mipIter != mips.rend(); ++mipIter) { - case mipType_e::STATIC: - input.Read(pCurrentPosStatic, mipMap.mipSize); - pCurrentPosStatic += mipMap.mipSizeAligned; // move ptr - - break; - case mipType_e::STREAMED: - input.Read(pCurrentPosStreamed, mipMap.mipSize); - pCurrentPosStreamed += mipMap.mipSizeAligned; // move ptr - - break; - case mipType_e::STREAMED_OPT: - input.Read(pCurrentPosStreamedOpt, mipMap.mipSize); - pCurrentPosStreamedOpt += mipMap.mipSizeAligned; // move ptr - - break; - default: - break; - } + const mipLevel_t& mipMap = *mipIter; + input.SeekGet(mipMap.mipOffset); - mipLevel--; + // only used by static mip types, used for offsetting the pointer + // from mip base into the actual mip corresponding to the texture + // in the array. + char* targetPos; + + switch (mipMap.mipType) + { + case mipType_e::STATIC: + targetPos = pCurrentPosStatic + (mipMap.mipSizeAligned * i); + input.Read(targetPos, mipMap.mipSize); + + // texture arrays group mips together, i.e. mip 1 of texture 1 + // 2 and 3 are directly placed into a contiguous block, and to + // access the second one, the mip size must be multiplied by + // the texture index. + pCurrentPosStatic += mipMap.mipSizeAligned * textureArray.size(); // move ptr + + break; + case mipType_e::STREAMED: + input.Read(pCurrentPosStreamed, mipMap.mipSize); + pCurrentPosStreamed += mipMap.mipSizeAligned; // move ptr + + break; + case mipType_e::STREAMED_OPT: + input.Read(pCurrentPosStreamedOpt, mipMap.mipSize); + pCurrentPosStreamedOpt += mipMap.mipSizeAligned; // move ptr + + break; + default: + break; + } + } } // now time to add the higher level asset entry diff --git a/src/public/texture.h b/src/public/texture.h index 7cbcb12..f68a4d4 100644 --- a/src/public/texture.h +++ b/src/public/texture.h @@ -5,7 +5,13 @@ #define MAX_PERM_MIP_SIZE 0x3FFF // "Any MIP below 64kiB is permanent." #define MAX_STREAM_MIP_SIZE 0xFFFFF -static inline const std::pair s_pBytesPerPixel[] = +struct TextureBytesPerPixel_t +{ + uint8_t x; + uint8_t y; +}; + +static inline const TextureBytesPerPixel_t s_pBytesPerPixel[] = { { uint8_t(8u), uint8_t(4u) }, { uint8_t(8u), uint8_t(4u) },