Jump to content

Unswizzling Xbox 360 textures (Condemned 2 / LithTech Jupiter Ex)


Go to solution Solved by DKDave,

Recommended Posts

Hi,

I’ve been a regular guest of the old XeNTaX forums, but this is my first post in this new community, so hi 🙂

I’m currently working on trying to render maps for LithTech 5 (Jupiter Ex) games, FEAR 2 and Condemned 2. I’ve already decoded most of the world data and can render it pretty well (you can check my findings here). For Condemned 2 though, I’m stuck with the textures. Since the game was never released for PC, I am using the Xbox 360 version.

My biggest issue is that the textures appear to be swizzled, and the unswizzling does not seem to work. I’m not sure if I am using the wrong pattern, or if I use the method in the wrong way. I am currently using AAP’s algorithm (https://github.com/aap/librw/blob/master/src/d3d/xbox.cpp#L661). I am limiting myself for now to diffuse texture that are almost certainly DXT1.

To make it more complicated, the texture header is not default DDS, and I couldn’t reliably detect the width/height yet. For the example I'm testing with (see below) I know it should be 1024*1024 since the same texture is present in the PC version of FEAR2. On the PC, this texture (without DDS header) has 699,048 bytes including 9 mip levels. The version on XBox is 720,896 bytes, so that’s mathematically a 1024*1048 image. I assume there is also some mipmaps included, but that still would be too little, so possibly also some additional padding (there are many zeros in the example file).

I assume that the unswizzling must be done on the raw DXT1 data with the proper block size, but I’m uncertain if I need to pass the full raw data to the unswizzling, and with what parameters for width, height, and depth? I assume the bytesPerPixel for unswizzling must be 8 (DXT1 block size) and width/height probably divided by 4, i.e. something like:

Unswizzle(data, w = width / 4, h = height / 4, bpp = 8);

If I try to hand in the full data, i.e. the theoretical 1024*1048 image block, the algorithm will throw an array-out-of-bounds exception. If I feed just the base mip level (1024*1024), it will run, but the image still looks distorted.

This is the swizzled version from Condemned 2 Xbox (just adding a DDS DXT1 header and flipping byte order), this is the unswizzling above applied, and this is the properly decoded version (from FEAR2 PC). Just from the visuals, you can see that the 4x4 DXT1 blocks are correct, just misplaced. I also attached the raw Xbox texture data, just in case. I also got the raw, headless data here, if you wish to have a look.

Maybe someone with experience on that topic can spot from the image what issue could be, or if I need a different type of swizzling pattern, or may be doing something else wrong? Any help is greatly appreciated. 😊

Link to comment
Share on other sites

Thanks for the links ikskoks! The file format seems a bit too different to use directly with EA Graphics Manager, but I'll definitely check out the algorithm. I *think* that what I linked above is also supposed to compute Morton Order, but then again there could be a subtle difference. I'll let you know once I've checked.

In the meanwhile, I also tried to find of swizzled / unswizzled comparison to understand better what happens... I didn't understand the pattern yet, but at least it appears that some (all?) textures are padded to square size.

texture_examples.png

  • Like 1
Link to comment
Share on other sites

Yeah, something weird is going on with this texture as standard morton order algorithm doesn't seem to work no matter how do I proceed with the data.
Custom swizzling maybe? Or alignment issue? I'm not sure.

Link to comment
Share on other sites

  • Solution
23 hours ago, micTronic said:

Hi,

I’ve been a regular guest of the old XeNTaX forums, but this is my first post in this new community, so hi 🙂

I’m currently working on trying to render maps for LithTech 5 (Jupiter Ex) games, FEAR 2 and Condemned 2. I’ve already decoded most of the world data and can render it pretty well (you can check my findings here). For Condemned 2 though, I’m stuck with the textures. Since the game was never released for PC, I am using the Xbox 360 version.

My biggest issue is that the textures appear to be swizzled, and the unswizzling does not seem to work. I’m not sure if I am using the wrong pattern, or if I use the method in the wrong way. I am currently using AAP’s algorithm (https://github.com/aap/librw/blob/master/src/d3d/xbox.cpp#L661). I am limiting myself for now to diffuse texture that are almost certainly DXT1.

To make it more complicated, the texture header is not default DDS, and I couldn’t reliably detect the width/height yet. For the example I'm testing with (see below) I know it should be 1024*1024 since the same texture is present in the PC version of FEAR2. On the PC, this texture (without DDS header) has 699,048 bytes including 9 mip levels. The version on XBox is 720,896 bytes, so that’s mathematically a 1024*1048 image. I assume there is also some mipmaps included, but that still would be too little, so possibly also some additional padding (there are many zeros in the example file).

I assume that the unswizzling must be done on the raw DXT1 data with the proper block size, but I’m uncertain if I need to pass the full raw data to the unswizzling, and with what parameters for width, height, and depth? I assume the bytesPerPixel for unswizzling must be 8 (DXT1 block size) and width/height probably divided by 4, i.e. something like:

Unswizzle(data, w = width / 4, h = height / 4, bpp = 8);

If I try to hand in the full data, i.e. the theoretical 1024*1048 image block, the algorithm will throw an array-out-of-bounds exception. If I feed just the base mip level (1024*1024), it will run, but the image still looks distorted.

This is the swizzled version from Condemned 2 Xbox (just adding a DDS DXT1 header and flipping byte order), this is the unswizzling above applied, and this is the properly decoded version (from FEAR2 PC). Just from the visuals, you can see that the 4x4 DXT1 blocks are correct, just misplaced. I also attached the raw Xbox texture data, just in case. I also got the raw, headless data here, if you wish to have a look.

Maybe someone with experience on that topic can spot from the image what issue could be, or if I need a different type of swizzling pattern, or may be doing something else wrong? Any help is greatly appreciated. 😊

Having looked at the data, it's not Morton swizzling, but it is DXT1.

Firstly you need to swap the endian of the data as it's XBox 360 (every 2 bytes).

Then you need to untile the data (not sure what the X360 method is called, but it's Untile360DXT in Noesis), and then you can decode the DXT1 image to get the final data as follows (resized for display purposes here)

texture_raw_test.jpg.b81da9417f35c388285818c70b5ababa.jpg

 

 

 

 

 

  • Like 3
Link to comment
Share on other sites

Awesome, untiling really was the solution! And I was already drawing sheets over sheets of paper finding out how it works... strange that I haven't read about that more often. Thanks to both of you for helping out!!! I'll keep updating my repo with further findings on LithTech. 🙂

Link to comment
Share on other sites

Here's some C++ code adapted from NCDyson that I've used, if it helps. For BC1, blockPixelSize=4 and texelByteSize=8. Note the Xbox Xenon's address-swizzled textures are more complex than Xbox 2001's simpler Morton curve nested tiling, using a combination of nested tiling, interleaving, and parity swapping too (the name Untile360DXT is a bit of a misnomer, as tiling alone is insufficient to express the swizzle pattern). You'll also need to (like DKDave said) swap the bytes on the source before calling it.

    // Adapted from https://github.com/NCDyson/RareView/blob/master/RareView/Texture.cs
    // Model Viewer for RARE games on Xbox 360

   int XGAddress2DTiledX(uint32_t blockOffset, uint32_t widthInBlocks, uint32_t texelBytePitch)
    {
        uint32_t alignedWidth = (widthInBlocks + 31) & ~31;

        uint32_t logBpp = (texelBytePitch >> 2) + ((texelBytePitch >> 1) >> (texelBytePitch >> 2));
        uint32_t offsetByte = blockOffset << logBpp; // Essentially offset * texelBytePitch.
        uint32_t offsetTile = ((offsetByte & ~0xFFF) >> 3) + ((offsetByte & 0x700) >> 2) + (offsetByte & 0x3F);
        uint32_t offsetMacro = offsetTile >> (7 + logBpp);

        uint32_t macroX = ((offsetMacro % (alignedWidth >> 5)) << 2);
        uint32_t tile = ((((offsetTile >> (5 + logBpp)) & 2) + (offsetByte >> 6)) & 3);
        uint32_t macro = (macroX + tile) << 3;
        uint32_t micro = (((((offsetTile >> 1) & ~0xF) + (offsetTile & 0xF)) & ((texelBytePitch << 3) - 1))) >> logBpp;

        return macro + micro;
    }

    int XGAddress2DTiledY(uint32_t blockOffset, uint32_t widthInBlocks, uint32_t texelBytePitch)
    {
        uint32_t alignedWidth = (widthInBlocks + 31) & ~31;

        uint32_t logBpp = (texelBytePitch >> 2) + ((texelBytePitch >> 1) >> (texelBytePitch >> 2));
        uint32_t offsetByte = blockOffset << logBpp;
        uint32_t offsetTile = ((offsetByte & ~0xFFF) >> 3) + ((offsetByte & 0x700) >> 2) + (offsetByte & 0x3F);
        uint32_t offsetMacro = offsetTile >> (7 + logBpp);

        uint32_t macroY = ((offsetMacro / (alignedWidth >> 5)) << 2);
        uint32_t tile = ((offsetTile >> (6 + logBpp)) & 1) + (((offsetByte & 0x800) >> 10));
        uint32_t macro = (macroY + tile) << 3;
        uint32_t micro = ((((offsetTile & (((texelBytePitch << 6) - 1) & ~0x1F)) + ((offsetTile & 0xF) << 1)) >> (3 + logBpp)) & ~1);

        return macro + micro + ((offsetTile & 0x10) >> 4);
    }

    std::vector<uint8_t> Xbox360ConvertToLinearTexture(array_ref<uint8_t const> data, int pixelWidth, int pixelHeight, GraphicFormat textureFormat)
    {
        std::vector<uint8_t> destData(data.size());
        uint32_t blockPixelSize;
        uint32_t texelBytePitch;

        switch (textureFormat)
        {
        case TEXTURE_FORMAT_A8L8:
            blockPixelSize = 1;
            texelBytePitch = 2;
            break;
        case TEXTURE_FORMAT_L8: // LinearPaletteIndex8bpp:
            blockPixelSize = 1;
            texelBytePitch = 1;
            break;
        case TEXTURE_FORMAT_DXT1: // Bc1Dxt1
            blockPixelSize = 4;
            texelBytePitch = 8;
            break;
        case TEXTURE_FORMAT_DXT3: // Bc2Dxt2 & Bc2Dxt3
        case TEXTURE_FORMAT_DXT5: // Bc3Dxt4 & Bc3Dxt5
        case TEXTURE_FORMAT_DXN:
            blockPixelSize = 4;
            texelBytePitch = 16;
            break;
        case TEXTURE_FORMAT_A8R8G8B8: // {b8,g8,r8,ap8}
            blockPixelSize = 1;
            texelBytePitch = 4;
            break;
        case TEXTURE_FORMAT_X4R4G4B4: // {b4,g4,r4,x4}
            blockPixelSize = 1;
            texelBytePitch = 2;
            break;
        case TEXTURE_FORMAT_R5G6B5: // {b5,g6,r5}
            blockPixelSize = 1;
            texelBytePitch = 2;
            break;
        default:
            throw std::invalid_argument("Bad texture type!");
        }

        // Width and height in number of blocks.
        // So a 256x128 DXT1 image would be 64x32 in 4x4 blocks.
        uint32_t widthInBlocks = pixelWidth / blockPixelSize;
        uint32_t heightInBlocks = pixelHeight / blockPixelSize;

        // This loops in terms of the swizzled source.
        for (uint32_t j = 0; j < heightInBlocks; j++)
        {
            for (uint32_t i = 0; i < widthInBlocks; i++)
            {
                uint32_t blockOffset = j * widthInBlocks + i;
                uint32_t x = XGAddress2DTiledX(blockOffset, widthInBlocks, texelBytePitch);
                uint32_t y = XGAddress2DTiledY(blockOffset, widthInBlocks, texelBytePitch);
                uint32_t srcByteOffset = j * widthInBlocks * texelBytePitch + i * texelBytePitch;
                uint32_t destByteOffset = y * widthInBlocks * texelBytePitch + x * texelBytePitch;

                if (destByteOffset + texelBytePitch > destData.size()) continue;
                memcpy(&destData[destByteOffset], &data[srcByteOffset], texelBytePitch);
            }
        }

        return destData;
    }

condemned-dxt1-1024x1048-xbox-xenon-address-swizzle.png

Edited by piken
  • Thanks 2
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...