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)
This commit is contained in:
cabalex
2024-08-19 23:18:06 -07:00
parent ebc7fbf1c9
commit d1a75b78dd
3 changed files with 99 additions and 52 deletions
+46 -28
View File
@@ -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')
+38 -11
View File
@@ -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))
+15 -13
View File
@@ -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