oot/tools/assets/extract/extase_oot64/dlist_resources.py

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