From d1a75b78dddb5f1afcc67926e6aca1828beccc5c Mon Sep 17 00:00:00 2001 From: cabalex <31020729+cabalex@users.noreply.github.com> Date: Mon, 19 Aug 2024 23:18:06 -0700 Subject: [PATCH] Fix NieR Switch texture issues - Texture issues were due to misunderstanding of .tex headers; should be fixed now? - Added support for special padding (Astral Chain, Bayo 3) --- wta_wtp/exporter/export_wta_wtp.py | 74 +++++++++++++++++---------- wta_wtp/importer/wtpImportOperator.py | 49 ++++++++++++++---- wta_wtp/tegrax1swizzle.py | 28 +++++----- 3 files changed, 99 insertions(+), 52 deletions(-) diff --git a/wta_wtp/exporter/export_wta_wtp.py b/wta_wtp/exporter/export_wta_wtp.py index 5e79956..c293b9e 100644 --- a/wta_wtp/exporter/export_wta_wtp.py +++ b/wta_wtp/exporter/export_wta_wtp.py @@ -2,7 +2,7 @@ import subprocess import asyncio import math import json -from ...utils.ioUtils import read_int32, write_Int32, write_uInt16, write_float16 +from ...utils.ioUtils import read_int32, write_Int32, write_uInt32, write_byte from . import generate_wta_wtp_data from .wta_wtp_utils import * from ..tegrax1swizzle import compressImageData, getFormatByIndex @@ -205,17 +205,13 @@ def main(context, export_filepath_wta, export_filepath_wtp, exportingForGame): info = { "magic": b".tex", "format": 0x7D, - "unk1": 1, "width": 0, "height": 0, "depth": 1, "mipCount": 1, - "unk2": 256, - "unk3": 0.25, - "unk4": 0, # guesses used for swizzling (wtpImportOperator.py) "type": 1, - "textureLayout": [4, 0], + "blockHeightLog2": 4, "arrayCount": 1 } @@ -249,8 +245,13 @@ def main(context, export_filepath_wta, export_filepath_wtp, exportingForGame): unknownArray1[i] = 1677721632 # DDS textures are always SRGB wtaTextureOffset[i] = wtp_fp.tell() + # Change infos + if info["height"] < 256: + info["blockHeightLog2"] = 8 & 7 + if info["height"] < 128: + info["blockHeightLog2"] = 16 & 7 + dds_fp.seek(0x80) - blockHeightLog2 = info["textureLayout"][0] & 7 wtp_fp.write(compressImageData( getFormatByIndex(info['format']), info['width'], @@ -259,7 +260,7 @@ def main(context, export_filepath_wta, export_filepath_wtp, exportingForGame): info['arrayCount'], info['mipCount'], dds_fp.read(), - blockHeightLog2 + info['blockHeightLog2'] )) wtaTextureSize[i] = wtp_fp.tell() - wtaTextureOffset[i] if wtaTextureSize[i] < 90112: @@ -277,7 +278,6 @@ def main(context, export_filepath_wta, export_filepath_wtp, exportingForGame): info["height"] = int.from_bytes(astc_fp.read(3), "little") astc_fp.seek(16) - blockHeightLog2 = info["textureLayout"][0] & 7 wtp_fp.write(compressImageData( getFormatByIndex(info['format']), info['width'], @@ -286,7 +286,7 @@ def main(context, export_filepath_wta, export_filepath_wtp, exportingForGame): info['arrayCount'], info['mipCount'], astc_fp.read(), - blockHeightLog2 + info['blockHeightLog2'] )) wtaTextureSize[i] = wtp_fp.tell() - wtaTextureOffset[i] if wtaTextureSize[i] < 90112: @@ -331,17 +331,17 @@ def main(context, export_filepath_wta, export_filepath_wtp, exportingForGame): wta_fp.write(to_bytes(wtaTextureIdentifier[i])) for i in range(textureCount): + print("Texture", getFormatByIndex(textureInfoArray[i]["format"]), f"{textureInfoArray[i]['width']}x{textureInfoArray[i]['height']}", textureInfoArray[i]["blockHeightLog2"]) wta_fp.seek(textureInfoArrayOffset + i * 0x100) wta_fp.write(textureInfoArray[i]["magic"]) - write_Int32(wta_fp, textureInfoArray[i]["format"]) - write_Int32(wta_fp, 1) - write_Int32(wta_fp, textureInfoArray[i]["width"]) - write_Int32(wta_fp, textureInfoArray[i]["height"]) - write_Int32(wta_fp, textureInfoArray[i]["depth"]) - write_Int32(wta_fp, textureInfoArray[i]["mipCount"]) - write_Int32(wta_fp, textureInfoArray[i]["unk2"]) - write_float16(wta_fp, textureInfoArray[i]["unk3"]) - write_uInt16(wta_fp, textureInfoArray[i]["unk4"]) + write_uInt32(wta_fp, textureInfoArray[i]["format"]) + write_uInt32(wta_fp, 1) # Texture type (1) + write_uInt32(wta_fp, textureInfoArray[i]["width"]) + write_uInt32(wta_fp, textureInfoArray[i]["height"]) + write_uInt32(wta_fp, textureInfoArray[i]["depth"]) + write_uInt32(wta_fp, textureInfoArray[i]["mipCount"]) + write_uInt32(wta_fp, 256) # header size? (0x100 = 256) + write_uInt32(wta_fp, wtaTextureSize[i]) # Texture size? (0x3400 = 13312) while wta_fp.tell() % 16 != 0: wta_fp.write(b'\x00') @@ -393,15 +393,24 @@ def main(context, export_filepath_wta, export_filepath_wtp, exportingForGame): "width": 0, "height": 0, "depth": 1, - "unk4": 32, # Always 32 (?) - "textureLayout": [1027, 65543], + "specialPad": 32, # Always 32 (?) - special padding + "blockHeightLog2": 3, + "flags": 4, + "unk2": 0, + "unk3": 0, + "unk4": 65543, "arrayCount": 1 } if identifiers_array[i].upper() in metadata.keys(): info["type"] = metadata[identifiers_array[i].upper()]["type"] info["format"] = metadata[identifiers_array[i].upper()]["format"] - info["textureLayout"] = metadata[identifiers_array[i].upper()]["textureLayout"] + info["specialPad"] = metadata[identifiers_array[i].upper()]["specialPad"] + info["blockHeightLog2"] = metadata[identifiers_array[i].upper()]["blockHeightLog2"] + info["flags"] = metadata[identifiers_array[i].upper()]["flags"] + info["unk2"] = metadata[identifiers_array[i].upper()]["unk2"] + info["unk3"] = metadata[identifiers_array[i].upper()]["unk3"] + info["unk4"] = metadata[identifiers_array[i].upper()]["unk4"] if ".dds" in path.lower(): dds_fp = open(path, 'rb') @@ -436,7 +445,9 @@ def main(context, export_filepath_wta, export_filepath_wtp, exportingForGame): wtaTextureOffset[i] = wtp_fp.tell() dds_fp.seek(80) - blockHeightLog2 = info["textureLayout"][0] & 7 + specialPad = 0 + if info["flags"] & 0x4: + specialPad = 32 wtp_fp.write(compressImageData( getFormatByIndex(info['format']), info['width'], @@ -445,7 +456,8 @@ def main(context, export_filepath_wta, export_filepath_wtp, exportingForGame): info['arrayCount'], info['mipCount'], dds_fp.read(), - blockHeightLog2 + info['blockHeightLog2'], + specialPad=specialPad )) wtaTextureSize[i] = wtp_fp.tell() - wtaTextureOffset[i] if wtaTextureSize[i] < 90112: @@ -463,7 +475,9 @@ def main(context, export_filepath_wta, export_filepath_wtp, exportingForGame): info["height"] = int.from_bytes(astc_fp.read(3), "little") astc_fp.seek(16) - blockHeightLog2 = info["textureLayout"][0] & 7 + specialPad = 0 + if info["flags"] & 0x4: + specialPad = 32 wtp_fp.write(compressImageData( getFormatByIndex(info['format']), info['width'], @@ -472,7 +486,8 @@ def main(context, export_filepath_wta, export_filepath_wtp, exportingForGame): info['arrayCount'], info['mipCount'], astc_fp.read(), - blockHeightLog2 + info['blockHeightLog2'], + specialPad=specialPad )) wtaTextureSize[i] = wtp_fp.tell() - wtaTextureOffset[i] if wtaTextureSize[i] < 90112: @@ -528,9 +543,12 @@ def main(context, export_filepath_wta, export_filepath_wtp, exportingForGame): write_Int32(wta_fp, textureInfoArray[i]["width"]) write_Int32(wta_fp, textureInfoArray[i]["height"]) write_Int32(wta_fp, textureInfoArray[i]["depth"]) + write_Int32(wta_fp, textureInfoArray[i]["specialPad"]) + write_byte(wta_fp, textureInfoArray[i]["blockHeightLog2"]) + write_byte(wta_fp, textureInfoArray[i]["flags"]) + write_byte(wta_fp, textureInfoArray[i]["unk2"]) + write_byte(wta_fp, textureInfoArray[i]["unk3"]) write_Int32(wta_fp, textureInfoArray[i]["unk4"]) - write_Int32(wta_fp, textureInfoArray[i]["textureLayout"][0]) - write_Int32(wta_fp, textureInfoArray[i]["textureLayout"][1]) while wta_fp.tell() % 16 != 0: wta_fp.write(b'\x00') diff --git a/wta_wtp/importer/wtpImportOperator.py b/wta_wtp/importer/wtpImportOperator.py index 6b1d272..01b374c 100644 --- a/wta_wtp/importer/wtpImportOperator.py +++ b/wta_wtp/importer/wtpImportOperator.py @@ -51,7 +51,8 @@ class WTAData: self.infos = [] infoFormat = f.read(4) - if infoFormat == b'XT1\x00': # Astral Chain, Bayonetta 3 WTA format + if infoFormat == b'XT1\x00': + # Astral Chain, Bayonetta 3 WTA format self.type = "XT1" f.seek(f.tell() - 4) for i in range(self.num_files): @@ -66,14 +67,20 @@ class WTAData: "width": io.read_uint32(f), "height": io.read_uint32(f), "depth": io.read_uint32(f), + "specialPad": io.read_uint32(f), + "blockHeightLog2": io.read_uint8(f), + "flags": io.read_uint8(f), # 0x4: use specialPad + "unk2": io.read_uint8(f), + "unk3": io.read_uint8(f), "unk4": io.read_uint32(f), - "textureLayout": [io.read_uint32(f), io.read_uint32(f)], "arrayCount": 1 } if info["type"] == 3 or info["type"] == 8: # T_Cube or T_Cube_Array info["arrayCount"] = 6 self.infos.append(info) - elif infoFormat == b'.tex': # NieR Switch WTA format + + elif infoFormat == b'.tex': + # NieR Switch WTA format self.type = "TEX" for i in range(self.num_files): f.seek(self.offsetTextureInfo + i * 0x100) @@ -85,13 +92,18 @@ class WTAData: "height": io.read_uint32(f), "depth": io.read_uint32(f), "mipCount": io.read_uint32(f), - "unk2": io.read_uint32(f), - "unk3": io.read_float16(f), - "unk4": io.read_uint16(f), + "headerSize": io.read_uint32(f), + "textureSize": io.read_uint64(f), # identical to the one in the headers "type": 1, - "textureLayout": [4, 0], + "blockHeightLog2": 4, "arrayCount": 1 } + + if info["width"] < 256: + info["blockHeightLog2"] = 8 & 7 + if info["width"] < 128: + info["blockHeightLog2"] = 16 & 7 + self.infos.append(info) else: self.type = "PC" @@ -118,7 +130,11 @@ class WTAData: os.makedirs(extractionDir, exist_ok=True) # Unswizzle textureFormat = getFormatByIndex(self.infos[i]["format"]) - blockHeightLog2 = self.infos[i]["textureLayout"][0] & 7 + + specialPad = 1 + if "specialPad" in self.infos[i].keys() and "flags" in self.infos[i].keys() and self.infos[i]["flags"] & 0x4: + specialPad = self.infos[i]["specialPad"] + texture = loadImageData( textureFormat, self.infos[i]['width'], @@ -127,7 +143,8 @@ class WTAData: self.infos[i]['arrayCount'], self.infos[i]['mipCount'], self.data[self.offsets[i]:self.offsets[i]+self.sizes[i]], - blockHeightLog2 + self.infos[i]["blockHeightLog2"], + specialPad=specialPad ) # Construct headers @@ -156,14 +173,24 @@ class WTAData: loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.gather(*tasks)) - # Write metadata file (need to store format and textureLayout for later) + # Write metadata file (need to store format and other data for later) metadata = {} for i in range(self.num_files): metadata[f"{self.idx[i]:0>8X}"] = { "type": self.infos[i]["type"], "format": self.infos[i]["format"], - "textureLayout": self.infos[i]["textureLayout"] + "blockHeightLog2": self.infos[i]["blockHeightLog2"], } + + if "specialPad" in self.infos[i].keys(): + # add AC/B3 specialPad + metadata[f"{self.idx[i]:0>8X}"]["specialPad"] = self.infos[i]["specialPad"] + metadata[f"{self.idx[i]:0>8X}"]["blockHeightLog2"] = self.infos[i]["blockHeightLog2"] + metadata[f"{self.idx[i]:0>8X}"]["flags"] = self.infos[i]["flags"] + metadata[f"{self.idx[i]:0>8X}"]["unk2"] = self.infos[i]["unk2"] + metadata[f"{self.idx[i]:0>8X}"]["unk3"] = self.infos[i]["unk3"] + metadata[f"{self.idx[i]:0>8X}"]["unk4"] = self.infos[i]["unk4"] + with open(os.path.join(extractionDir, "xt1_info.json"), "w") as f: f.write(json.dumps(metadata, indent=4)) diff --git a/wta_wtp/tegrax1swizzle.py b/wta_wtp/tegrax1swizzle.py index ae5b494..a63cbfd 100644 --- a/wta_wtp/tegrax1swizzle.py +++ b/wta_wtp/tegrax1swizzle.py @@ -1,9 +1,11 @@ -# TegraX1Swizzle.py - cabalex [Updated Dec 2022] +# TegraX1Swizzle.py - cabalex [Updated Aug 2024] # Based on: # KillzXGaming's Switch Toolbox texture decoding - https://github.com/KillzXGaming/Switch-Toolbox/blob/604f7b3d369bc97d9d05632da3211ed11b990ba7/Switch_Toolbox_Library/Texture%20Decoding/Switch/TegraX1Swizzle.cs # aboood40091's BNTX-Extractor - https://github.com/aboood40091/BNTX-Extractor/blob/master/swizzle.py # [Format table] Ryujinx's image table - https://github.com/Ryujinx/Ryujinx/blob/c86aacde76b5f8e503e2b412385c8491ecc86b3b/Ryujinx.Graphics/Graphics3d/Texture/ImageUtils.cs +# Aug 2024: Added special padding to GOB offsets. + formatTable = { "R8G8B8A8_UNORM": [4, 1, 1, 1], "BC1_UNORM": [8, 4, 4, 1], @@ -76,7 +78,7 @@ def round_up(x, y): return ((x - 1) | (y - 1)) + 1 -def _swizzle(width, height, depth, blkWidth, blkHeight, blkDepth, roundPitch, bpp, tileMode, blockHeightLog2, data, toSwizzle): +def _swizzle(width, height, depth, blkWidth, blkHeight, blkDepth, roundPitch, bpp, tileMode, blockHeightLog2, specialPad, data, toSwizzle): block_height = 1 << blockHeightLog2 width = DIV_ROUND_UP(width, blkWidth) @@ -102,7 +104,7 @@ def _swizzle(width, height, depth, blkWidth, blkHeight, blkDepth, roundPitch, bp pos = y * pitch + x * bpp else: - pos = getAddrBlockLinear(x, y, width, bpp, 0, block_height) + pos = getAddrBlockLinear(x, y, width, bpp, 0, block_height, specialPad) pos_ = (y * width + x) * bpp @@ -116,20 +118,20 @@ def _swizzle(width, height, depth, blkWidth, blkHeight, blkDepth, roundPitch, bp return result[:size] #def deswizzle(width, height, blkWidth, blkHeight, bpp, tileMode, alignment, size_range, data): -def deswizzle(width, height, depth, blkWidth, blkHeight, blkDepth, roundPitch, bpp, tileMode, size_range, data): - return _swizzle(width, height, depth, blkWidth, blkHeight, blkDepth, roundPitch, bpp, tileMode, size_range, bytes(data), 0) +def deswizzle(width, height, depth, blkWidth, blkHeight, blkDepth, roundPitch, bpp, tileMode, size_range, specialPad, data): + return _swizzle(width, height, depth, blkWidth, blkHeight, blkDepth, roundPitch, bpp, tileMode, size_range, specialPad, bytes(data), 0) #return _swizzle(width, height, blkWidth, blkHeight, bpp, tileMode, alignment, size_range, bytes(data), 0) -def swizzle(width, height, depth, blkWidth, blkHeight, blkDepth, roundPitch, bpp, tileMode, size_range, data): - return _swizzle(width, height, depth, blkWidth, blkHeight, blkDepth, roundPitch, bpp, tileMode, size_range, bytes(data), 1) +def swizzle(width, height, depth, blkWidth, blkHeight, blkDepth, roundPitch, bpp, tileMode, size_range, specialPad, data): + return _swizzle(width, height, depth, blkWidth, blkHeight, blkDepth, roundPitch, bpp, tileMode, size_range, specialPad, bytes(data), 1) -def getAddrBlockLinear(x, y, image_width, bytes_per_pixel, base_address, block_height): +def getAddrBlockLinear(x, y, image_width, bytes_per_pixel, base_address, block_height, specialPad): """ From the Tegra X1 TRM """ - image_width_in_gobs = DIV_ROUND_UP(image_width * bytes_per_pixel, 64) + image_width_in_gobs = DIV_ROUND_UP(round_up(image_width, specialPad) * bytes_per_pixel, 64) GOB_address = (base_address + (y // (8 * block_height)) * 512 * block_height * image_width_in_gobs @@ -143,7 +145,7 @@ def getAddrBlockLinear(x, y, image_width, bytes_per_pixel, base_address, block_h return Address -def loadImageData(format: str, width: int, height: int, depth: int, arrayCount: int, mipCount: int, imageData, blockHeightLog2, target=1, linearTileMode=False): +def loadImageData(format: str, width: int, height: int, depth: int, arrayCount: int, mipCount: int, imageData, blockHeightLog2, target=1, linearTileMode=False, specialPad=1): [bpp, blkWidth, blkHeight, blkDepth] = getFormatTable(format) blockHeight = DIV_ROUND_UP(height, blkHeight) pitch = 0 @@ -185,7 +187,7 @@ def loadImageData(format: str, width: int, height: int, depth: int, arrayCount: try: pitch = round_up(width__ * bpp, 64) surfaceSize += pitch * round_up(height__, max(1, blockHeight >> blockHeightShift) * 8) - result = deswizzle(width, height, depth, blkWidth, blkHeight, blkDepth, target, bpp, tileMode, max(0, blockHeightLog2 - blockHeightShift), data_) + result = deswizzle(width, height, depth, blkWidth, blkHeight, blkDepth, target, bpp, tileMode, max(0, blockHeightLog2 - blockHeightShift), specialPad, data_) # the program creates a copy and uses that to remove unneeded data # yeah, i'm not doing that @@ -197,7 +199,7 @@ def loadImageData(format: str, width: int, height: int, depth: int, arrayCount: arrayOffset += len(imageData) / arrayCount return False -def compressImageData(format: str, width: int, height: int, depth: int, arrayCount: int, mipCount: int, imageData, blockHeightLog2, target=1, linearTileMode=False): +def compressImageData(format: str, width: int, height: int, depth: int, arrayCount: int, mipCount: int, imageData, blockHeightLog2, target=1, linearTileMode=False, specialPad=1): bpp = formatTable[format][0] blkWidth = formatTable[format][1] blkHeight = formatTable[format][2] @@ -242,7 +244,7 @@ def compressImageData(format: str, width: int, height: int, depth: int, arrayCou try: pitch = round_up(width__ * bpp, 64) surfaceSize += pitch * round_up(height__, max(1, blockHeight >> blockHeightShift) * 8) - result = swizzle(width, height, depth, blkWidth, blkHeight, blkDepth, target, bpp, tileMode, max(0, blockHeightLog2 - blockHeightShift), data_) + result = swizzle(width, height, depth, blkWidth, blkHeight, blkDepth, target, bpp, tileMode, max(0, blockHeightLog2 - blockHeightShift), specialPad, data_) # the program creates a copy and uses that to remove unneeded data # yeah, i'm not doing that