-
Notifications
You must be signed in to change notification settings - Fork 2
Proper X360 tiling/detiling support #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -153,88 +153,190 @@ public static void WriteUntiled360TextureFile(TextureX360 xboxHeader, string tex | |
| public static byte[] GetUntiled360TextureData(TextureX360 xboxHeader, byte[] bitmapData) | ||
| { | ||
| return xboxHeader.Format.Tiled | ||
| ? ConvertToLinearTexture(bitmapData, xboxHeader.Width, xboxHeader.Height, xboxHeader.Format.DataFormat) | ||
| ? ConvertToLinearTexture(bitmapData, xboxHeader.Width, xboxHeader.Height, xboxHeader.MipmapLevels, xboxHeader.Format.DataFormat) | ||
| : bitmapData; | ||
| } | ||
|
|
||
| // THE BELOW CODE IS CREDITED TO NCDyson for RareView | ||
| // AND "Pimpin Tyler and Anthony" for GTA IV Xbox 360 Texture Editor | ||
| public static byte[] GetTiled360TextureData(TextureX360 xboxHeader, byte[] bitmapData) | ||
| { | ||
| return xboxHeader.Format.Tiled | ||
| ? ConvertToTiledTexture(bitmapData, xboxHeader.Width, xboxHeader.Height, xboxHeader.MipmapLevels, xboxHeader.Format.DataFormat) | ||
| : bitmapData; | ||
| } | ||
|
|
||
| // It originated from the GTA IV Xbox 360 Texture Editor, | ||
| // in which its source code was released publicly. | ||
| // X360 GPU surfaces are stored as 16 bit big endian words. | ||
| public static void SwapEndian8in16(byte[] data) | ||
| { | ||
| int count = data.Length & ~1; | ||
| for (int i = 0; i < count; i += 2) | ||
| { | ||
| (data[i], data[i + 1]) = (data[i + 1], data[i]); | ||
| } | ||
| } | ||
|
|
||
| // I borrowed it from RareView as links to the GTA version seem to be dead. | ||
| private static (int BlockSize, int TexelPitch) GetX360BlockInfo(GPUTEXTUREFORMAT format) | ||
| { | ||
| return format switch | ||
| { | ||
| GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8 | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8_A | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8_B | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_1 => (1, 1), | ||
|
|
||
| GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8_8 | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_4_4_4_4 | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_5_6_5 | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_1_5_5_5 | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_6_5_5 | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_16 | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_16_FLOAT => (1, 2), | ||
|
|
||
| GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8_8_8_8 | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_2_10_10_10 | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_16_16 | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_16_16_FLOAT | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_32 | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_32_FLOAT => (1, 4), | ||
|
|
||
| GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_16_16_16_16 | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_16_16_16_16_FLOAT | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_32_32 => (1, 8), | ||
|
|
||
| // THERE WAS NO PROPER LICENSE FOR IT THOUGH, I DID NOT WRITE IT!! | ||
| GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT1 | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT1_AS_16_16_16_16 | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT3A | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT5A | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_CTX1 => (4, 8), | ||
|
|
||
| GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT2_3 | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT2_3_AS_16_16_16_16 | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT4_5 | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT4_5_AS_16_16_16_16 | ||
| or GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXN => (4, 16), | ||
|
|
||
| // Its calculations are accurate to what the Xbox 360 does officially, | ||
| // so the output would be identical whether I spent the insane amount | ||
| // of hours figuring it out and writing it opposed to not reinventing the wheel. | ||
| _ => throw new ArgumentOutOfRangeException(nameof(format), format, "Unsupported X360 texture format for detiling."), | ||
| }; | ||
| } | ||
|
|
||
| private static byte[] ConvertToLinearTexture(byte[] data, int _width, int _height, GPUTEXTUREFORMAT _textureFormat) | ||
| // The 360 stores a tiled surface padded to 32-block tiles in BOTH dimensions, so the source | ||
| // mip's storage is alignedBlockWidth x alignedBlockHeight blocks. We walk every block of that | ||
| // padded grid, map its tiled-storage offset back to its linear (x, y) with the inverse address | ||
| // math below, drop the padding blocks that fall outside the real WxH, and write the rest into a | ||
| // tightly-packed linear mip. Each subsequent mip is read from the next aligned-tiled region | ||
| // (BaseAddress/MipAddress packing aside; this assumes consecutive per-mip tiled storage, which | ||
| // is the common case -- mip 0, the surface that matters most, is always correct). Bounds are | ||
| // clamped so a short/odd buffer can never throw or scatter out of range. | ||
| private static byte[] ConvertToLinearTexture(byte[] data, int width, int height, int mipCount, GPUTEXTUREFORMAT format) | ||
| { | ||
| byte[] destData = new byte[data.Length]; | ||
| (int blockSize, int texelPitch) = GetX360BlockInfo(format); | ||
|
|
||
| int blockSize; | ||
| int texelPitch; | ||
| using MemoryStream linearStream = new(); | ||
| int srcMipOffset = 0; | ||
| int mipWidth = width; | ||
| int mipHeight = height; | ||
|
|
||
| switch (_textureFormat) | ||
| for (int mip = 0; mip < Math.Max(1, mipCount); mip++) | ||
| { | ||
| case GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8_8: | ||
| blockSize = 1; | ||
| texelPitch = 2; | ||
| break; | ||
| case GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8: | ||
| blockSize = 1; | ||
| texelPitch = 1; | ||
| break; | ||
| case GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT1: | ||
| blockSize = 4; | ||
| texelPitch = 8; | ||
| break; | ||
| case GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT2_3: | ||
| case GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT4_5: | ||
| case GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXN: | ||
| blockSize = 4; | ||
| texelPitch = 16; | ||
| break; | ||
| case GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8_8_8_8: | ||
| blockSize = 1; | ||
| texelPitch = 4; | ||
| break; | ||
| case GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_4_4_4_4: | ||
| blockSize = 1; | ||
| texelPitch = 2; | ||
| break; | ||
| case GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_5_6_5: | ||
| blockSize = 1; | ||
| texelPitch = 2; | ||
| if (srcMipOffset >= data.Length) | ||
| { | ||
| break; | ||
| default: | ||
| throw new ArgumentOutOfRangeException("Bad texture type!"); | ||
| } | ||
|
|
||
| int blockWidth = Math.Max(1, mipWidth / blockSize); | ||
| int blockHeight = Math.Max(1, mipHeight / blockSize); | ||
| int alignedBlockWidth = (blockWidth + 31) & ~31; | ||
| int alignedBlockHeight = (blockHeight + 31) & ~31; | ||
|
|
||
| int linearMipBytes = blockWidth * blockHeight * texelPitch; | ||
| byte[] linearMip = new byte[linearMipBytes]; | ||
|
|
||
| int tiledBlockCount = alignedBlockWidth * alignedBlockHeight; | ||
| for (int tiledOffset = 0; tiledOffset < tiledBlockCount; tiledOffset++) | ||
| { | ||
| int x = XGAddress2DTiledX(tiledOffset, blockWidth, texelPitch); | ||
| int y = XGAddress2DTiledY(tiledOffset, blockWidth, texelPitch); | ||
|
|
||
| // Skip the tile padding that falls outside the real surface. | ||
| if (x >= blockWidth || y >= blockHeight) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| int srcOffset = srcMipOffset + tiledOffset * texelPitch; | ||
| int destOffset = (y * blockWidth + x) * texelPitch; | ||
| if (srcOffset + texelPitch > data.Length || destOffset + texelPitch > linearMipBytes) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| Array.Copy(data, srcOffset, linearMip, destOffset, texelPitch); | ||
| } | ||
|
|
||
| linearStream.Write(linearMip, 0, linearMipBytes); | ||
|
|
||
| srcMipOffset += alignedBlockWidth * alignedBlockHeight * texelPitch; | ||
| mipWidth = Math.Max(1, mipWidth / 2); | ||
| mipHeight = Math.Max(1, mipHeight / 2); | ||
| } | ||
|
|
||
| int blockWidth = _width / blockSize; | ||
| int blockHeight = _height / blockSize; | ||
| return linearStream.ToArray(); | ||
| } | ||
|
|
||
| private static byte[] ConvertToTiledTexture(byte[] data, int width, int height, int mipCount, GPUTEXTUREFORMAT format) | ||
| { | ||
| (int blockSize, int texelPitch) = GetX360BlockInfo(format); | ||
|
|
||
| using MemoryStream tiledStream = new(); | ||
| int srcMipOffset = 0; | ||
| int mipWidth = width; | ||
| int mipHeight = height; | ||
|
|
||
| for (int j = 0; j < blockHeight; j++) | ||
| for (int mip = 0; mip < Math.Max(1, mipCount); mip++) | ||
| { | ||
| for (int i = 0; i < blockWidth; i++) | ||
| if (srcMipOffset >= data.Length) | ||
| { | ||
| int blockOffset = j * blockWidth + i; | ||
| break; | ||
| } | ||
|
|
||
| int blockWidth = Math.Max(1, mipWidth / blockSize); | ||
| int blockHeight = Math.Max(1, mipHeight / blockSize); | ||
|
Comment on lines
+301
to
+302
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For block-compressed formats Useful? React with 👍 / 👎. |
||
| int alignedBlockWidth = (blockWidth + 31) & ~31; | ||
| int alignedBlockHeight = (blockHeight + 31) & ~31; | ||
|
|
||
| int x = XGAddress2DTiledX(blockOffset, blockWidth, texelPitch); | ||
| int y = XGAddress2DTiledY(blockOffset, blockWidth, texelPitch); | ||
| int linearMipBytes = blockWidth * blockHeight * texelPitch; | ||
|
|
||
| int srcOffset = j * blockWidth * texelPitch + i * texelPitch; | ||
| int destOffset = y * blockWidth * texelPitch + x * texelPitch; | ||
| //TODO: ConvertToLinearTexture apparently breaks on on textures with a height of 64... | ||
| if (destOffset >= destData.Length) continue; | ||
| Array.Copy(data, srcOffset, destData, destOffset, texelPitch); | ||
| int tiledBlockCount = alignedBlockWidth * alignedBlockHeight; | ||
| byte[] tiledMip = new byte[tiledBlockCount * texelPitch]; | ||
|
|
||
| for (int tiledOffset = 0; tiledOffset < tiledBlockCount; tiledOffset++) | ||
| { | ||
| int x = XGAddress2DTiledX(tiledOffset, blockWidth, texelPitch); | ||
| int y = XGAddress2DTiledY(tiledOffset, blockWidth, texelPitch); | ||
|
|
||
| // Skip the tile padding that has no corresponding linear source block. | ||
| if (x >= blockWidth || y >= blockHeight) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| int srcOffset = srcMipOffset + (y * blockWidth + x) * texelPitch; | ||
| int destOffset = tiledOffset * texelPitch; | ||
| if (srcOffset + texelPitch > data.Length || destOffset + texelPitch > tiledMip.Length) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| Array.Copy(data, srcOffset, tiledMip, destOffset, texelPitch); | ||
| } | ||
|
|
||
| tiledStream.Write(tiledMip, 0, tiledMip.Length); | ||
|
|
||
| srcMipOffset += linearMipBytes; | ||
| mipWidth = Math.Max(1, mipWidth / 2); | ||
| mipHeight = Math.Max(1, mipHeight / 2); | ||
| } | ||
|
|
||
| return destData; | ||
| return tiledStream.ToArray(); | ||
| } | ||
|
|
||
| private static int XGAddress2DTiledX(int Offset, int Width, int TexelPitch) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When porting TUB/BPR data to X360,
ConvertFormatleaves the destination fetch constant at theGPUTEXTURE_FETCH_CONSTANTdefaultGPUENDIAN_8IN16, and this path now marks the payload tiled. This new write-back only callsGetTiled360TextureData, which reorders blocks but never applies the newSwapEndian8in16helper, so multi-byte texel/block data is written in PC byte order while the X360 header advertises 8-in-16 endian storage; those converted textures will be byte-swapped incorrectly on load and render corrupted.Useful? React with 👍 / 👎.