Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ private async Task<string> PortFileAsync(

if (destinationTexture is TextureX360 destX && sourceTexture.ResourcePlatform != Platform.X360)
{
destX.Format.MaxMipLevel = destX.Format.MinMipLevel;
destX.Format.Tiled = true;
}

if (!TextureFormatConverter.TryConvertTexture(sourceTexture, destinationTexture, sourceBitmapData, destinationBitmapPath))
Expand All @@ -164,6 +164,14 @@ private async Task<string> PortFileAsync(
}
LogVerbose(verbose, $"Wrote texture bitmap data to {destinationSpec.DisplayName} destination directory.");

if (destinationTexture is TextureX360 destTiled && destTiled.Format.Tiled && File.Exists(destinationBitmapPath))
{
LogVerbose(verbose, $"Tiling X360 bitmap data ({Math.Max(1, (int)destTiled.MipmapLevels)} mip level(s))...");
byte[] linearData = await File.ReadAllBytesAsync(destinationBitmapPath, cancellationToken);
byte[] tiledData = X360TextureUtilities.GetTiled360TextureData(destTiled, linearData);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Swap X360 texture words before tiling

When porting TUB/BPR data to X360, ConvertFormat leaves the destination fetch constant at the GPUTEXTURE_FETCH_CONSTANT default GPUENDIAN_8IN16, and this path now marks the payload tiled. This new write-back only calls GetTiled360TextureData, which reorders blocks but never applies the new SwapEndian8in16 helper, 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 👍 / 👎.

await File.WriteAllBytesAsync(destinationBitmapPath, tiledData, cancellationToken);
}

if (destinationTexture is TextureBPR destBprTexture && File.Exists(destinationBitmapPath))
{
destBprTexture.PlacedDataSize = (uint)new FileInfo(destinationBitmapPath).Length;
Expand Down
220 changes: 161 additions & 59 deletions src/Volatility.Core/Utilities/X360TextureUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use ceil block counts for compressed X360 mips

For block-compressed formats GetX360BlockInfo returns BlockSize = 4, but these calculations floor-divide the mip dimensions. A 10x10 DXT1 mip has 3x3 blocks, while this code treats it as 2x2, dropping the final block row/column and advancing subsequent mip offsets too little; the same floor division appears in ConvertToLinearTexture, so both porting to X360 and detiling existing X360 NPOT compressed textures corrupt their bitmap data. The older CalculateMipSize already uses (width + 3) / 4, which is the shape needed here too.

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)
Expand Down
Loading