├── timg └── .gitignore ├── requirements.txt ├── .gitignore ├── mypy.ini ├── .vscode └── settings.json ├── CHANGELOG.md ├── README.md ├── .github └── workflows │ └── ci.yml ├── LICENSE └── lfgfx.py /timg/.gitignore: -------------------------------------------------------------------------------- 1 | *.png 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | argparse 2 | n64img 3 | pygfxd 4 | sty 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/configurationCache.log 2 | .vscode/dryrun.log 3 | .vscode/targets.log 4 | .vscode/launch.json 5 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.8 3 | 4 | [mypy-pygfxd.*] 5 | ignore_missing_imports = True 6 | 7 | [mypy-n64img.*] 8 | ignore_missing_imports = True 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black", 3 | "files.eol": "\n", 4 | "files.insertFinalNewline": true, 5 | "[python]": { 6 | "editor.formatOnSave": true, 7 | }, 8 | "python.linting.mypyEnabled": true, 9 | "python.linting.enabled": true, 10 | } 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.1 2 | * Fixed bug involving vram not being a global anymore, switching to thread local storage 3 | * Appended rom addr to symbol names 4 | 5 | ## 0.2.0 6 | * Splat mode initial poc 7 | * Fixed bug involving end of display list detection for non-f3dex2 targets 8 | * Python 3.8 compatibility 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LFGFX (Looking for GFX) 2 | lfgfx is a tool to help reverse engineers analyze blobs of graphics data from N64 games. 3 | 4 | image 5 | 6 | ### Currently supported graphics objects: 7 | * display list 8 | * vtx 9 | * texture image 10 | * palette 11 | 12 | with more on the way! 13 | 14 | ### Requirements 15 | Python 3.8 or newer 16 | `pip install -r requirements.txt` 17 | 18 | ## Usage 19 | ``` 20 | ./lfgfx.py ~/repos/papermario/assets/us/37ADD0.bin 0x09000000 21 | ``` 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | checks: 9 | runs-on: ubuntu-latest 10 | name: mypy / black 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v1 14 | 15 | - name: Set up Python 3.8 16 | uses: actions/setup-python@v1 17 | with: 18 | python-version: 3.8 19 | 20 | - name: Install Dependencies 21 | run: | 22 | pip install mypy 23 | pip install black 24 | pip install -r requirements.txt 25 | 26 | - name: mypy 27 | run: mypy --show-column-numbers --hide-error-context . 28 | 29 | - name: black 30 | run: black . 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ethan Roseman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lfgfx.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # lfgfx by Ethan Roseman (ethteck) 4 | 5 | import argparse 6 | from pathlib import Path 7 | from typing import Dict, List, Optional, Tuple, Type 8 | from sty import Style, fg # type: ignore 9 | import threading 10 | 11 | import n64img.image 12 | 13 | from pygfxd import GfxdMacroId 14 | from pygfxd import * # type: ignore 15 | 16 | DEBUG = True 17 | 18 | 19 | def debug(msg: str) -> None: 20 | if DEBUG: 21 | print(msg) 22 | 23 | 24 | class LFGFXLocal(threading.local): 25 | vram: int = 0 26 | found_objects: Dict[int, "Chunk"] = {} 27 | initialized: bool = False 28 | latest_macro: Optional[GfxdMacroId] = None 29 | 30 | def __init__(self, **kw): 31 | if self.initialized: 32 | raise SystemError("__init__ called too many times") 33 | self.initialized = True 34 | self.__dict__.update(kw) 35 | 36 | def add_found_object(self, obj: "Chunk"): 37 | if ( 38 | obj.start in self.found_objects 39 | and obj.type != self.found_objects[obj.start].type 40 | ): 41 | print( 42 | f"Duplicate objects found at 0x{obj.start:X}: {self.found_objects[obj.start]} and {obj} - ignoring the latter" 43 | ) 44 | else: 45 | self.found_objects[obj.start] = obj 46 | 47 | 48 | def auto_int(x): 49 | return int(x, 0) 50 | 51 | 52 | parser = argparse.ArgumentParser( 53 | description="Analyze the Gfx / display lists in a binary file" 54 | ) 55 | parser.add_argument("in_file", help="path to the input binary") 56 | parser.add_argument( 57 | "vram", 58 | help="vram address at the given offset (or beginning of file)", 59 | type=auto_int, 60 | ) 61 | parser.add_argument( 62 | "--mode", 63 | help="execution mode", 64 | choices=["simple", "splat"], 65 | default="simple", 66 | ) 67 | parser.add_argument( 68 | "--start", help="start offset into the input file", default=0, type=auto_int 69 | ) 70 | parser.add_argument( 71 | "--end", 72 | help="end offset into the input file. defaults to the end of the file", 73 | type=auto_int, 74 | ) 75 | parser.add_argument( 76 | "--gfx-target", 77 | help="gfx target to use", 78 | choices=["f3d", "f3db", "f3dex", "f3dexb", "f3dex2"], 79 | default="f3dex2", 80 | ) 81 | parser.add_argument( 82 | "--splat-rom-offset", 83 | help="start rom offset for this segment (for use with splat mode)", 84 | type=auto_int, 85 | ) 86 | parser.add_argument( 87 | "--known-gfx", 88 | help="list of file offsets where known display lists begin", 89 | action="extend", 90 | nargs="+", 91 | type=auto_int, 92 | ) 93 | 94 | 95 | class Chunk: 96 | start: int 97 | end: int 98 | type: str = "unmapped" 99 | splat_type: str = "type" 100 | display_color: Optional[Style] = None 101 | has_splat_extension: bool = False 102 | file_ext: str = "inc.c" 103 | 104 | def __init__(self, start: int, end: int): 105 | self.start = start 106 | self.end = end 107 | 108 | def __repr__(self): 109 | return f"{self.type}: {self.start:X}-{self.end:X}" 110 | 111 | @property 112 | def addr(self): 113 | return thread_ctx.vram + self.start 114 | 115 | def type_color(self): 116 | if self.display_color: 117 | color = self.display_color 118 | elif self.type == "padding": 119 | color = fg.da_grey 120 | elif self.type == "unmapped": 121 | color = fg.li_red 122 | else: 123 | color = fg.white 124 | 125 | return color + self.type + fg.rs 126 | 127 | def symbol_name(self, rom_offset: int): 128 | return f"D_{self.addr:08X}_{rom_offset + self.start:06X}" 129 | 130 | def to_yaml(self, rom_offset): 131 | if self.has_splat_extension: 132 | return f"- [0x{rom_offset + self.start:X}, {self.splat_type}, {self.symbol_name(rom_offset)}]\n" 133 | else: 134 | return f"- [0x{rom_offset + self.start:X}] # {self.type}\n" 135 | 136 | def to_c(self, data: bytes, rom_offset: int): 137 | if self.has_splat_extension: 138 | return f'#include "effects/gfx/{self.symbol_name(rom_offset)}{self.file_ext}"\n' 139 | else: 140 | raw_c = f"u8 {self.symbol_name(rom_offset)}[] = " + "{\n" 141 | raw_c += " " + ", ".join(f"0x{x:X}" for x in data[self.start : self.end]) 142 | raw_c += "\n};\n" 143 | 144 | return raw_c 145 | 146 | def raw_chunk(self): 147 | return f"{self.start:X} - {self.end:X}" 148 | 149 | def __str__(self): 150 | ret = f"{self.type_color()}: 0x{self.start:X} - 0x{self.end:X}" 151 | 152 | if self.type == "unmapped": 153 | ret += f" (0x{self.end - self.start:X} bytes)" 154 | 155 | return ret 156 | 157 | 158 | class Tlut(Chunk): 159 | type: str = "tlut" 160 | splat_type: str = "palette" 161 | display_color: Style = fg.li_green 162 | count: int 163 | has_splat_extension: bool = True 164 | file_ext: str = ".pal.inc.c" 165 | 166 | def __init__(self, start: int, end: int, idx: int, count: int): 167 | super().__init__(start, end) 168 | self.idx = idx 169 | self.count = count 170 | 171 | 172 | class Timg(Chunk): 173 | type: str = "timg" 174 | display_color: Style = fg.li_cyan 175 | fmt: str 176 | size: int 177 | width: int 178 | height: int 179 | has_splat_extension: bool = True 180 | file_ext: str = ".png.inc.c" 181 | 182 | @property 183 | def splat_type(self): 184 | if self.fmt == 0: 185 | if self.size == 2: 186 | return "rgba16" 187 | elif self.size == 3: 188 | return "rgba32" 189 | elif self.fmt == 2: 190 | if self.size == 0: 191 | return "ci4" 192 | elif self.size == 1: 193 | return "ci8" 194 | elif self.fmt == 3: 195 | if self.size == 0: 196 | return "ia4" 197 | elif self.size == 1: 198 | return "ia8" 199 | elif self.size == 2: 200 | return "ia16" 201 | elif self.fmt == 4: 202 | if self.size == 0: 203 | return "i4" 204 | elif self.size == 1: 205 | return "i8" 206 | raise RuntimeError(f"Unknown format / size {self.fmt}, {self.size}") 207 | 208 | def resize(self, new_end: int): 209 | self.end = new_end 210 | self.height = (self.end - self.start) // self.width * (2 - self.size) 211 | 212 | def to_file(self, data: bytes) -> None: 213 | outname = Path("timg") / f"{self.start:X}.png" 214 | 215 | imgcls: Optional[Type[n64img.image.Image]] = None 216 | if self.splat_type == "rgba16": 217 | imgcls = n64img.image.RGBA16 218 | elif self.splat_type == "rgba32": 219 | imgcls = n64img.image.RGBA32 220 | elif self.splat_type == "ci4": 221 | imgcls = n64img.image.CI4 222 | elif self.splat_type == "ci8": 223 | imgcls = n64img.image.CI8 224 | elif self.splat_type == "ia4": 225 | imgcls = n64img.image.IA4 226 | elif self.splat_type == "ia8": 227 | imgcls = n64img.image.IA8 228 | elif self.splat_type == "ia16": 229 | imgcls = n64img.image.IA16 230 | elif self.splat_type == "i4": 231 | imgcls = n64img.image.I4 232 | elif self.splat_type == "i8": 233 | imgcls = n64img.image.I8 234 | 235 | if imgcls is None: 236 | raise RuntimeError(f"Unknown format / size {self.fmt}, {self.size}") 237 | 238 | imgcls(data[self.start : self.end], self.width, self.height).write(outname) 239 | 240 | def to_yaml(self, rom_offset): 241 | return f"- [0x{rom_offset + self.start:X}, {self.splat_type}, {self.symbol_name(rom_offset)}, {self.width}, {self.height}]\n" 242 | 243 | def __init__( 244 | self, 245 | start: int, 246 | end: int, 247 | fmt: str, 248 | size: int, 249 | width: int, 250 | height: int, 251 | ): 252 | super().__init__(start, end) 253 | self.fmt = fmt 254 | self.size = size 255 | self.width = width 256 | self.height = height 257 | 258 | 259 | class Dlist(Chunk): 260 | type: str = "dlist" 261 | splat_type: str = "gfx" 262 | display_color: Style = fg.li_blue 263 | has_splat_extension: bool = True 264 | file_ext: str = ".gfx.inc.c" 265 | 266 | 267 | class Vtx(Chunk): 268 | type: str = "vtx" 269 | splat_type: str = "vtx" 270 | display_color: Style = fg.li_green 271 | count: int 272 | has_splat_extension: bool = True 273 | file_ext: str = ".vtx.inc.c" 274 | 275 | def __init__(self, start: int, end: int, count: int): 276 | super().__init__(start, end) 277 | self.count = count 278 | 279 | 280 | def macro_fn(): 281 | gfxd_puts(" ") 282 | gfxd_macro_dflt() 283 | gfxd_puts(",\n") 284 | 285 | thread_ctx.latest_macro = gfxd_macro_id() 286 | return 0 287 | 288 | 289 | def tlut_handler(addr, idx, count): 290 | gfxd_printf(f"D_{addr:08X}") 291 | 292 | start = addr - thread_ctx.vram 293 | end = start + count * 2 294 | tlut = Tlut(start, end, idx, count) 295 | thread_ctx.add_found_object(tlut) 296 | return 1 297 | 298 | 299 | def timg_handler(addr, fmt, size, width, height, pal): 300 | gfxd_printf(f"D_{addr:08X}") 301 | 302 | if height == 0 or height == -1: 303 | # Guess height 304 | height = width 305 | 306 | num_bytes = width * height 307 | 308 | # Too small 309 | if num_bytes < 8: 310 | return 0 311 | 312 | if size == 0: 313 | num_bytes /= 2 314 | elif size == 1: 315 | num_bytes *= 1 316 | elif size == 2: 317 | num_bytes *= 2 318 | elif size == 3: 319 | num_bytes *= 4 320 | else: 321 | print(f"Unknown timg size format {size}") 322 | return 0 323 | 324 | start = addr - thread_ctx.vram 325 | 326 | if start < 0: 327 | return 0 328 | 329 | end = int(start + num_bytes) 330 | timg = Timg(start, end, fmt, size, width, height) 331 | thread_ctx.add_found_object(timg) 332 | return 1 333 | 334 | 335 | def cimg_handler(addr, fmt, size, width): 336 | gfxd_printf(f"D_{addr:08X}") 337 | 338 | if addr < 0xFFFFFFFF: 339 | pass 340 | # print(f"cimg at 0x{addr:08X}, fmt {fmt}, size {size}, width {width}") 341 | return 1 342 | 343 | 344 | def zimg_handler(addr): 345 | gfxd_printf(f"D_{addr:08X}") 346 | print(f"zimg at 0x{addr:08X}") 347 | return 1 348 | 349 | 350 | def dl_handler(addr): 351 | gfxd_printf(f"D_{addr:08X}") 352 | start = addr - thread_ctx.vram 353 | thread_ctx.add_found_object(Dlist(start, start)) 354 | return 1 355 | 356 | 357 | def mtx_handler(addr): 358 | gfxd_printf(f"D_{addr:08X}") 359 | print(f"mtx at 0x{addr:08X}") 360 | return 1 361 | 362 | 363 | def lookat_handler(addr, count): 364 | gfxd_printf(f"D_{addr:08X}") 365 | print(f"lookat at 0x{addr:08X}, count {count}") 366 | return 1 367 | 368 | 369 | def light_handler(addr, count): 370 | gfxd_printf(f"D_{addr:08X}") 371 | print(f"light at 0x{addr:08X}, count {count}") 372 | return 1 373 | 374 | 375 | def vtx_handler(addr, count): 376 | gfxd_printf(f"D_{addr:08X}") 377 | 378 | start = addr - thread_ctx.vram 379 | end = start + count * 0x10 380 | vtx = Vtx(start, end, count) 381 | thread_ctx.add_found_object(vtx) 382 | return 1 383 | 384 | 385 | def vp_handler(addr): 386 | gfxd_printf(f"D_{addr:08X}") 387 | print(f"vp at 0x{addr:08X}") 388 | return 1 389 | 390 | 391 | def gfxd_scan_bytes(data: bytes) -> int: 392 | gfxd_input_buffer(data) # type: ignore 393 | return gfxd_execute() # type: ignore 394 | 395 | 396 | def is_bad_command(data: bytes) -> bool: 397 | gfxd_scan_bytes(data) 398 | 399 | if thread_ctx.latest_macro == GfxdMacroId.DPNoOp: 400 | return True 401 | 402 | if thread_ctx.latest_macro == GfxdMacroId.BranchZ: 403 | return True 404 | 405 | if thread_ctx.latest_macro == GfxdMacroId.SPModifyVertex: 406 | if data[0] == 0x02: 407 | if int.from_bytes(data[2:4], byteorder="big") > 79: 408 | return True 409 | 410 | return False 411 | 412 | 413 | def valid_dlist(data: bytes) -> int: 414 | return gfxd_scan_bytes(data) != -1 # type: ignore 415 | 416 | 417 | def find_earliest_start( 418 | data: bytes, min: int, end: int, known_dlists: List[int] 419 | ) -> int: 420 | for i in range(end - 8, min, -8): 421 | if i in known_dlists: 422 | # scan the first command since it may reference an object 423 | gfxd_scan_bytes(data[i : i + 8]) 424 | return i 425 | if is_bad_command(data[i : i + 8]) or not valid_dlist(data[i:end]): 426 | return i + 8 427 | if i == min + 8: 428 | # We know the dlist starts at min 429 | # scan the first command since it may reference an object 430 | gfxd_scan_bytes(data[min : min + 8]) 431 | return min 432 | 433 | 434 | def get_end_dlist_cmd(gfx_target): 435 | if gfx_target == gfxd_f3dex2: 436 | return b"\xDF\x00\x00\x00\x00\x00\x00\x00" 437 | else: 438 | return b"\xB8\x00\x00\x00\x00\x00\x00\x00" 439 | 440 | 441 | def collect_dlists(data: bytes, gfx_target, known_dlists: List[int]) -> List[Dlist]: 442 | ret: List[Dlist] = [] 443 | ends: List[int] = [] 444 | 445 | for i in range(0, len(data), 8): 446 | if data[i : i + 8] == get_end_dlist_cmd(gfx_target): 447 | ends.append(i + 8) 448 | 449 | min = 0 450 | for end in ends: 451 | start = find_earliest_start(data, min, end, known_dlists) 452 | ret.append(Dlist(start, end)) 453 | min = end 454 | 455 | return ret 456 | 457 | 458 | def is_zeros(data: bytes) -> bool: 459 | for i in range(len(data)): 460 | if data[i] != 0: 461 | return False 462 | return True 463 | 464 | 465 | def pygfxd_init(target): 466 | gfxd_target(target) 467 | gfxd_macro_fn(macro_fn) 468 | 469 | # callbacks 470 | gfxd_tlut_callback(tlut_handler) 471 | gfxd_timg_callback(timg_handler) 472 | gfxd_cimg_callback(cimg_handler) # TODO 473 | gfxd_zimg_callback(zimg_handler) # TODO 474 | gfxd_dl_callback(dl_handler) 475 | gfxd_mtx_callback(mtx_handler) # TODO 476 | gfxd_lookat_callback(lookat_handler) # TODO 477 | gfxd_light_callback(light_handler) # TODO 478 | # gfxd_seg_callback ? 479 | gfxd_vtx_callback(vtx_handler) 480 | gfxd_vp_callback(vp_handler) # TODO 481 | # gfxd_uctext_callback ? 482 | # gfxd_ucdata_callback ? 483 | # gfxd_dram_callback ? 484 | 485 | 486 | def target_arg_to_object(arg: str): 487 | if arg == "f3d": 488 | return gfxd_f3d # type: ignore 489 | elif arg == "f3db": 490 | return gfxd_f3db # type: ignore 491 | elif arg == "f3dex": 492 | return gfxd_f3dex # type: ignore 493 | elif arg == "f3dexb": 494 | return gfxd_f3dexb # type: ignore 495 | elif arg == "f3dex2": 496 | return gfxd_f3dex2 # type: ignore 497 | else: 498 | raise RuntimeError(f"Unknown target {arg}") 499 | 500 | 501 | def splat_chunks(chunks: List[Chunk], data: bytes, rom_offset: int) -> Tuple[str, str]: 502 | yaml = "" 503 | c_code = "" 504 | 505 | empty_line = True 506 | for i, chunk in enumerate(chunks): 507 | yaml += chunk.to_yaml(rom_offset) 508 | 509 | if not chunk.has_splat_extension and not empty_line: 510 | c_code += "\n" 511 | 512 | c_code += chunk.to_c(data, rom_offset) 513 | 514 | if chunk.has_splat_extension: 515 | empty_line = False 516 | else: 517 | c_code += "\n" 518 | empty_line = True 519 | 520 | return yaml, c_code 521 | 522 | 523 | def scan_binary(data: bytes, vram, gfx_target, known_dlists: List[int]) -> List[Chunk]: 524 | pygfxd_init(gfx_target) 525 | 526 | thread_ctx.found_objects.clear() 527 | thread_ctx.vram = vram 528 | 529 | chunks: List[Chunk] = [] 530 | 531 | # dlists 532 | dlists: List[Dlist] = collect_dlists(data, gfx_target, known_dlists) 533 | 534 | chunks.extend(dlists) 535 | chunks.extend(thread_ctx.found_objects.values()) 536 | 537 | chunks.sort(key=lambda x: x.start) 538 | 539 | # Truncate things as needed 540 | for i, chunk in enumerate(chunks): 541 | if i < len(chunks) - 1 and chunk.end > chunks[i + 1].start: 542 | if isinstance(chunk, Timg): 543 | # Allow truncation of images 544 | debug(f"Truncating {chunk} to end at 0x{chunks[i + 1].start:X}") 545 | chunk.resize(chunks[i + 1].start) 546 | elif isinstance(chunk, Tlut): 547 | # Allow truncation of palettes 548 | debug(f"Truncating {chunk} to end at 0x{chunks[i + 1].start:X}") 549 | chunk.end = chunks[i + 1].start 550 | elif isinstance(chunk, Vtx): 551 | # TODO conditionally truncate vtx blobs 552 | # raise RuntimeError("Vtx chunk overlap!") 553 | pass 554 | # # Allow truncation of vtx blobs under certain conditions 555 | # if (chunks[i + 1].start - chunk.start) % 0x10 == 0: 556 | # debug(f"Truncating {chunk} to end at 0x{chunks[i + 1].start:X}") 557 | # chunk.end = chunks[i + 1].start 558 | # else: 559 | # raise RuntimeError(f"Cannot truncate {chunk} to end at 0x{chunks[i + 1].start:X} - not aligned to 0x10 bytes!") 560 | else: 561 | # raise RuntimeError("Chunk overlap!") 562 | pass 563 | 564 | # Fill gaps 565 | to_add: List[Chunk] = [] 566 | for i, chunk in enumerate(chunks): 567 | if i < len(chunks) - 1 and chunk.end < chunks[i + 1].start: 568 | to_add.append(Chunk(chunk.end, chunks[i + 1].start)) 569 | chunks.extend(to_add) 570 | chunks.sort(key=lambda x: x.start) 571 | 572 | # Add a chunk for the beginning of the file 573 | if len(chunks) == 0: 574 | chunks.insert(0, Chunk(0, len(data))) 575 | elif chunks[0].start != 0: 576 | chunks.insert(0, Chunk(0, chunks[0].start)) 577 | 578 | # Add a chunk for the rest of the file 579 | end_pos = chunks[len(chunks) - 1].end 580 | if end_pos < len(data): 581 | chunks.append(Chunk(end_pos, len(data))) 582 | 583 | # Remove chunks that are empty 584 | chunks = [chunk for chunk in chunks if chunk.start < chunk.end] 585 | 586 | # Mark chunks that are filled with 0s as padding 587 | for chunk in chunks: 588 | if chunk.type == "unmapped" and is_zeros(data[chunk.start : chunk.end]): 589 | chunk.type = "padding" 590 | 591 | return chunks 592 | 593 | 594 | def main(args): 595 | with open(args.in_file, "rb") as f: 596 | input_bytes = f.read() 597 | 598 | start = args.start 599 | end = len(input_bytes) if args.end is None else args.end 600 | gfx_target = target_arg_to_object(args.gfx_target) 601 | 602 | print(f"Scanning input binary {args.in_file} from 0x{start:X} to 0x{end:X}") 603 | 604 | chunks = scan_binary(input_bytes[start:end], args.vram, gfx_target, args.known_gfx) 605 | 606 | if args.mode == "simple": 607 | for chunk in chunks: 608 | print(chunk) 609 | 610 | if isinstance(chunk, Timg): 611 | chunk.to_file(input_bytes) 612 | 613 | elif args.mode == "splat": 614 | if args.splat_rom_offset is None: 615 | raise RuntimeError("Must specify --splat-rom-offset with splat mode") 616 | splat_yaml, c_code = splat_chunks(chunks, input_bytes, args.splat_rom_offset) 617 | print("\nSplat yaml:\n") 618 | print(splat_yaml) 619 | print("C code:\n") 620 | print(c_code) 621 | else: 622 | raise RuntimeError(f"Unsupported mode {args.mode}") 623 | 624 | 625 | thread_ctx = LFGFXLocal() 626 | 627 | if __name__ == "__main__": 628 | args = parser.parse_args() 629 | main(args) 630 | --------------------------------------------------------------------------------