//#define DISABLE_TWINDDLING_ROUTINE #define ASSUME_IMAGE_TILING using System.Runtime.CompilerServices; namespace AssetRipper.TextureDecoder.Pvrtc { public static partial class PvrtcDecoder { /// /// Decompresses PVRTC to RGBA 8888 /// /// The PVRTC texture data to decompress /// X dimension (width) of the texture /// Y dimension (height) of the texture /// The decompressed texture data /// Signifies whether the data is PVRTC2 or PVRTC4 /// public unsafe static void DecompressPVRTC(ReadOnlySpan input, int xDim, int yDim, bool do2bitMode, Span output) { fixed (byte* inputPtr = input) { PVRDecompress(inputPtr, xDim, yDim, do2bitMode, output); } } private unsafe static void PVRDecompress(byte* compressedData, int xDim, int yDim, bool do2bitMode, Span output) { int xBlockSize = do2bitMode ? BlockX2bpp : BlockX4bpp; // for MBX don't allow the sizes to get too small int blockXDim = Math.Max(2, xDim / xBlockSize); int blockYDim = Math.Max(2, yDim / BlockYSize); uint pblocki00 = uint.MaxValue; uint pblocki01 = uint.MaxValue; uint pblocki10 = uint.MaxValue; uint pblocki11 = uint.MaxValue; Colours5554* m_colors = stackalloc Colours5554[2 * 2]; // interpolated A colors for the pixel int* aSig = stackalloc int[4]; // interpolated B colors for the pixel int* bSig = stackalloc int[4]; int* modulationVals = stackalloc int[8 * 16]; int* modulationModes = stackalloc int[8 * 16]; // step through the pixels of the image decompressing each one in turn. // Note that this is a hideously inefficient way to do this! for (int y = 0; y < yDim; y++) { for (int x = 0; x < xDim; x++) { // map this pixel to the top left neighbourhood of blocks int blockX = (x - xBlockSize / 2); int blockY = (y - BlockYSize / 2); blockX = LimitCoord(blockX, xDim) / xBlockSize; blockY = LimitCoord(blockY, yDim) / BlockYSize; // compute the positions of the other 3 blocks int blockXp1 = LimitCoord(blockX + 1, blockXDim); int blockYp1 = LimitCoord(blockY + 1, blockYDim); // map to block memory locations uint blocki00 = TwiddleUV((uint)blockYDim, (uint)blockXDim, (uint)blockY, (uint)blockX); uint blocki01 = TwiddleUV((uint)blockYDim, (uint)blockXDim, (uint)blockY, (uint)blockXp1); uint blocki10 = TwiddleUV((uint)blockYDim, (uint)blockXDim, (uint)blockYp1, (uint)blockX); uint blocki11 = TwiddleUV((uint)blockYDim, (uint)blockXDim, (uint)blockYp1, (uint)blockXp1); // extract the colours and the modulation information IF the previous values have changed. bool changed = blocki00 != pblocki00 || blocki01 != pblocki01 || blocki10 != pblocki10 || blocki11 != pblocki11; if (changed) { AmtcBlock* block00 = &((AmtcBlock*)compressedData)[blocki00]; Unpack5554Colour(block00, m_colors[0].Reps); UnpackModulations(block00, do2bitMode, modulationVals, modulationModes, 0, 0); AmtcBlock* block01 = &((AmtcBlock*)compressedData)[blocki01]; Unpack5554Colour(block01, m_colors[1].Reps); UnpackModulations(block01, do2bitMode, modulationVals, modulationModes, xBlockSize, 0); AmtcBlock* block10 = &((AmtcBlock*)compressedData)[blocki10]; Unpack5554Colour(block10, m_colors[2].Reps); UnpackModulations(block10, do2bitMode, modulationVals, modulationModes, 0, BlockYSize); AmtcBlock* block11 = &((AmtcBlock*)compressedData)[blocki11]; Unpack5554Colour(block11, m_colors[3].Reps); UnpackModulations(block11, do2bitMode, modulationVals, modulationModes, xBlockSize, BlockYSize); pblocki00 = blocki00; pblocki01 = blocki01; pblocki10 = blocki10; pblocki11 = blocki11; } // decompress the pixel. First compute the interpolated A and B signals InterpolateColours(m_colors[0].Reps, m_colors[1].Reps, m_colors[2].Reps, m_colors[3].Reps, 0, do2bitMode, x, y, aSig); InterpolateColours(m_colors[0].Reps, m_colors[1].Reps, m_colors[2].Reps, m_colors[3].Reps, 1, do2bitMode, x, y, bSig); GetModulationValue(x, y, do2bitMode, modulationVals, modulationModes, out int mod, out bool doPT); // compute the modulated color. Swap red and blue channel int position = (x + y * xDim) << 2; output[position + 0] = (byte)((aSig[2] * 8 + mod * (bSig[2] - aSig[2])) >> 3); output[position + 1] = (byte)((aSig[1] * 8 + mod * (bSig[1] - aSig[1])) >> 3); output[position + 2] = (byte)((aSig[0] * 8 + mod * (bSig[0] - aSig[0])) >> 3); output[position + 3] = doPT ? (byte)0 : (byte)((aSig[3] * 8 + mod * (bSig[3] - aSig[3])) >> 3); } } } /// /// Given the Block (or pixel) coordinates and the dimension of the texture in blocks (or pixels) this returns /// the twiddled offset of the block (or pixel) from the start of the map. /// NOTE the dimensions of the texture must be a power of 2 /// /// Y dimension of the texture in pixels /// X dimension of the texture in pixels /// Pixel Y position /// Pixel X position /// The twiddled offset of the pixel private static uint TwiddleUV(uint ySize, uint xSize, uint yPos, uint xPos) { #if DEBUG if (yPos >= ySize) { throw new Exception(); } if (xPos >= xSize) { throw new Exception(); } if (!IsPowerOf2((uint)ySize)) { throw new Exception(); } if (!IsPowerOf2((uint)xSize)) { throw new Exception(); } #endif uint minDimension; uint maxValue; if (ySize < xSize) { minDimension = ySize; maxValue = xPos; } else { minDimension = xSize; maxValue = yPos; } #if DISABLE_TWINDDLING_ROUTINE // nasty hack to disable twiddling return (yPos * xSize + xPos); #else // step through all the bits in the "minimum" dimension uint srcBitPos = 1; uint dstBitPos = 1; uint twiddled = 0; int shiftCount = 0; while (srcBitPos < minDimension) { if ((yPos & srcBitPos) != 0) { twiddled |= dstBitPos; } if ((xPos & srcBitPos) != 0) { twiddled |= (dstBitPos << 1); } srcBitPos <<= 1; dstBitPos <<= 2; shiftCount += 1; } // prepend any unused bits maxValue >>= shiftCount; twiddled |= (maxValue << (2 * shiftCount)); return twiddled; #endif } /// /// Get the modulation value as a numerator of a fraction of 8ths /// private unsafe static void GetModulationValue(int x, int y, bool do2bitMode, int* modulationVals, int* modulationModes, out int mod, out bool doPT) { // Map X and Y into the local 2x2 block y = (y & 0x3) | ((~y & 0x2) << 1); x = do2bitMode ? (x & 0x7) | ((~x & 0x4) << 1) : (x & 0x3) | ((~x & 0x2) << 1); // assume no PT for now doPT = false; // extract the modulation value. If a simple encoding int modVal; if (modulationModes[y * 16 + x] == 0) { modVal = m_repVals0[modulationVals[y * 16 + x]]; } else if (do2bitMode) { // if this is a stored value if (((x ^ y) & 1) == 0) { modVal = m_repVals0[modulationVals[y * 16 + x]]; } // else average from the neighbours if H&V interpolation.. else if (modulationModes[y * 16 + x] == 1) { modVal = (m_repVals0[modulationVals[(y - 1) * 16 + x]] + m_repVals0[modulationVals[(y + 1) * 16 + x]] + m_repVals0[modulationVals[y * 16 + x - 1]] + m_repVals0[modulationVals[y * 16 + x + 1]] + 2) / 4; } // else if H-Only else if (modulationModes[y * 16 + x] == 2) { modVal = (m_repVals0[modulationVals[y * 16 + x - 1]] + m_repVals0[modulationVals[y * 16 + x + 1]] + 1) / 2; } // else it's V-Only else { modVal = (m_repVals0[modulationVals[(y - 1) * 16 + x]] + m_repVals0[modulationVals[(y + 1) * 16 + x]] + 1) / 2; } } // else it's 4BPP and PT encoding else { modVal = m_repVals1[modulationVals[y * 16 + x]]; doPT = modulationVals[y * 16 + x] == PTIndex; } mod = modVal; } /// /// This performs a HW bit accurate interpolation of either the A or B colours for a particular pixel /// NOTE: It is assumed that the source colours are in ARGB 5554 format - This means that some "preparation" of the values will be necessary /// private unsafe static void InterpolateColours(int* colorP, int* colorQ, int* colorR, int* colorS, int ci, bool do2bitMode, int x, int y, int* result) { // put the x and y values into the right range and get the u and v scale amounts int v = ((y & 0x3) | ((~y & 0x2) << 1)) - BlockYSize / 2; int u = do2bitMode ? (((x & 0x7) | ((~x & 0x4) << 1)) - BlockX2bpp / 2) : (((x & 0x3) | ((~x & 0x2) << 1)) - BlockX4bpp / 2); int uscale = do2bitMode ? 8 : 4; for (int i = 0; i < 4; i++) { int tmp1 = colorP[ci * 4 + i] * uscale + u * (colorQ[ci * 4 + i] - colorP[ci * 4 + i]); int tmp2 = colorR[ci * 4 + i] * uscale + u * (colorS[ci * 4 + i] - colorR[ci * 4 + i]); result[i] = tmp1 * 4 + v * (tmp2 - tmp1); } // lop off the appropriate number of bits to get us to 8 bit precision if (do2bitMode) { // do RGB for (int i = 0; i < 3; i++) { result[i] >>= 2; } result[3] >>= 1; } else { // do RGB (A is ok) for (int i = 0; i < 3; i++) { result[i] >>= 1; } } #if DEBUG // sanity check for (int i = 0; i < 4; i++) { if (result[i] >= 256) { throw new Exception("Sanity failed"); } } #endif // convert from 5554 to 8888, do RGB 5.3 => 8 for (int i = 0; i < 3; i++) { result[i] += result[i] >> 5; } result[3] += result[3] >> 4; #if DEBUG // 2nd sanity check for (int i = 0; i < 4; i++) { if (result[i] >= 256) { throw new Exception("2nd sanity failed"); } } #endif } /// /// Given the block and the texture type and it's relative position in the 2x2 group of blocks, extract the bit patterns for the fully defined pixels. /// private unsafe static void UnpackModulations(AmtcBlock* block, bool do2bitMode, int* modulationVals, int* modulationModes, int startX, int startY) { int blockModMode = (int)(block->PackedData1 & 1); uint modulationBits = block->PackedData0; // if it's in an interpolated mode if (do2bitMode && (blockModMode != 0)) { // run through all the pixels in the block. Note we can now treat all the "stored" values as if they have 2bits (even when they didn't!) for (int y = 0; y < BlockYSize; y++) { for (int x = 0; x < BlockX2bpp; x++) { modulationModes[(y + startY) * 16 + x + startX] = blockModMode; // if this is a stored value... if (((x ^ y) & 1) == 0) { modulationVals[(y + startY) * 16 + x + startX] = (int)(modulationBits & 3); modulationBits >>= 2; } } } } // else if direct encoded 2bit mode - i.e. 1 mode bit per pixel else if (do2bitMode) { for (int y = 0; y < BlockYSize; y++) { for (int x = 0; x < BlockX2bpp; x++) { modulationModes[(y + startY) * 16 + x + startX] = blockModMode; // double the bits so 0=> 00, and 1=>11 if ((modulationBits & 1) != 0) { modulationVals[(y + startY) * 16 + x + startX] = 0x3; } else { modulationVals[(y + startY) * 16 + x + startX] = 0x0; } modulationBits >>= 1; } } } // else its the 4bpp mode so each value has 2 bits else { for (int y = 0; y < BlockYSize; y++) { for (int x = 0; x < BlockX4bpp; x++) { modulationModes[(y + startY) * 16 + x + startX] = blockModMode; modulationVals[(y + startY) * 16 + x + startX] = (int)(modulationBits & 3); modulationBits >>= 2; } } } // make sure nothing is left over if (modulationBits != 0) { throw new Exception("Something is left over"); } } /// /// Given a block, extract the colour information and convert to 5554 formats /// private unsafe static void Unpack5554Colour(AmtcBlock* block, int* abColors) { uint* rawBits = stackalloc uint[2]; // extract A and B // 15 bits (shifted up by one) rawBits[0] = block->PackedData1 & (0xFFFE); // 16 bits rawBits[1] = block->PackedData1 >> 16; // step through both colours for (int i = 0; i < 2; i++) { // if completely opaque if ((rawBits[i] & (1 << 15)) != 0) { // extract R and G (both 5 bit) abColors[i * 4 + 0] = (int)((rawBits[i] >> 10) & 0x1F); abColors[i * 4 + 1] = (int)((rawBits[i] >> 5) & 0x1F); // The precision of Blue depends on A or B. If A then we need to replicate the top bit to get 5 bits in total abColors[i * 4 + 2] = (int)(rawBits[i] & 0x1F); if (i == 0) { abColors[0 * 4 + 2] |= abColors[0 * 4 + 2] >> 4; } // set 4bit alpha fully on... abColors[i * 4 + 3] = 0xF; } // else if colour has variable translucency else { // extract R and G (both 4 bit). Leave a space on the end for the replication of bits abColors[i * 4 + 0] = (int)((rawBits[i] >> (8 - 1)) & 0x1E); abColors[i * 4 + 1] = (int)((rawBits[i] >> (4 - 1)) & 0x1E); // replicate bits to truly expand to 5 bits abColors[i * 4 + 0] |= abColors[i * 4 + 0] >> 4; abColors[i * 4 + 1] |= abColors[i * 4 + 1] >> 4; // grab the 3(+padding) or 4 bits of blue and add an extra padding bit abColors[i * 4 + 2] = (int)((rawBits[i] & 0xF) << 1); // expand from 3 to 5 bits if this is from colour A, or 4 to 5 bits if from colour B if (i == 0) { abColors[0 * 4 + 2] |= abColors[0 * 4 + 2] >> 3; } else { abColors[0 * 4 + 2] |= abColors[0 * 4 + 2] >> 4; } // set the alpha bits to be 3 + a zero on the end abColors[i * 4 + 3] = (int)((rawBits[i] >> 11) & 0xE); } } } /// /// Check that a number is an integer power of two, i.e. 1, 2, 4, 8, ... etc. Returns false for zero /// /// A number /// True if the number is an integer power of two, else false [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsPowerOf2(uint input) { if (input == 0) { return false; } uint minus1 = input - 1; return ((input | minus1) == (input ^ minus1)); } /// /// Define an expression to either wrap or clamp large or small vals to the legal coordinate range /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int LimitCoord(int value, int size) { #if ASSUME_IMAGE_TILING // wrap coord return value & (size - 1); #else // clamp return h < x ? h : (x > l ? x : l); return Clamp(Val, 0, Size - 1); #endif } /// /// The Punch-through index /// private const int PTIndex = 2; /// /// Always 4 for all 2D block types /// private const int BlockYSize = 4; /// /// Max X dimension for blocks /// private const int BlockXMax = 8; // Dimensions for the two formats private const int BlockX2bpp = 8; private const int BlockX4bpp = 4; private static readonly int[] m_repVals0 = new int[] { 0, 3, 5, 8 }; private static readonly int[] m_repVals1 = new int[] { 0, 4, 4, 8 }; } }