mirror of https://github.com/zeldaret/oot.git
1623 lines
53 KiB
Python
1623 lines
53 KiB
Python
# SPDX-FileCopyrightText: © 2025 ZeldaRET
|
|
# SPDX-License-Identifier: CC0-1.0
|
|
|
|
import enum
|
|
import io
|
|
from pathlib import Path
|
|
import reprlib
|
|
|
|
from typing import TYPE_CHECKING, Union, Optional, Callable
|
|
|
|
try:
|
|
from rich.pretty import pprint as rich_pprint
|
|
except ImportError:
|
|
rich_pprint = print
|
|
|
|
import pygfxd
|
|
|
|
if TYPE_CHECKING:
|
|
from ..extase.memorymap import MemoryContext
|
|
from ..extase.memorymap import UnmappedAddressError, UnexpectedResourceTypeError
|
|
|
|
from ..extase import (
|
|
RESOURCE_PARSE_SUCCESS,
|
|
ResourceParseWaiting,
|
|
Resource,
|
|
File,
|
|
)
|
|
from ..extase.cdata_resources import (
|
|
CDataResource,
|
|
CDataExt_Array,
|
|
CDataExt_Struct,
|
|
CDataExt_Value,
|
|
CDataExtWriteContext,
|
|
INDENT,
|
|
fmt_hex_s,
|
|
fmt_hex_u,
|
|
)
|
|
|
|
|
|
BEST_EFFORT = True
|
|
|
|
VERBOSE_ColorIndexedTexturesManager = False
|
|
VERBOSE_BEST_EFFORT_TLUT_NO_REAL_USER = True
|
|
|
|
EXPLICIT_DL_AND_TEX_SIZES = True
|
|
TEXS_SHORTER_NAMES = True
|
|
|
|
|
|
class MtxResource(CDataResource):
|
|
braces_in_source = False
|
|
|
|
def write_mtx(resource, memory_context, v, wctx: CDataExtWriteContext):
|
|
assert isinstance(v, dict)
|
|
assert v.keys() == {"intPart", "fracPart"}
|
|
intPart = v["intPart"]
|
|
fracPart = v["fracPart"]
|
|
|
|
f = wctx.f
|
|
f.write(wctx.line_prefix)
|
|
f.write("gdSPDefMtx(\n")
|
|
|
|
for i in range(4):
|
|
if i != 0:
|
|
f.write(",\n")
|
|
f.write(wctx.line_prefix + INDENT)
|
|
|
|
for j in range(4):
|
|
# #define IPART(x) (((s32)((x) * 0x10000) >> 16) & 0xFFFF)
|
|
xi = intPart[j][i]
|
|
# #define FPART(x) ((s32)((x) * 0x10000) & 0xFFFF)
|
|
xf = fracPart[j][i]
|
|
# Reconstruct the `(s32)((x) * 0x10000)` but as a u32
|
|
# (u32 since intPart and fracPart are u16 arrays)
|
|
# This works since `(s32)((x) * 0x10000)` in the IPART and FPART
|
|
# macros could be switched to `(u32)(s32)((x) * 0x10000)` without issue
|
|
u32_x_s15_16 = (xi << 16) | xf
|
|
# Cast to s32 (`(s32)(u32)(s32)((x) * 0x10000)` == `(s32)((x) * 0x10000)`)
|
|
s32_x_s15_16 = (
|
|
u32_x_s15_16
|
|
if u32_x_s15_16 < 0x8000_0000
|
|
else u32_x_s15_16 - 0x1_0000_0000
|
|
)
|
|
x = s32_x_s15_16 / 0x10000
|
|
|
|
if j != 0:
|
|
f.write(", ")
|
|
f.write(f"{x}f")
|
|
f.write("\n")
|
|
|
|
f.write(wctx.line_prefix)
|
|
f.write(")")
|
|
|
|
return True
|
|
|
|
cdata_ext = CDataExt_Struct(
|
|
(
|
|
("intPart", CDataExt_Array(CDataExt_Array(CDataExt_Value.u16, 4), 4)),
|
|
("fracPart", CDataExt_Array(CDataExt_Array(CDataExt_Value.u16, 4), 4)),
|
|
)
|
|
).set_write(write_mtx)
|
|
|
|
def get_c_declaration_base(self):
|
|
return f"Mtx {self.symbol_name}"
|
|
|
|
def get_c_reference(self, resource_offset: int):
|
|
if resource_offset == 0:
|
|
return f"&{self.symbol_name}"
|
|
else:
|
|
raise ValueError
|
|
|
|
def get_h_includes(self):
|
|
return ("ultra64.h",)
|
|
|
|
|
|
class VtxArrayResource(CDataResource):
|
|
def write_elem(resource, memory_context, v, wctx: CDataExtWriteContext):
|
|
assert isinstance(v, dict)
|
|
wctx.f.write(wctx.line_prefix)
|
|
wctx.f.write(
|
|
f"VTX({v['x']:6}, {v['y']:6}, {v['z']:6}, "
|
|
f"{fmt_hex_s(v['s']):>7}, {fmt_hex_s(v['t']):>7}, "
|
|
f"{fmt_hex_u(v['crnx'], 2)}, {fmt_hex_u(v['cgny'], 2)}, {fmt_hex_u(v['cbnz'], 2)}, {fmt_hex_u(v['a'], 2)})"
|
|
)
|
|
return True
|
|
|
|
element_cdata_ext = CDataExt_Struct(
|
|
(
|
|
("x", CDataExt_Value.s16),
|
|
("y", CDataExt_Value.s16),
|
|
("z", CDataExt_Value.s16),
|
|
(
|
|
"pad6",
|
|
CDataExt_Value.pad16,
|
|
), # Not technically padding but unused and expected to always be 0
|
|
("s", CDataExt_Value.s16),
|
|
("t", CDataExt_Value.s16),
|
|
("crnx", CDataExt_Value.u8),
|
|
("cgny", CDataExt_Value.u8),
|
|
("cbnz", CDataExt_Value.u8),
|
|
("a", CDataExt_Value.u8),
|
|
)
|
|
).set_write(write_elem)
|
|
|
|
def __init__(self, file: File, range_start: int, range_end: int, name: str):
|
|
num = (range_end - range_start) // self.element_cdata_ext.size
|
|
self.cdata_ext = CDataExt_Array(self.element_cdata_ext, num)
|
|
super().__init__(file, range_start, name)
|
|
|
|
def get_as_xml(self):
|
|
return f"""\
|
|
<Array Name="{self.symbol_name}" Count="{(self.range_end - self.range_start) // self.element_cdata_ext.size}" Offset="0x{self.range_start:X}">
|
|
<Vtx/>
|
|
</Array>"""
|
|
|
|
def get_c_declaration_base(self):
|
|
if hasattr(self, "HACK_IS_STATIC_ON"):
|
|
return f"Vtx {self.symbol_name}[{self.cdata_ext.length}]"
|
|
return f"Vtx {self.symbol_name}[]"
|
|
|
|
def get_c_reference(self, resource_offset: int):
|
|
if resource_offset % self.element_cdata_ext.size != 0:
|
|
raise ValueError(
|
|
"unaligned offset into vtx array",
|
|
hex(resource_offset),
|
|
self.element_cdata_ext.size,
|
|
)
|
|
index = resource_offset // self.element_cdata_ext.size
|
|
return f"&{self.symbol_name}[{index}]"
|
|
|
|
def get_c_includes(self):
|
|
return ("gfx.h",)
|
|
|
|
def get_h_includes(self):
|
|
return ("ultra64.h",)
|
|
|
|
|
|
from ...n64 import G_IM_FMT, G_IM_SIZ, G_TT, G_MDSFT_TEXTLUT
|
|
from ... import n64texconv
|
|
|
|
G_IM_FMT_n64texconv_by_n64 = {
|
|
G_IM_FMT.RGBA: n64texconv.G_IM_FMT_RGBA,
|
|
G_IM_FMT.YUV: n64texconv.G_IM_FMT_YUV,
|
|
G_IM_FMT.CI: n64texconv.G_IM_FMT_CI,
|
|
G_IM_FMT.IA: n64texconv.G_IM_FMT_IA,
|
|
G_IM_FMT.I: n64texconv.G_IM_FMT_I,
|
|
}
|
|
G_IM_SIZ_n64texconv_by_n64 = {
|
|
G_IM_SIZ._4b: n64texconv.G_IM_SIZ_4b,
|
|
G_IM_SIZ._8b: n64texconv.G_IM_SIZ_8b,
|
|
G_IM_SIZ._16b: n64texconv.G_IM_SIZ_16b,
|
|
G_IM_SIZ._32b: n64texconv.G_IM_SIZ_32b,
|
|
}
|
|
|
|
|
|
def write_n64_image_to_png(
|
|
path: Path, width: int, height: int, fmt: G_IM_FMT, siz: G_IM_SIZ, data: memoryview
|
|
):
|
|
n64texconv.N64Image.from_bin(
|
|
data,
|
|
width,
|
|
height,
|
|
G_IM_FMT_n64texconv_by_n64[fmt],
|
|
G_IM_SIZ_n64texconv_by_n64[siz],
|
|
).to_png(str(path), False)
|
|
|
|
|
|
def write_n64_image_to_png_color_indexed(
|
|
path: Path,
|
|
width: int,
|
|
height: int,
|
|
fmt: G_IM_FMT,
|
|
siz: G_IM_SIZ,
|
|
data: memoryview,
|
|
tlut_data: memoryview,
|
|
tlut_count: int,
|
|
tlut_fmt: G_IM_FMT,
|
|
):
|
|
assert tlut_count * 2 == len(tlut_data)
|
|
n64texconv.N64Image.from_bin(
|
|
data,
|
|
width,
|
|
height,
|
|
G_IM_FMT_n64texconv_by_n64[fmt],
|
|
G_IM_SIZ_n64texconv_by_n64[siz],
|
|
n64texconv.N64Palette.from_bin(tlut_data, G_IM_FMT_n64texconv_by_n64[tlut_fmt]),
|
|
).to_png(str(path), False)
|
|
|
|
|
|
class TextureResource(Resource):
|
|
needs_build = True
|
|
extracted_path_suffix = ".png"
|
|
|
|
def __init__(
|
|
self,
|
|
file: File,
|
|
range_start: int,
|
|
name: str,
|
|
fmt: G_IM_FMT,
|
|
siz: G_IM_SIZ,
|
|
width: int,
|
|
height: int,
|
|
):
|
|
size_bits = siz.bpp * width * height
|
|
assert size_bits % 8 == 0, size_bits
|
|
size_bytes = size_bits // 8
|
|
range_end = range_start + size_bytes
|
|
|
|
super().__init__(file, range_start, range_end, name)
|
|
|
|
self.fmt = fmt
|
|
self.siz = siz
|
|
self.width = width
|
|
self.height = height
|
|
|
|
# For handling color-indexed textures:
|
|
self.resource_tlut: Optional[TextureResource] = None
|
|
"""For CI textures, the TLUT used"""
|
|
self.resources_ci_list: list[TextureResource] = []
|
|
"""For TLUT "textures", the CI textures using it"""
|
|
|
|
if size_bytes % 8 == 0 and (file.alignment + range_start) % 8 == 0:
|
|
self.alignment = 8
|
|
elif size_bytes % 4 == 0 and (file.alignment + range_start) % 4 == 0:
|
|
self.alignment = 4
|
|
else:
|
|
raise NotImplementedError(
|
|
"unimplemented: unaligned texture size/offset",
|
|
hex(size_bytes),
|
|
hex(range_start),
|
|
)
|
|
|
|
alignment_bits = self.alignment * 8
|
|
self.elem_type = f"u{alignment_bits}"
|
|
assert self.elem_type in {"u64", "u32"}
|
|
|
|
self.width_name = f"{self.symbol_name}_WIDTH"
|
|
self.height_name = f"{self.symbol_name}_HEIGHT"
|
|
|
|
def get_as_xml(self):
|
|
tlut_offset_attr = (
|
|
f' TlutOffset="0x{self.resource_tlut.range_start:X}"'
|
|
if self.resource_tlut
|
|
else ""
|
|
)
|
|
return f"""\
|
|
<Texture Name="{self.symbol_name}" Format="{self.fmt.name.lower()}{self.siz.bpp}" Width="{self.width}" Height="{self.height}" Offset="0x{self.range_start:X}"{tlut_offset_attr}/>"""
|
|
|
|
def check_declare_length(self):
|
|
return (
|
|
hasattr(self, "HACK_IS_STATIC_ON") or EXPLICIT_DL_AND_TEX_SIZES
|
|
) and not self.is_tlut()
|
|
|
|
def get_c_declaration_base(self):
|
|
if hasattr(self, "HACK_IS_STATIC_ON") and self.is_tlut():
|
|
raise NotImplementedError
|
|
if self.check_declare_length():
|
|
return (
|
|
f"{self.elem_type} {self.symbol_name}"
|
|
f"[TEX_LEN({self.elem_type}, {self.width_name}, {self.height_name}, {self.siz.bpp})]"
|
|
)
|
|
return f"{self.elem_type} {self.symbol_name}[]"
|
|
|
|
def get_c_reference(self, resource_offset: int):
|
|
if resource_offset == 0:
|
|
return self.symbol_name
|
|
else:
|
|
raise ValueError(self, hex(resource_offset))
|
|
|
|
def resources_ci_list_append(self, resource_ci: "TextureResource"):
|
|
if resource_ci not in self.resources_ci_list:
|
|
self.resources_ci_list.append(resource_ci)
|
|
|
|
HACK_NO_CHECK_TLUT_BOUNDS = self.name in {
|
|
# this TLUT should be 184 colors instead of 136 as defined in the xml,
|
|
# but then it would overlap with gZelda2_6TLUT.
|
|
"gZelda2_5TLUT",
|
|
# the skybox TLUTs are used "weirdly" (TODO understand)
|
|
"gSunriseSkyboxTLUT",
|
|
"gSunsetSkyboxTLUT",
|
|
"gDayOvercastSkyboxTLUT",
|
|
"gNightOvercastSkyboxTLUT",
|
|
"gHoly1SkyboxTLUT",
|
|
}
|
|
|
|
if __debug__ and not HACK_NO_CHECK_TLUT_BOUNDS:
|
|
# if not a TLUTResource, but only a TextureResource,
|
|
# this TLUT is xml-defined (using <Texture>)
|
|
if self.__class__ == TextureResource:
|
|
# check the CI texture doesn't index OOB into this tlut
|
|
|
|
if resource_ci.file.data is None:
|
|
# TODO see similar in TLUTResource.resources_ci_list_append
|
|
return
|
|
|
|
# Copypasted from TLUTResource.resources_ci_list_append
|
|
|
|
resource_ci_data = resource_ci.file.data[
|
|
resource_ci.range_start : resource_ci.range_end
|
|
]
|
|
|
|
assert resource_ci.siz in {G_IM_SIZ._4b, G_IM_SIZ._8b}
|
|
|
|
if resource_ci.siz == G_IM_SIZ._4b:
|
|
v_max = max(max((b >> 4) & 0xF, b & 0xF) for b in resource_ci_data)
|
|
assert v_max < 16
|
|
|
|
if resource_ci.siz == G_IM_SIZ._8b:
|
|
v_max = max(resource_ci_data)
|
|
assert v_max < 256
|
|
|
|
new_min_count = v_max + 1
|
|
|
|
# end Copypasted
|
|
|
|
cur_count = self.width * self.height
|
|
|
|
assert cur_count >= new_min_count, (
|
|
"TLUT resource",
|
|
self,
|
|
"is defined as having",
|
|
cur_count,
|
|
"colors, but there is an image using it as having at least",
|
|
new_min_count,
|
|
"colors:",
|
|
resource_ci,
|
|
)
|
|
|
|
def set_tlut(self, resource_tlut: "TextureResource"):
|
|
assert self.fmt == G_IM_FMT.CI, (self, resource_tlut)
|
|
if self.resource_tlut is not None:
|
|
HACK_NO_FAIL_MULTIPLE_TLUTS = self.name in {
|
|
"gZelda2Tex_003A08",
|
|
}
|
|
if self.resource_tlut != resource_tlut and not HACK_NO_FAIL_MULTIPLE_TLUTS:
|
|
# Technically not impossible so NotImplementedError
|
|
raise NotImplementedError(
|
|
"Color-indexed texture using two different TLUTs",
|
|
self,
|
|
resource_tlut,
|
|
)
|
|
return
|
|
|
|
# Assert resource_tlut is rgba16.
|
|
# Note it could be ia16, but that's not implemented
|
|
assert (
|
|
resource_tlut.fmt == G_IM_FMT.RGBA and resource_tlut.siz == G_IM_SIZ._16b
|
|
), resource_tlut
|
|
|
|
self.resource_tlut = resource_tlut
|
|
assert self not in resource_tlut.resources_ci_list
|
|
resource_tlut.resources_ci_list_append(self)
|
|
|
|
def try_parse_data(self, memory_context):
|
|
if self.fmt != G_IM_FMT.CI:
|
|
# Nothing to do
|
|
return RESOURCE_PARSE_SUCCESS
|
|
else:
|
|
if self.resource_tlut is None:
|
|
raise ResourceParseWaiting(waiting_for=["self.resource_tlut"])
|
|
return RESOURCE_PARSE_SUCCESS
|
|
|
|
def is_tlut(self):
|
|
"""The result is only meaningful after all resources have been parsed
|
|
|
|
(otherwise, for example, the dlists referencing this resource
|
|
as a tlut may not have been parsed and this would be considered
|
|
a regular texture)
|
|
"""
|
|
return len(self.resources_ci_list) != 0
|
|
|
|
def is_shared_tlut(self):
|
|
# Same caveat as is_tlut
|
|
return len(self.resources_ci_list) >= 2
|
|
|
|
def tlut_can_omit_tlut_info_from_users(self):
|
|
assert self.is_tlut()
|
|
return len(self.resources_ci_list) == 1 and self.alignment == 8
|
|
|
|
def tlut_get_count(self):
|
|
assert self.is_tlut()
|
|
return self.width * self.height
|
|
|
|
def get_filename_stem(self):
|
|
format_name = f"{self.fmt.name.lower()}{self.siz.bpp}"
|
|
|
|
if self.elem_type != "u64":
|
|
elem_type_suffix = f".{self.elem_type}"
|
|
else:
|
|
elem_type_suffix = ""
|
|
|
|
if self.fmt == G_IM_FMT.CI:
|
|
assert self.resource_tlut is not None
|
|
tlut_info = f"tlut_{self.resource_tlut.name}"
|
|
if self.resource_tlut.elem_type != "u64":
|
|
tlut_info += f"_{self.resource_tlut.elem_type}"
|
|
if not self.resource_tlut.tlut_can_omit_tlut_info_from_users():
|
|
return f"{self.name}.{format_name}.{tlut_info}{elem_type_suffix}"
|
|
else:
|
|
return f"{self.name}.{format_name}{elem_type_suffix}"
|
|
elif self.is_tlut():
|
|
if not self.tlut_can_omit_tlut_info_from_users():
|
|
return f"{self.name}.tlut.{format_name}{elem_type_suffix}"
|
|
else:
|
|
return f"{self.resources_ci_list[0].name}.tlut.{format_name}{elem_type_suffix}"
|
|
else:
|
|
return f"{self.name}.{format_name}{elem_type_suffix}"
|
|
|
|
def write_extracted(self, memory_context):
|
|
if self.is_tlut():
|
|
# TLUTs are extracted as part of the color-indexed textures using them
|
|
|
|
def is_all_resources_fake():
|
|
return all(
|
|
hasattr(res, "FAKE_FOR_BEST_EFFORT")
|
|
for res in self.resources_ci_list
|
|
)
|
|
|
|
if BEST_EFFORT:
|
|
if is_all_resources_fake():
|
|
assert self.fmt == G_IM_FMT.RGBA
|
|
|
|
if VERBOSE_BEST_EFFORT_TLUT_NO_REAL_USER and not getattr(
|
|
self, "HACK_ignore_orphaned_tlut", False
|
|
):
|
|
print(
|
|
"BEST_EFFORT",
|
|
"no real (non-fake for best effort) ci resource uses this tlut",
|
|
)
|
|
rich_pprint(self)
|
|
print(
|
|
" extracting the tlut as its own png",
|
|
self.extract_to_path.resolve().as_uri(),
|
|
"instead of relying on it being generated",
|
|
"\n (note while the result may build and match the tlut probably is",
|
|
"the wrong size since there was no ci image to take/guess its length from)",
|
|
)
|
|
|
|
# Extract the tlut as png instead of relying on
|
|
# it being generated from pngs using it, since
|
|
# there are no such pngs.
|
|
# (Copypaste of the general case (non-tlut) below):
|
|
data = self.file.data[self.range_start : self.range_end]
|
|
assert len(data) == self.range_end - self.range_start
|
|
write_n64_image_to_png(
|
|
self.extract_to_path,
|
|
self.width,
|
|
self.height,
|
|
self.fmt,
|
|
self.siz,
|
|
data,
|
|
)
|
|
else:
|
|
# assert this TLUT is used by at least one real resource,
|
|
# otherwise it won't be generated by anything
|
|
assert not is_all_resources_fake()
|
|
return
|
|
|
|
data = self.file.data[self.range_start : self.range_end]
|
|
assert len(data) == self.range_end - self.range_start
|
|
if self.fmt == G_IM_FMT.CI:
|
|
tlut_data = self.resource_tlut.file.data[
|
|
self.resource_tlut.range_start : self.resource_tlut.range_end
|
|
]
|
|
tlut_count = self.resource_tlut.tlut_get_count()
|
|
tlut_fmt = self.resource_tlut.fmt
|
|
write_n64_image_to_png_color_indexed(
|
|
self.extract_to_path,
|
|
self.width,
|
|
self.height,
|
|
self.fmt,
|
|
self.siz,
|
|
data,
|
|
tlut_data,
|
|
tlut_count,
|
|
tlut_fmt,
|
|
)
|
|
else:
|
|
write_n64_image_to_png(
|
|
self.extract_to_path, self.width, self.height, self.fmt, self.siz, data
|
|
)
|
|
|
|
def write_c_declaration(self, h: io.TextIOBase):
|
|
if self.is_tlut():
|
|
# TODO
|
|
h.writelines(
|
|
(f"//#define {self.symbol_name}_TLUT_COUNT {self.tlut_get_count()}\n",)
|
|
)
|
|
else:
|
|
h.writelines(
|
|
(
|
|
f"#define {self.width_name} {self.width}\n",
|
|
f"#define {self.height_name} {self.height}\n",
|
|
)
|
|
)
|
|
super().write_c_declaration(h)
|
|
|
|
def get_h_includes(self):
|
|
return (
|
|
"ultra64.h",
|
|
*(("tex_len.h",) if self.check_declare_length() else ()),
|
|
)
|
|
|
|
@reprlib.recursive_repr()
|
|
def __repr__(self):
|
|
return super().__repr__().removesuffix(")") + (
|
|
f", fmt={self.fmt}, siz={self.siz}"
|
|
f", width={self.width}, height={self.height}"
|
|
f", elem_type={self.elem_type}"
|
|
f", resource_tlut={self.resource_tlut}, resources_ci_list={self.resources_ci_list}"
|
|
")"
|
|
)
|
|
|
|
def __rich_repr__(self):
|
|
yield from super().__rich_repr__()
|
|
yield "fmt", self.fmt
|
|
yield "siz", self.siz
|
|
yield "width", self.width
|
|
yield "height", self.height
|
|
yield "elem_type", self.elem_type
|
|
yield "resource_tlut", self.resource_tlut
|
|
yield "resources_ci_list", self.resources_ci_list
|
|
|
|
__rich_repr__.angular = True
|
|
|
|
|
|
class TLUTResource(TextureResource, can_size_be_unknown=True):
|
|
"""
|
|
Note this resource is only used for discovered TLUTs, not tluts from xmls
|
|
(TODO maybe change the xmls eventually)
|
|
Discovered TLUTs are different because their size is unknown
|
|
"""
|
|
|
|
def __init__(self, file: File, range_start: int, name: str, fmt: G_IM_FMT):
|
|
assert fmt in {G_IM_FMT.RGBA, G_IM_FMT.IA}
|
|
|
|
# just to make TextureResource.__init__ happy
|
|
# Note these values are picked so u64 elem_type is possible
|
|
fake_width = 4
|
|
fake_height = 1
|
|
|
|
super().__init__(
|
|
file, range_start, name, fmt, G_IM_SIZ._16b, fake_width, fake_height
|
|
)
|
|
|
|
def tlut_get_count(self):
|
|
if self.range_end is None:
|
|
raise Exception("cannot tlut_get_count, unknown count yet")
|
|
return super().tlut_get_count()
|
|
|
|
def resources_ci_list_append(self, resource_ci: "TextureResource"):
|
|
super().resources_ci_list_append(resource_ci)
|
|
|
|
if resource_ci.file.data is None:
|
|
# Can't expand length, the user CI texture has no data attached
|
|
# TODO handle better? but idk
|
|
# maybe just a warning
|
|
return
|
|
|
|
resource_ci_data = resource_ci.file.data[
|
|
resource_ci.range_start : resource_ci.range_end
|
|
]
|
|
|
|
assert resource_ci.siz in {G_IM_SIZ._4b, G_IM_SIZ._8b}
|
|
|
|
if resource_ci.siz == G_IM_SIZ._4b:
|
|
v_max = max(max((b >> 4) & 0xF, b & 0xF) for b in resource_ci_data)
|
|
assert v_max < 16
|
|
|
|
if resource_ci.siz == G_IM_SIZ._8b:
|
|
v_max = max(resource_ci_data)
|
|
assert v_max < 256
|
|
|
|
new_min_count = v_max + 1
|
|
|
|
assert self.height == 1
|
|
if new_min_count > self.width:
|
|
# round width up to a multiple of 4, for elem_type=u64
|
|
self.width = (new_min_count + 3) // 4 * 4
|
|
|
|
assert self.width <= 256
|
|
|
|
# TODO HACK this is hacky because not explicitly permitted,
|
|
# once set self.range_end is assumed to be fixed
|
|
# but surely it'll be fine (copium)
|
|
# In practice nothing should reference inside a tlut,
|
|
# so the resource has all the time to expand.
|
|
# A better implementation would be to give a "final warning" kind of signal
|
|
# to the resource on try_parse_data (add optional tryhard_parse_data ?)
|
|
assert self.siz == G_IM_SIZ._16b
|
|
self.range_end = self.range_start + self.width * self.height * 2
|
|
|
|
|
|
class TextureSplitTlutResource(TextureResource):
|
|
|
|
def __init__(self, file, range_start, name, fmt, siz, width, height, lo_half: bool):
|
|
assert fmt == G_IM_FMT.CI
|
|
assert siz == G_IM_SIZ._8b
|
|
super().__init__(file, range_start, name, fmt, siz, width, height)
|
|
self.lo_half = lo_half
|
|
|
|
def get_filename_stem(self):
|
|
assert self.elem_type == "u64"
|
|
assert self.resource_tlut.elem_type == "u64"
|
|
|
|
return f"{self.name}.ci8.split_{'lo' if self.lo_half else 'hi'}.tlut_{self.resource_tlut.name}"
|
|
|
|
def write_extracted(self, memory_context):
|
|
data = self.file.data[self.range_start : self.range_end]
|
|
assert len(data) == self.range_end - self.range_start
|
|
|
|
if self.lo_half:
|
|
assert all(_b < 128 for _b in data)
|
|
else:
|
|
assert all(_b >= 128 for _b in data)
|
|
data = bytes(_b - 128 for _b in data)
|
|
|
|
tlut_data = self.resource_tlut.file.data[
|
|
self.resource_tlut.range_start : self.resource_tlut.range_end
|
|
]
|
|
|
|
assert self.resource_tlut.tlut_get_count() == 128
|
|
|
|
write_n64_image_to_png_color_indexed(
|
|
self.extract_to_path,
|
|
self.width,
|
|
self.height,
|
|
self.fmt,
|
|
self.siz,
|
|
data,
|
|
tlut_data,
|
|
128,
|
|
self.resource_tlut.fmt,
|
|
)
|
|
|
|
|
|
def gfxdis(
|
|
*,
|
|
input_buffer: Union[bytes, memoryview],
|
|
output_callback: Optional[
|
|
Callable[[bytes], None] # deviates a bit from gfxd, no count arg/return
|
|
] = None,
|
|
enable_caps: set[pygfxd.GfxdCap] = {
|
|
pygfxd.GfxdCap.stop_on_end,
|
|
pygfxd.GfxdCap.emit_dec_color,
|
|
},
|
|
target: pygfxd.gfx_ucode_t = pygfxd.gfxd_f3dex2,
|
|
vtx_callback: Optional[Callable[[int, int], int]] = None,
|
|
timg_callback: Optional[Callable[[int, int, int, int, int, int], int]] = None,
|
|
tlut_callback: Optional[Callable[[int, int, int], int]] = None,
|
|
mtx_callback: Optional[Callable[[int], int]] = None,
|
|
dl_callback: Optional[Callable[[int], int]] = None,
|
|
macro_fn: Optional[Callable[[], int]] = None,
|
|
arg_fn: Optional[Callable[[int], None]] = None,
|
|
):
|
|
for cap in (
|
|
pygfxd.GfxdCap.stop_on_invalid,
|
|
pygfxd.GfxdCap.stop_on_end,
|
|
pygfxd.GfxdCap.emit_dec_color,
|
|
pygfxd.GfxdCap.emit_q_macro,
|
|
pygfxd.GfxdCap.emit_ext_macro,
|
|
):
|
|
if cap in enable_caps:
|
|
pygfxd.gfxd_enable(cap)
|
|
else:
|
|
pygfxd.gfxd_disable(cap)
|
|
|
|
pygfxd.gfxd_target(target)
|
|
|
|
pygfxd.gfxd_input_buffer(bytes(input_buffer))
|
|
|
|
uncaught_exc_infos = []
|
|
|
|
# output_callback
|
|
|
|
if output_callback:
|
|
|
|
def output_callback_wrapper(buf, count):
|
|
try:
|
|
output_callback(buf)
|
|
except:
|
|
import sys
|
|
|
|
exc_info = sys.exc_info()
|
|
uncaught_exc_infos.append(exc_info)
|
|
return count
|
|
|
|
else:
|
|
|
|
def output_callback_wrapper(buf, count):
|
|
return count
|
|
|
|
pygfxd.gfxd_output_callback(output_callback_wrapper)
|
|
|
|
# vtx_callback
|
|
|
|
if vtx_callback:
|
|
|
|
def vtx_callback_wrapper(vtx, num):
|
|
try:
|
|
ret = vtx_callback(vtx, num)
|
|
except:
|
|
import sys
|
|
|
|
exc_info = sys.exc_info()
|
|
uncaught_exc_infos.append(exc_info)
|
|
|
|
ret = 0
|
|
return ret
|
|
|
|
else:
|
|
|
|
def vtx_callback_wrapper(vtx, num):
|
|
return 0
|
|
|
|
pygfxd.gfxd_vtx_callback(vtx_callback_wrapper)
|
|
|
|
# timg_callback
|
|
|
|
if timg_callback:
|
|
|
|
def timg_callback_wrapper(timg, fmt, siz, width, height, pal):
|
|
try:
|
|
ret = timg_callback(timg, fmt, siz, width, height, pal)
|
|
assert isinstance(ret, int)
|
|
except:
|
|
import sys
|
|
|
|
exc_info = sys.exc_info()
|
|
uncaught_exc_infos.append(exc_info)
|
|
|
|
ret = 0
|
|
return ret
|
|
|
|
else:
|
|
|
|
def timg_callback_wrapper(timg, fmt, siz, width, height, pal):
|
|
return 0
|
|
|
|
pygfxd.gfxd_timg_callback(timg_callback_wrapper)
|
|
|
|
# tlut_callback
|
|
|
|
if tlut_callback:
|
|
|
|
def tlut_callback_wrapper(tlut, idx, count):
|
|
try:
|
|
ret = tlut_callback(tlut, idx, count)
|
|
except:
|
|
import sys
|
|
|
|
exc_info = sys.exc_info()
|
|
uncaught_exc_infos.append(exc_info)
|
|
|
|
ret = 0
|
|
return ret
|
|
|
|
else:
|
|
|
|
def tlut_callback_wrapper(tlut, idx, count):
|
|
return 0
|
|
|
|
pygfxd.gfxd_tlut_callback(tlut_callback_wrapper)
|
|
|
|
# mtx_callback
|
|
|
|
if mtx_callback:
|
|
|
|
def mtx_callback_wrapper(mtx):
|
|
try:
|
|
ret = mtx_callback(mtx)
|
|
except:
|
|
import sys
|
|
|
|
exc_info = sys.exc_info()
|
|
uncaught_exc_infos.append(exc_info)
|
|
|
|
ret = 0
|
|
return ret
|
|
|
|
else:
|
|
|
|
def mtx_callback_wrapper(mtx):
|
|
return 0
|
|
|
|
pygfxd.gfxd_mtx_callback(mtx_callback_wrapper)
|
|
|
|
# dl_callback
|
|
|
|
if dl_callback:
|
|
|
|
def dl_callback_wrapper(dl):
|
|
try:
|
|
ret = dl_callback(dl)
|
|
except:
|
|
import sys
|
|
|
|
exc_info = sys.exc_info()
|
|
uncaught_exc_infos.append(exc_info)
|
|
|
|
ret = 0
|
|
return ret
|
|
|
|
else:
|
|
|
|
def dl_callback_wrapper(dl):
|
|
return 0
|
|
|
|
pygfxd.gfxd_dl_callback(dl_callback_wrapper)
|
|
|
|
# macro_fn
|
|
|
|
if macro_fn:
|
|
|
|
def macro_fn_wrapper():
|
|
try:
|
|
ret = macro_fn()
|
|
except:
|
|
import sys
|
|
|
|
exc_info = sys.exc_info()
|
|
uncaught_exc_infos.append(exc_info)
|
|
|
|
ret = 0
|
|
|
|
# TODO consider:
|
|
if uncaught_exc_infos:
|
|
pygfxd.gfxd_input_buffer(
|
|
b""
|
|
) # interrupt current execution TODO check if this is safe and if it works
|
|
ret = 1
|
|
|
|
return ret
|
|
|
|
else:
|
|
|
|
def macro_fn_wrapper():
|
|
ret = pygfxd.gfxd_macro_dflt()
|
|
|
|
# TODO consider:
|
|
if uncaught_exc_infos:
|
|
pygfxd.gfxd_input_buffer(b"") # TODO see same line above
|
|
ret = 1
|
|
|
|
return ret
|
|
|
|
pygfxd.gfxd_macro_fn(macro_fn_wrapper)
|
|
|
|
# arg_fn
|
|
|
|
if arg_fn:
|
|
|
|
def arg_fn_wrapper(arg_num):
|
|
try:
|
|
arg_fn(arg_num)
|
|
except:
|
|
import sys
|
|
|
|
exc_info = sys.exc_info()
|
|
uncaught_exc_infos.append(exc_info)
|
|
|
|
pygfxd.gfxd_arg_fn(arg_fn_wrapper)
|
|
else:
|
|
pygfxd.gfxd_arg_fn(None)
|
|
|
|
# Execute
|
|
|
|
pygfxd.gfxd_execute()
|
|
|
|
# The offset is in bytes and indicates the last command,
|
|
# so add 8 (sizeof(Gfx))
|
|
size = pygfxd.gfxd_macro_offset() + 8
|
|
|
|
if uncaught_exc_infos:
|
|
import traceback
|
|
|
|
msg = "There were uncaught python errors in callbacks during gfxd execution."
|
|
|
|
print()
|
|
print(msg)
|
|
print("vvv See below for a list of the traces of the uncaught errors:")
|
|
|
|
for exc_info in uncaught_exc_infos:
|
|
import sys
|
|
|
|
print()
|
|
traceback.print_exception(*exc_info, file=sys.stdout)
|
|
|
|
print()
|
|
print(msg)
|
|
print("^^^ See above for a list of the traces of the uncaught errors.")
|
|
|
|
raise Exception(
|
|
msg,
|
|
"See the standard output for a list of the traces of the uncaught errors.",
|
|
uncaught_exc_infos,
|
|
)
|
|
|
|
return size
|
|
|
|
|
|
class StringWrapper:
|
|
def __init__(
|
|
self,
|
|
data: Union[str, bytes],
|
|
max_line_length,
|
|
writer: Callable[[Union[str, bytes]], None],
|
|
):
|
|
self.max_line_length = max_line_length
|
|
self.pending_data = data
|
|
self.writer = writer
|
|
self.newline_char = "\n" if isinstance(data, str) else b"\n"
|
|
self.space_char = " " if isinstance(data, str) else b" "
|
|
|
|
def append(self, data: Union[str, bytes]):
|
|
self.pending_data += data
|
|
self.proc()
|
|
|
|
def proc(self, flush=False):
|
|
while len(self.pending_data) > self.max_line_length or (
|
|
flush and self.pending_data
|
|
):
|
|
i = self.pending_data.find(self.newline_char, 0, self.max_line_length)
|
|
if i >= 0:
|
|
i += 1
|
|
self.writer(self.pending_data[:i])
|
|
self.pending_data = self.pending_data[i:]
|
|
continue
|
|
|
|
i = self.pending_data.rfind(self.space_char, 1, self.max_line_length)
|
|
if i < 0:
|
|
i = self.pending_data.find(self.space_char, 1)
|
|
if i < 0:
|
|
if flush:
|
|
i = len(self.pending_data)
|
|
else:
|
|
# Avoid adding a line return in the middle of a word
|
|
return
|
|
self.writer(self.pending_data[:i])
|
|
self.pending_data = self.pending_data[i:]
|
|
if not flush or self.pending_data:
|
|
self.writer(self.newline_char)
|
|
|
|
def flush(self):
|
|
self.proc(flush=True)
|
|
|
|
|
|
class Ucode(enum.Enum):
|
|
f3dex = pygfxd.gfxd_f3dex
|
|
f3dex2 = pygfxd.gfxd_f3dex2
|
|
|
|
def __init__(self, gfxd_ucode: pygfxd.gfx_ucode_t):
|
|
self.gfxd_ucode = gfxd_ucode
|
|
|
|
|
|
# TODO probably need to split TLUTResource from TextureResource
|
|
# to achieve cleaner code,
|
|
# tluts behave very differently
|
|
|
|
|
|
# TODO maybe refactor this once it works tm
|
|
class ColorIndexedTexturesManager:
|
|
from dataclasses import dataclass
|
|
|
|
@dataclass
|
|
class Tex:
|
|
timg: int
|
|
fmt: G_IM_FMT
|
|
siz: G_IM_SIZ
|
|
width: int
|
|
height: int
|
|
pal: int
|
|
|
|
@dataclass
|
|
class Tlut:
|
|
tlut: int
|
|
idx: int
|
|
count: int
|
|
|
|
@dataclass
|
|
class CIState:
|
|
tlut_mode: G_TT
|
|
tluts_count: int
|
|
tluts: dict[int, "ColorIndexedTexturesManager.Tlut"]
|
|
texs: list["ColorIndexedTexturesManager.Tex"]
|
|
|
|
def __init__(self, *, HACK_late_SetTextureLUT=False):
|
|
self.cur_tlut_mode: G_TT = None
|
|
|
|
self.cur_tluts_count: int = None
|
|
self.cur_tluts: dict[int, ColorIndexedTexturesManager.Tlut] = dict()
|
|
self.cur_texs: list[ColorIndexedTexturesManager.Tex] = []
|
|
|
|
self.ci_states: list[ColorIndexedTexturesManager.CIState] = []
|
|
|
|
# Rarely,
|
|
# gsDPSetTextureLUT comes after gsDPLoadTextureBlock and gsDPLoadTLUT,
|
|
# instead of before
|
|
self.HACK_late_SetTextureLUT = HACK_late_SetTextureLUT
|
|
|
|
def ci_timg(self, timg, fmt: G_IM_FMT, siz: G_IM_SIZ, width, height, pal):
|
|
if VERBOSE_ColorIndexedTexturesManager:
|
|
print(
|
|
"ColorIndexedTexturesManager.ci_timg",
|
|
hex(timg),
|
|
fmt,
|
|
siz,
|
|
width,
|
|
height,
|
|
pal,
|
|
)
|
|
assert fmt == G_IM_FMT.CI
|
|
if not self.HACK_late_SetTextureLUT:
|
|
assert self.cur_tlut_mode != G_TT.NONE
|
|
|
|
self.cur_texs.append(
|
|
ColorIndexedTexturesManager.Tex(timg, fmt, siz, width, height, pal)
|
|
)
|
|
|
|
def tlut(self, tlut, idx, count):
|
|
if VERBOSE_ColorIndexedTexturesManager:
|
|
print("ColorIndexedTexturesManager.tlut", hex(tlut), idx, count)
|
|
if idx == -1:
|
|
# HACK idx==-1 may be a libgfxd bug?
|
|
assert count == 256
|
|
idx = 0
|
|
if not self.HACK_late_SetTextureLUT:
|
|
assert self.cur_tlut_mode != G_TT.NONE
|
|
if self.cur_tluts_count != count:
|
|
self.cur_tluts.clear() # TODO ? idk. (at worst it will cause errors)
|
|
self.cur_tluts_count = count
|
|
self.cur_tluts[idx] = ColorIndexedTexturesManager.Tlut(tlut, idx, count)
|
|
|
|
def tlut_mode(self, tt: G_TT):
|
|
if VERBOSE_ColorIndexedTexturesManager:
|
|
print("ColorIndexedTexturesManager.tlut_mode", tt)
|
|
if self.cur_tlut_mode != tt:
|
|
if not self.HACK_late_SetTextureLUT:
|
|
self.cur_tluts.clear() # TODO ? idk. (at worst it will cause errors)
|
|
self.cur_tlut_mode = tt
|
|
|
|
def commit_state(self):
|
|
if self.cur_tlut_mode == G_TT.NONE:
|
|
return
|
|
if not self.cur_texs:
|
|
return
|
|
assert self.cur_tluts
|
|
assert self.cur_tluts_count is not None
|
|
if self.cur_tlut_mode is None:
|
|
if BEST_EFFORT:
|
|
# Some dlists (eg gMegamiPiece2DL) inherit G_TT_RGBA16
|
|
# since ia16 is uncommon if not unused, just default to that
|
|
self.cur_tlut_mode = G_TT.RGBA16
|
|
assert self.cur_tlut_mode is not None
|
|
cur_state = ColorIndexedTexturesManager.CIState(
|
|
self.cur_tlut_mode,
|
|
self.cur_tluts_count,
|
|
self.cur_tluts.copy(),
|
|
self.cur_texs,
|
|
)
|
|
self.cur_texs = []
|
|
self.ci_states.append(cur_state)
|
|
if VERBOSE_ColorIndexedTexturesManager:
|
|
print(
|
|
"ColorIndexedTexturesManager.commit_state",
|
|
"cur_state =",
|
|
cur_state,
|
|
)
|
|
|
|
def report_states(self, reporter: Resource, memory_context: "MemoryContext"):
|
|
if VERBOSE_ColorIndexedTexturesManager:
|
|
print("ColorIndexedTexturesManager.report_states")
|
|
for ci_state in self.ci_states:
|
|
if VERBOSE_ColorIndexedTexturesManager:
|
|
print(" ci_state =", ci_state)
|
|
assert ci_state.tlut_mode != G_TT.NONE
|
|
for tex in ci_state.texs:
|
|
if VERBOSE_ColorIndexedTexturesManager:
|
|
print(" tex =", tex)
|
|
assert tex.fmt == G_IM_FMT.CI
|
|
|
|
# HACK
|
|
if (
|
|
reporter.file.name
|
|
in {
|
|
"jyasinzou_room_5",
|
|
"jyasinzou_room_25",
|
|
}
|
|
and ci_state.tluts_count == 256
|
|
and tex.siz == G_IM_SIZ._4b
|
|
):
|
|
# For some reason jyasinzou_room_5DL_008EC0 has this:
|
|
"""
|
|
gsDPLoadTextureBlock_4b(jyasinzou_sceneTex_019320, G_IM_FMT_CI, 64, 64, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR
|
|
| G_TX_WRAP, 6, 6, G_TX_NOLOD, G_TX_NOLOD),
|
|
gsDPLoadTLUT_pal16(0, jyasinzou_sceneTLUT_018000),
|
|
gsDPLoadTextureBlock(jyasinzou_room_5Tex_00DFC8, G_IM_FMT_CI, G_IM_SIZ_8b, 64, 32, 0, G_TX_NOMIRROR | G_TX_WRAP,
|
|
G_TX_NOMIRROR | G_TX_WRAP, 6, 5, G_TX_NOLOD, G_TX_NOLOD),
|
|
gsDPLoadTLUT_pal256(jyasinzou_sceneTLUT_017DE0),
|
|
"""
|
|
# (where the first texture & tlut loads are useless, overriden by the latter two)
|
|
|
|
# -> Ignore the first texture (in `tex` at this point)
|
|
|
|
# similar thing in jyasinzou_room_25DL_005050
|
|
|
|
# (note the condition to restrict this to the above cases is very broad,
|
|
# just checking reporter.file.name, so this may happen more in those files
|
|
# and not have been caught and documented here)
|
|
|
|
continue
|
|
|
|
# Not a proper hard requirement but if this fails
|
|
# something is probably wrong, look at details then
|
|
assert (
|
|
ci_state.tluts_count
|
|
== {
|
|
G_IM_SIZ._4b: 16,
|
|
G_IM_SIZ._8b: 256,
|
|
}[tex.siz]
|
|
), (reporter, ci_state, tex)
|
|
|
|
if ci_state.tluts_count > 16:
|
|
assert ci_state.tluts.keys() == {0}, ci_state.tluts
|
|
|
|
resource = memory_context.report_resource_at_segmented(
|
|
reporter,
|
|
tex.timg,
|
|
TextureResource,
|
|
lambda file, offset: TextureResource(
|
|
file,
|
|
offset,
|
|
f"{file.name if TEXS_SHORTER_NAMES else reporter.name}_{offset:08X}_CITex",
|
|
tex.fmt,
|
|
tex.siz,
|
|
tex.width,
|
|
tex.height,
|
|
),
|
|
)
|
|
|
|
tlut = ci_state.tluts[tex.pal]
|
|
assert tlut.idx == tex.pal
|
|
assert tlut.count == ci_state.tluts_count
|
|
|
|
resource_tlut = memory_context.report_resource_at_segmented(
|
|
reporter,
|
|
tlut.tlut,
|
|
# TLUTs declared in xmls use <Texture> so are TextureResource,
|
|
# so we can only expects a TextureResource
|
|
# (TLUTResource is a subclass of TextureResource)
|
|
TextureResource,
|
|
lambda file, offset: TLUTResource(
|
|
file,
|
|
offset,
|
|
f"{file.name if TEXS_SHORTER_NAMES else reporter.name}_{offset:08X}_TLUT",
|
|
{
|
|
G_TT.RGBA16: G_IM_FMT.RGBA,
|
|
G_TT.IA16: G_IM_FMT.IA,
|
|
}[ci_state.tlut_mode],
|
|
),
|
|
)
|
|
|
|
resource.set_tlut(resource_tlut)
|
|
|
|
|
|
class DListResource(Resource, can_size_be_unknown=True):
|
|
def __init__(
|
|
self,
|
|
file: File,
|
|
range_start: int,
|
|
name: str,
|
|
*,
|
|
target_ucode: Ucode = Ucode.f3dex2,
|
|
):
|
|
super().__init__(file, range_start, None, name)
|
|
self.target_ucode = target_ucode
|
|
self.ignored_raw_pointers: set[int] = set()
|
|
|
|
def set_length(self, length: int):
|
|
if length != ((self.range_end - self.range_start) // 8):
|
|
raise ValueError("length already set and different")
|
|
self.range_end = self.range_start + length * 8
|
|
|
|
def try_parse_data(self, memory_context):
|
|
offset = self.range_start
|
|
|
|
if VERBOSE2:
|
|
print(self.name, hex(offset))
|
|
|
|
def vtx_cb(vtx, num):
|
|
if vtx in self.ignored_raw_pointers:
|
|
return 0
|
|
|
|
# TODO be smarter about buffer merging
|
|
# (don't merge buffers from two different DLs, if they can be split cleanly)
|
|
# if that even happens
|
|
memory_context.mark_resource_buffer_at_segmented(
|
|
self,
|
|
VtxArrayResource,
|
|
f"{self.name}_{vtx:08X}_Vtx",
|
|
vtx,
|
|
vtx + num * VtxArrayResource.element_cdata_ext.size,
|
|
)
|
|
return 0
|
|
|
|
ci_tex_manager = ColorIndexedTexturesManager(
|
|
# TODO
|
|
HACK_late_SetTextureLUT=(self.name in {"gEponaHeadLimb_0600AC20_DL"})
|
|
)
|
|
|
|
def timg_cb(timg, fmt, siz, width, height, pal):
|
|
if timg in self.ignored_raw_pointers:
|
|
return 0
|
|
|
|
g_fmt = G_IM_FMT.by_i[fmt]
|
|
g_siz = G_IM_SIZ.by_i[siz]
|
|
|
|
if g_fmt == G_IM_FMT.CI:
|
|
ci_tex_manager.ci_timg(timg, g_fmt, g_siz, width, height, pal)
|
|
else:
|
|
memory_context.report_resource_at_segmented(
|
|
self,
|
|
timg,
|
|
TextureResource,
|
|
lambda file, offset: TextureResource(
|
|
file,
|
|
offset,
|
|
f"{file.name if TEXS_SHORTER_NAMES else self.name}_{offset:08X}_Tex",
|
|
g_fmt,
|
|
g_siz,
|
|
width,
|
|
height,
|
|
),
|
|
)
|
|
return 0
|
|
|
|
def tlut_cb(tlut, idx, count):
|
|
if tlut in self.ignored_raw_pointers:
|
|
return 0
|
|
|
|
ci_tex_manager.tlut(tlut, idx, count)
|
|
return 0
|
|
|
|
def mtx_cb(mtx):
|
|
if mtx in self.ignored_raw_pointers:
|
|
return 0
|
|
|
|
memory_context.report_resource_at_segmented(
|
|
self,
|
|
mtx,
|
|
MtxResource,
|
|
lambda file, offset: MtxResource(
|
|
file, offset, f"{self.name}_{mtx:08X}_Mtx"
|
|
),
|
|
)
|
|
return 0
|
|
|
|
def dl_cb(dl):
|
|
if dl in self.ignored_raw_pointers:
|
|
return 0
|
|
|
|
memory_context.report_resource_at_segmented(
|
|
self,
|
|
dl,
|
|
DListResource,
|
|
lambda file, offset: DListResource(
|
|
file,
|
|
offset,
|
|
f"{self.name}_{dl:08X}_DL",
|
|
target_ucode=self.target_ucode,
|
|
),
|
|
)
|
|
return 0
|
|
|
|
def macro_fn():
|
|
tt = pygfxd.gfxd_value_by_type(pygfxd.GfxdArgType.Tt, 0)
|
|
if tt is not None:
|
|
tt = tt[1]
|
|
else:
|
|
othermodehi = pygfxd.gfxd_value_by_type(
|
|
pygfxd.GfxdArgType.Othermodehi, 0
|
|
)
|
|
if othermodehi is not None:
|
|
othermodehi = othermodehi[1]
|
|
tt = othermodehi & (0b11 << G_MDSFT_TEXTLUT)
|
|
else:
|
|
tt = None
|
|
if tt is not None:
|
|
g_tt = G_TT.by_i[tt]
|
|
ci_tex_manager.tlut_mode(g_tt)
|
|
|
|
macro_id = pygfxd.gfxd_macro_id()
|
|
if macro_id in {
|
|
pygfxd.GfxdMacroId.SP1Triangle,
|
|
pygfxd.GfxdMacroId.SP2Triangles,
|
|
}:
|
|
ci_tex_manager.commit_state()
|
|
|
|
return pygfxd.gfxd_macro_dflt()
|
|
|
|
if self.range_end is None:
|
|
dlist_data = self.file.data[self.range_start :]
|
|
else:
|
|
dlist_data = self.file.data[self.range_start : self.range_end]
|
|
|
|
size = gfxdis(
|
|
input_buffer=dlist_data,
|
|
target=self.target_ucode.gfxd_ucode,
|
|
vtx_callback=vtx_cb,
|
|
timg_callback=timg_cb,
|
|
tlut_callback=tlut_cb,
|
|
mtx_callback=mtx_cb,
|
|
dl_callback=dl_cb,
|
|
macro_fn=macro_fn,
|
|
)
|
|
|
|
# Also commit state at the end of the display list.
|
|
# This handles "material" dlists that only load a texture,
|
|
# without drawing geometry.
|
|
ci_tex_manager.commit_state()
|
|
|
|
ci_tex_manager.report_states(self, memory_context)
|
|
|
|
self.range_end = self.range_start + size
|
|
|
|
if VERBOSE2:
|
|
print(self.name, hex(offset), hex(self.range_end))
|
|
|
|
return RESOURCE_PARSE_SUCCESS
|
|
|
|
def get_as_xml(self):
|
|
return f"""\
|
|
<DList Name="{self.symbol_name}" Offset="0x{self.range_start:X}"/>"""
|
|
|
|
def get_c_declaration_base(self):
|
|
if hasattr(self, "HACK_IS_STATIC_ON") or EXPLICIT_DL_AND_TEX_SIZES:
|
|
length = (self.range_end - self.range_start) // 8
|
|
return f"Gfx {self.symbol_name}[{length}]"
|
|
return f"Gfx {self.symbol_name}[]"
|
|
|
|
def get_c_reference(self, resource_offset: int):
|
|
if resource_offset == 0:
|
|
return self.symbol_name
|
|
else:
|
|
raise ValueError()
|
|
|
|
def write_extracted(self, memory_context):
|
|
def macro_fn():
|
|
pygfxd.gfxd_puts(INDENT)
|
|
ret = pygfxd.gfxd_macro_dflt()
|
|
pygfxd.gfxd_puts(",\n")
|
|
return ret
|
|
|
|
def arg_fn_handle_Dim(arg_num: int):
|
|
timg = pygfxd.gfxd_value_by_type(pygfxd.GfxdArgType.Timg, 0)
|
|
if timg is None:
|
|
return False
|
|
_, timg_segmented, _ = timg
|
|
dim_args_i = []
|
|
for arg_i in range(pygfxd.gfxd_arg_count()):
|
|
if pygfxd.gfxd_arg_type(arg_i) == pygfxd.GfxdArgType.Dim:
|
|
dim_args_i.append(arg_i)
|
|
assert arg_num in dim_args_i
|
|
assert len(dim_args_i) <= 2
|
|
if len(dim_args_i) != 2:
|
|
return False
|
|
width_arg_i, height_arg_i = dim_args_i
|
|
try:
|
|
timg_resolved = memory_context.resolve_segmented(timg_segmented)
|
|
except UnmappedAddressError:
|
|
# TODO store failed resolutions somewhere, for later printing
|
|
# (in general, it would be nice to fail less and *firmly* warn more instead,
|
|
# even if it means having compilation fail on purpose (#error))
|
|
return False
|
|
try:
|
|
resolved_resource = timg_resolved.get_resource(TextureResource)
|
|
except UnexpectedResourceTypeError:
|
|
# TODO investigate. eg spot18 uses 0x0800_0000 as both a DL and Tex ?
|
|
return False
|
|
|
|
assert isinstance(resolved_resource, TextureResource), (
|
|
hex(timg_segmented),
|
|
resolved_resource,
|
|
resolved_resource.__class__,
|
|
)
|
|
width_arg_value = pygfxd.gfxd_arg_value(width_arg_i)[1]
|
|
height_arg_value = pygfxd.gfxd_arg_value(height_arg_i)[1]
|
|
if (resolved_resource.width, resolved_resource.height) == (
|
|
width_arg_value,
|
|
height_arg_value,
|
|
):
|
|
if arg_num == width_arg_i:
|
|
if resolved_resource.width_name:
|
|
pygfxd.gfxd_puts(resolved_resource.width_name)
|
|
return True
|
|
else:
|
|
assert arg_num == height_arg_i
|
|
if resolved_resource.height_name:
|
|
pygfxd.gfxd_puts(resolved_resource.height_name)
|
|
return True
|
|
else:
|
|
HACK_no_warn_bad_dims_DLs = {
|
|
"sPresentedByNintendoDL", # uses gsDPLoadTextureTile, in which height is unused, so disassembled as 0 by gfxdis
|
|
"gMantUnusedMaterialDL", # DList bug
|
|
"gSunDL", # DList loads bigger chunks than the individual texture pieces (overlaps)
|
|
}
|
|
HACK_no_warn_bad_dims_Texs = {
|
|
"gPoeComposerFlatHeadDL_000060E0_Tex", # used as both rgba16 16x16 and rgba16 8x8
|
|
"gDekuStickTex", # used as both i8 8x8 and i8 16x16
|
|
"gHilite1Tex", # used as both rgba16 16x16 and rgba16 32x32
|
|
"gHilite2Tex", # used as both rgba16 16x16 and rgba16 32x32
|
|
"gUnknownCircle4Tex", # used as both i8 16x16 and rgba16 32x32
|
|
"gLinkChildLowerBootTex", # used as both ci8 32x32 and ci8 16x16
|
|
"gDecorativeFlameMaskTex", # used as both i4 32x128 and i4 32x64
|
|
}
|
|
if (
|
|
arg_num == width_arg_i
|
|
and self.name not in HACK_no_warn_bad_dims_DLs
|
|
and resolved_resource.name not in HACK_no_warn_bad_dims_Texs
|
|
):
|
|
print(
|
|
"Unexpected texture dimensions used: in dlist =",
|
|
self,
|
|
"texture =",
|
|
resolved_resource,
|
|
"texture resource has WxH =",
|
|
(resolved_resource.width, resolved_resource.height),
|
|
"but dlist uses WxH =",
|
|
(width_arg_value, height_arg_value),
|
|
)
|
|
pygfxd.gfxd_puts(
|
|
" /* ! Unexpected texture dimensions !"
|
|
f" DL={width_arg_value}x{height_arg_value}"
|
|
f" vs Tex={resolved_resource.width}x{resolved_resource.height} */ "
|
|
)
|
|
return False
|
|
|
|
arg_fn_handlers = {
|
|
pygfxd.GfxdArgType.Dim: arg_fn_handle_Dim,
|
|
}
|
|
|
|
def arg_fn(arg_num: int):
|
|
arg_type = pygfxd.gfxd_arg_type(arg_num)
|
|
arg_handler = arg_fn_handlers.get(arg_type)
|
|
|
|
if arg_handler is not None:
|
|
inhibit_default = arg_handler(arg_num)
|
|
else:
|
|
inhibit_default = False
|
|
|
|
if not inhibit_default:
|
|
pygfxd.gfxd_arg_dflt(arg_num)
|
|
|
|
def vtx_cb(vtx, num):
|
|
if vtx in self.ignored_raw_pointers:
|
|
return 0
|
|
|
|
pygfxd.gfxd_puts(memory_context.get_c_reference_at_segmented(vtx))
|
|
return 1
|
|
|
|
def timg_cb(timg, fmt, siz, width, height, pal):
|
|
if timg in self.ignored_raw_pointers:
|
|
return 0
|
|
|
|
try:
|
|
timg_c_ref = memory_context.get_c_reference_at_segmented(timg)
|
|
# except NoSegmentBaseError: # TODO
|
|
# timg_c_ref = None
|
|
except ValueError:
|
|
if BEST_EFFORT:
|
|
# (turns out I needed this because of a mistake in a xml so it can be useful)
|
|
import traceback
|
|
|
|
print("vvv /* BAD TIMG REF */ vvv")
|
|
traceback.print_exc()
|
|
pygfxd.gfxd_puts("/* BAD TIMG REF */")
|
|
return 0
|
|
else:
|
|
raise
|
|
if timg_c_ref:
|
|
pygfxd.gfxd_puts(timg_c_ref)
|
|
return 1
|
|
return 0
|
|
|
|
def tlut_cb(tlut, idx, count):
|
|
if tlut in self.ignored_raw_pointers:
|
|
return 0
|
|
|
|
tlut_c_ref = memory_context.get_c_reference_at_segmented(tlut)
|
|
pygfxd.gfxd_puts(tlut_c_ref)
|
|
return 1
|
|
|
|
def mtx_cb(mtx):
|
|
if mtx in self.ignored_raw_pointers:
|
|
return 0
|
|
|
|
mtx_c_ref = memory_context.get_c_reference_at_segmented(mtx)
|
|
pygfxd.gfxd_puts(mtx_c_ref)
|
|
return 1
|
|
|
|
def dl_cb(dl):
|
|
if dl in self.ignored_raw_pointers:
|
|
return 0
|
|
|
|
dl_c_ref = memory_context.get_c_reference_at_segmented(dl)
|
|
pygfxd.gfxd_puts(dl_c_ref)
|
|
return 1
|
|
|
|
with self.extract_to_path.open("wb") as f:
|
|
if not self.braces_in_source:
|
|
f.write(b"{\n")
|
|
|
|
out_string_wrapper = StringWrapper(b"", 120, f.write)
|
|
|
|
def output_cb(buf: bytes):
|
|
out_string_wrapper.append(buf)
|
|
|
|
gfxdis(
|
|
input_buffer=self.file.data[self.range_start : self.range_end],
|
|
output_callback=output_cb,
|
|
enable_caps={pygfxd.GfxdCap.emit_dec_color},
|
|
target=self.target_ucode.gfxd_ucode,
|
|
vtx_callback=vtx_cb,
|
|
timg_callback=timg_cb,
|
|
tlut_callback=tlut_cb,
|
|
mtx_callback=mtx_cb,
|
|
dl_callback=dl_cb,
|
|
macro_fn=macro_fn,
|
|
arg_fn=arg_fn,
|
|
)
|
|
|
|
out_string_wrapper.flush()
|
|
|
|
if not self.braces_in_source:
|
|
f.write(b"}\n")
|
|
|
|
def get_c_includes(self):
|
|
return (
|
|
# TODO these are not always needed:
|
|
"sys_matrix.h", # for gIdentityMtx
|
|
)
|
|
|
|
def get_h_includes(self):
|
|
return ("ultra64.h",)
|
|
|
|
|
|
def report_gfx_segmented(resource: Resource, memory_context: "MemoryContext", v):
|
|
assert isinstance(v, int)
|
|
address = v
|
|
if address != 0:
|
|
memory_context.report_resource_at_segmented(
|
|
resource,
|
|
address,
|
|
DListResource,
|
|
lambda file, offset: DListResource(
|
|
file,
|
|
offset,
|
|
f"{resource.name}_{address:08X}_DL",
|
|
),
|
|
)
|
|
|
|
|
|
def write_gfx_segmented(
|
|
resource: Resource,
|
|
memory_context: "MemoryContext",
|
|
v,
|
|
wctx: CDataExtWriteContext,
|
|
):
|
|
assert isinstance(v, int)
|
|
address = v
|
|
wctx.f.write(wctx.line_prefix)
|
|
if address == 0:
|
|
wctx.f.write("NULL")
|
|
else:
|
|
wctx.f.write(memory_context.get_c_reference_at_segmented(address))
|
|
return True
|
|
|
|
|
|
cdata_ext_gfx_segmented = (
|
|
CDataExt_Value("I").set_report(report_gfx_segmented).set_write(write_gfx_segmented)
|
|
)
|
|
|
|
VERBOSE2 = False
|