2021-01-15 02:26:06 +01:00
|
|
|
#! /usr/bin/python3
|
|
|
|
|
2021-04-18 15:26:00 +02:00
|
|
|
from typing import Dict, List, Union, Set, Any
|
2021-01-15 02:26:06 +01:00
|
|
|
import argparse
|
2021-04-13 09:47:52 +02:00
|
|
|
import pylibyaml
|
2021-01-15 02:26:06 +01:00
|
|
|
import yaml
|
|
|
|
import pickle
|
|
|
|
from colorama import Style, Fore
|
2021-04-13 09:47:52 +02:00
|
|
|
from segtypes.segment import Segment
|
|
|
|
from segtypes.linker_entry import LinkerWriter
|
2021-01-15 02:26:06 +01:00
|
|
|
from util import log
|
2021-03-23 03:29:47 +01:00
|
|
|
from util import options
|
2021-04-13 09:47:52 +02:00
|
|
|
from util import symbols
|
|
|
|
from util import palettes
|
2021-01-15 02:26:06 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
parser = argparse.ArgumentParser(description="Split a rom given a rom, a config, and output directory")
|
2021-01-15 02:26:06 +01:00
|
|
|
parser.add_argument("config", help="path to a compatible config .yaml file")
|
2021-04-13 09:47:52 +02:00
|
|
|
parser.add_argument("--target", help="path to a file to split (.z64 rom)")
|
|
|
|
parser.add_argument("--basedir", help="a directory in which to extract the rom")
|
2021-01-15 02:26:06 +01:00
|
|
|
parser.add_argument("--modes", nargs="+", default="all")
|
2021-04-13 09:47:52 +02:00
|
|
|
parser.add_argument("--verbose", action="store_true", help="Enable debug logging")
|
|
|
|
parser.add_argument("--use-cache", action="store_true", help="Only split changed segments in config")
|
2021-01-15 02:26:06 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
linker_writer: LinkerWriter
|
2021-04-18 15:26:00 +02:00
|
|
|
config: Dict[str, Any]
|
2021-01-15 02:26:06 +01:00
|
|
|
|
|
|
|
def fmt_size(size):
|
|
|
|
if size > 1000000:
|
|
|
|
return str(size // 1000000) + " MB"
|
|
|
|
elif size > 1000:
|
|
|
|
return str(size // 1000) + " KB"
|
|
|
|
else:
|
|
|
|
return str(size) + " B"
|
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
def initialize_segments(config_segments: Union[dict, list]) -> List[Segment]:
|
|
|
|
seen_segment_names: Set[str] = set()
|
2021-01-15 02:26:06 +01:00
|
|
|
ret = []
|
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
for i, seg_yaml in enumerate(config_segments):
|
|
|
|
# rompos marker
|
|
|
|
if isinstance(seg_yaml, list) and len(seg_yaml) == 1:
|
|
|
|
continue
|
2021-01-24 16:41:31 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
seg_type = Segment.parse_segment_type(seg_yaml)
|
2021-01-15 02:26:06 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
segment_class = Segment.get_class_for_type(seg_type)
|
|
|
|
|
|
|
|
this_start = Segment.parse_segment_start(seg_yaml)
|
|
|
|
next_start = Segment.parse_segment_start(config_segments[i + 1])
|
2021-01-15 02:26:06 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
segment: Segment = segment_class(seg_yaml, this_start, next_start)
|
2021-01-15 02:26:06 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
if segment.require_unique_name:
|
2021-01-15 02:26:06 +01:00
|
|
|
if segment.name in seen_segment_names:
|
2021-04-13 09:47:52 +02:00
|
|
|
log.error(f"segment name '{segment.name}' is not unique")
|
|
|
|
|
2021-01-15 02:26:06 +01:00
|
|
|
seen_segment_names.add(segment.name)
|
|
|
|
|
|
|
|
ret.append(segment)
|
|
|
|
|
|
|
|
return ret
|
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
def get_segment_symbols(segment, all_segments):
|
2021-02-03 16:09:01 +01:00
|
|
|
seg_syms = {}
|
|
|
|
other_syms = {}
|
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
for symbol in symbols.all_symbols:
|
|
|
|
if symbols.is_symbol_isolated(symbol, all_segments) and not symbol.rom:
|
2021-02-03 16:09:01 +01:00
|
|
|
if segment.contains_vram(symbol.vram_start):
|
|
|
|
if symbol.vram_start not in seg_syms:
|
|
|
|
seg_syms[symbol.vram_start] = []
|
|
|
|
seg_syms[symbol.vram_start].append(symbol)
|
|
|
|
else:
|
|
|
|
if symbol.vram_start not in other_syms:
|
|
|
|
other_syms[symbol.vram_start] = []
|
|
|
|
other_syms[symbol.vram_start].append(symbol)
|
|
|
|
else:
|
|
|
|
if symbol.rom and segment.contains_rom(symbol.rom):
|
|
|
|
if symbol.vram_start not in seg_syms:
|
|
|
|
seg_syms[symbol.vram_start] = []
|
|
|
|
seg_syms[symbol.vram_start].append(symbol)
|
|
|
|
else:
|
|
|
|
if symbol.vram_start not in other_syms:
|
|
|
|
other_syms[symbol.vram_start] = []
|
|
|
|
other_syms[symbol.vram_start].append(symbol)
|
|
|
|
|
|
|
|
return seg_syms, other_syms
|
2021-01-15 02:26:06 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
def do_statistics(seg_sizes, rom_bytes, seg_split, seg_cached):
|
|
|
|
unk_size = seg_sizes.get("unk", 0)
|
|
|
|
rest_size = 0
|
|
|
|
total_size = len(rom_bytes)
|
|
|
|
|
|
|
|
for typ in seg_sizes:
|
|
|
|
if typ != "unk":
|
|
|
|
rest_size += seg_sizes[typ]
|
|
|
|
|
|
|
|
known_ratio = rest_size / total_size
|
|
|
|
unk_ratio = unk_size / total_size
|
|
|
|
|
|
|
|
log.write(f"Split {fmt_size(rest_size)} ({known_ratio:.2%}) in defined segments")
|
|
|
|
for typ in seg_sizes:
|
|
|
|
if typ != "unk":
|
|
|
|
tmp_size = seg_sizes[typ]
|
|
|
|
tmp_ratio = tmp_size / total_size
|
|
|
|
log.write(f"{typ:>20}: {fmt_size(tmp_size):>8} ({tmp_ratio:.2%}) {Fore.GREEN}{seg_split[typ]} split{Style.RESET_ALL}, {Style.DIM}{seg_cached[typ]} cached")
|
|
|
|
log.write(f"{'unknown':>20}: {fmt_size(unk_size):>8} ({unk_ratio:.2%}) from unknown bin files")
|
|
|
|
|
|
|
|
def main(config_path, base_dir, target_path, modes, verbose, use_cache=True):
|
2021-04-18 15:26:00 +02:00
|
|
|
global config
|
|
|
|
|
2021-01-15 02:26:06 +01:00
|
|
|
# Load config
|
|
|
|
with open(config_path) as f:
|
2021-04-13 09:47:52 +02:00
|
|
|
config = yaml.load(f.read(), Loader=yaml.SafeLoader)
|
2021-01-15 02:26:06 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
options.initialize(config, config_path, base_dir, target_path)
|
2021-03-23 03:29:47 +01:00
|
|
|
options.set("modes", modes)
|
|
|
|
options.set("verbose", verbose)
|
2021-01-15 02:26:06 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
with options.get_target_path().open("rb") as f2:
|
|
|
|
rom_bytes = f2.read()
|
2021-02-08 11:57:56 +01:00
|
|
|
|
|
|
|
# Create main output dir
|
2021-04-13 09:47:52 +02:00
|
|
|
options.get_base_path().mkdir(parents=True, exist_ok=True)
|
2021-01-15 02:26:06 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
processed_segments: List[Segment] = []
|
2021-01-15 02:26:06 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
seg_sizes: Dict[str, int] = {}
|
|
|
|
seg_split: Dict[str, int] = {}
|
|
|
|
seg_cached: Dict[str, int] = {}
|
2021-01-15 02:26:06 +01:00
|
|
|
|
|
|
|
# Load cache
|
2021-04-13 09:47:52 +02:00
|
|
|
if use_cache:
|
|
|
|
try:
|
|
|
|
with options.get_cache_path().open("rb") as f3:
|
|
|
|
cache = pickle.load(f3)
|
|
|
|
|
|
|
|
if verbose:
|
|
|
|
log.write(f"Loaded cache ({len(cache.keys())} items)")
|
|
|
|
except Exception:
|
|
|
|
cache = {}
|
|
|
|
else:
|
2021-01-15 02:26:06 +01:00
|
|
|
cache = {}
|
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
# invalidate entire cache if options change
|
|
|
|
if use_cache and cache.get("__options__") != config.get("options"):
|
|
|
|
if verbose:
|
|
|
|
log.write("Options changed, invalidating cache")
|
|
|
|
|
|
|
|
cache = {
|
|
|
|
"__options__": config.get("options"),
|
|
|
|
}
|
|
|
|
|
2021-01-15 02:26:06 +01:00
|
|
|
# Initialize segments
|
2021-04-13 09:47:52 +02:00
|
|
|
all_segments = initialize_segments(config["segments"])
|
2021-01-15 02:26:06 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
# Load and process symbols
|
|
|
|
if options.mode_active("code"):
|
|
|
|
log.write("Loading and processing symbols")
|
|
|
|
symbols.initialize(all_segments)
|
|
|
|
|
|
|
|
# Resolve raster/palette siblings
|
|
|
|
if options.mode_active("img"):
|
|
|
|
palettes.initialize(all_segments)
|
2021-01-15 02:26:06 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
# Scan
|
|
|
|
log.write("Starting scan")
|
|
|
|
for segment in all_segments:
|
2021-02-08 11:57:56 +01:00
|
|
|
typ = segment.type
|
2021-01-15 02:26:06 +01:00
|
|
|
if segment.type == "bin" and segment.is_name_default():
|
2021-02-08 11:57:56 +01:00
|
|
|
typ = "unk"
|
2021-01-15 02:26:06 +01:00
|
|
|
|
2021-02-08 11:57:56 +01:00
|
|
|
if typ not in seg_sizes:
|
|
|
|
seg_sizes[typ] = 0
|
|
|
|
seg_split[typ] = 0
|
|
|
|
seg_cached[typ] = 0
|
2021-04-13 09:47:52 +02:00
|
|
|
seg_sizes[typ] += 0 if segment.size is None else segment.size
|
2021-01-15 02:26:06 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
if segment.should_scan():
|
|
|
|
# Check cache but don't write anything
|
|
|
|
if use_cache:
|
|
|
|
if segment.cache() == cache.get(segment.unique_id()):
|
|
|
|
continue
|
2021-01-15 02:26:06 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
if segment.needs_symbols:
|
|
|
|
segment_symbols, other_symbols = get_segment_symbols(segment, all_segments)
|
|
|
|
segment.given_seg_symbols = segment_symbols
|
|
|
|
segment.given_ext_symbols = other_symbols
|
2021-01-15 02:26:06 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
segment.did_run = True
|
|
|
|
segment.scan(rom_bytes)
|
2021-01-15 02:26:06 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
processed_segments.append(segment)
|
2021-01-15 02:26:06 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
seg_split[typ] += 1
|
2021-01-15 02:26:06 +01:00
|
|
|
|
|
|
|
log.dot(status=segment.status())
|
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
# Split
|
|
|
|
log.write("Starting split")
|
|
|
|
for segment in all_segments:
|
|
|
|
if use_cache:
|
|
|
|
cached = segment.cache()
|
2021-01-15 02:26:06 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
if cached == cache.get(segment.unique_id()):
|
|
|
|
# Cache hit
|
|
|
|
seg_cached[typ] += 1
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
# Cache miss; split
|
|
|
|
cache[segment.unique_id()] = cached
|
2021-02-03 16:09:01 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
if segment.should_split():
|
|
|
|
segment.split(rom_bytes)
|
2021-02-03 16:09:01 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
log.dot(status=segment.status())
|
|
|
|
|
|
|
|
if options.mode_active("ld"):
|
|
|
|
global linker_writer
|
|
|
|
linker_writer = LinkerWriter()
|
|
|
|
for segment in all_segments:
|
|
|
|
linker_writer.add(segment)
|
|
|
|
linker_writer.save_linker_script()
|
|
|
|
linker_writer.save_symbol_header()
|
|
|
|
|
|
|
|
# Write undefined_funcs_auto.txt
|
|
|
|
to_write = [s for s in symbols.all_symbols if s.referenced and not s.defined and not s.dead and s.type == "func"]
|
2021-01-15 02:26:06 +01:00
|
|
|
if len(to_write) > 0:
|
2021-04-13 09:47:52 +02:00
|
|
|
with open(options.get_undefined_funcs_auto_path(), "w", newline="\n") as f:
|
2021-02-03 16:09:01 +01:00
|
|
|
for symbol in to_write:
|
|
|
|
f.write(f"{symbol.name} = 0x{symbol.vram_start:X};\n")
|
2021-01-15 02:26:06 +01:00
|
|
|
|
|
|
|
# write undefined_syms_auto.txt
|
2021-04-13 09:47:52 +02:00
|
|
|
to_write = [s for s in symbols.all_symbols if s.referenced and not s.defined and not s.dead and not s.type == "func"]
|
2021-01-15 02:26:06 +01:00
|
|
|
if len(to_write) > 0:
|
2021-04-13 09:47:52 +02:00
|
|
|
with open(options.get_undefined_syms_auto_path(), "w", newline="\n") as f:
|
2021-02-03 16:09:01 +01:00
|
|
|
for symbol in to_write:
|
|
|
|
f.write(f"{symbol.name} = 0x{symbol.vram_start:X};\n")
|
2021-01-15 02:26:06 +01:00
|
|
|
|
2021-04-13 09:47:52 +02:00
|
|
|
# print warnings during split
|
2021-01-15 02:26:06 +01:00
|
|
|
for segment in all_segments:
|
2021-02-03 16:09:01 +01:00
|
|
|
if len(segment.warnings) > 0:
|
2021-01-15 02:26:06 +01:00
|
|
|
log.write(f"{Style.DIM}0x{segment.rom_start:06X}{Style.RESET_ALL} {segment.type} {Style.BRIGHT}{segment.name}{Style.RESET_ALL}:")
|
|
|
|
|
|
|
|
for warn in segment.warnings:
|
|
|
|
log.write("warning: " + warn, status="warn")
|
|
|
|
|
|
|
|
log.write("") # empty line
|
|
|
|
|
|
|
|
# Statistics
|
2021-04-13 09:47:52 +02:00
|
|
|
do_statistics(seg_sizes, rom_bytes, seg_split, seg_cached)
|
2021-01-15 02:26:06 +01:00
|
|
|
|
|
|
|
# Save cache
|
2021-04-13 09:47:52 +02:00
|
|
|
if cache != {} and use_cache:
|
2021-01-15 02:26:06 +01:00
|
|
|
if verbose:
|
2021-04-13 09:47:52 +02:00
|
|
|
log.write("Writing cache")
|
|
|
|
with open(options.get_cache_path(), "wb") as f4:
|
|
|
|
pickle.dump(cache, f4)
|
2021-01-15 02:26:06 +01:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
args = parser.parse_args()
|
2021-04-13 09:47:52 +02:00
|
|
|
main(args.config, args.basedir, args.target, args.modes, args.verbose, args.use_cache)
|