Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/decompnet/consoles/n64/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,15 @@ def add_segments(config: Path, workdir: Path, image: str, log=print) -> str:
def start_of(seg):
return seg[0] if isinstance(seg, list) else seg.get("start")

# splat measures a parent's VRAM end as ROM length + bss_size. New code segments can split its data
# tail: remember that end so the explicit BSS still describes the same addresses after the split.
bss_ends = {}
for seg in split.segments_from_config(config):
raw = next(s for s in cfg["segments"] if isinstance(s, dict) and s.get("start") == seg.rom_start)
if raw.get("bss_size") and any(isinstance(sub, dict) and sub.get("type") == "bss" and "vram" in sub
for sub in raw.get("subsegments", [])):
bss_ends[seg.rom_start] = seg.vram + seg.rom_end - seg.rom_start + raw["bss_size"]

starts = {st for st, _, _ in found}
segs = [s for s in cfg["segments"] if not (isinstance(s, dict) and s.get("type") == "bin" and start_of(s) in starts)
and not isinstance(s, list)] # the end marker comes back at the image's end
Expand All @@ -278,6 +287,11 @@ def start_of(seg):
segs.append({"type": "bin", "start": en})
segs.sort(key=lambda s: start_of(s) if start_of(s) is not None else -1)
segs.append([len(rom)])
for i, seg in enumerate(segs[:-1]):
start = start_of(seg)
if start in bss_ends:
end = next(start_of(s) for s in segs[i + 1:] if start_of(s) is not None)
seg["bss_size"] = bss_ends[start] - seg["vram"] - (end - start)
cfg["segments"] = segs
config.write_text(yaml.safe_dump(cfg, sort_keys=False, default_flow_style=None), encoding="utf-8")
kib = sum(en - st for st, en, _ in found) // 1024
Expand Down
16 changes: 14 additions & 2 deletions src/decompnet/rom/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from ..hostenv import NO_WINDOW, console_python
from ..limits import capped
from ..project import compiler_for
from . import match
from . import match, split
from .symbols import AUTO_NAME

_ADDR_COMMENT = re.compile(r"/\*\s*([0-9A-F]+)\s+([0-9A-F]{8})\b")
Expand Down Expand Up @@ -90,7 +90,14 @@ def _segments(config: dict) -> list[dict]:
if len(seg) == 1:
continue # the end marker
seg = {"start": seg[0], "type": seg[1], "name": seg[2] if len(seg) > 2 else f"{seg[0]:X}"}
out.append(dict(seg) | {"start": start, "end": end})
seg = dict(seg) | {"start": start, "end": end}
# splat's parent size spans the address gap left by newly discovered code segments. Export only the
# actual BSS extent, whose start may be well beyond this parent's shortened ROM-backed data.
bss_vram = next((sub["vram"] for sub in seg.get("subsegments", [])
if isinstance(sub, dict) and sub.get("type") == "bss" and "vram" in sub), None)
if bss_vram is not None and end is not None and "vram" in seg:
seg["bss_size"] = seg["vram"] + end - start + int(seg.get("bss_size") or 0) - bss_vram
out.append(seg)
return out


Expand Down Expand Up @@ -252,6 +259,11 @@ def export_rom(net, slug: str, out: Path, console, log=print) -> dict:
"pieces": sorted(data, key=lambda d: d["vram"]), "bss": sorted(bss, key=lambda d: d["vram"])})
syms = net.symbols(proj["id"])
absolute = {s.name: s.address for s in syms.by_name.values() if s.name not in defined}
# Startup assembly can retain an import-time alias after a discovered function receives an address-based
# name. Keep those known addresses too; otherwise its named jump target is undefined in the standalone link.
for name, address in split.named_symbols(work).items():
if name not in defined:
absolute.setdefault(name, address)
for auto in ("undefined_syms_auto.txt", "undefined_funcs_auto.txt"):
p = out / auto
if p.exists():
Expand Down
8 changes: 8 additions & 0 deletions tests/test_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,18 @@ def test_export_rebuilds_the_game(tmp_path, platform):
contents = yaml.safe_load(config.read_text())
contents["segments"][0]["name"] = "123"
config.write_text(yaml.safe_dump(contents, sort_keys=False))
# Startup code may refer to an import-time alias that is absent from the current symbol table.
# The alias still has a fixed address in the original splat symbol file.
with (Path(net._project(slug)["root"]) / "symbol_addrs.txt").open("a") as stream:
stream.write("\nstartup_alias = 0x80000400; // type:func\n")
out = net.export_rom(slug, log=lambda *_: None)
assert out["identical"], out["log"]
assert out["matched"] == len(FUNCTIONS) and not out["fallback"]
tree = Path(out["path"])
if platform == "n64":
import json

assert json.loads((tree / "build.json").read_text())["symbols"]["startup_alias"] == 0x80000400
assert len(list((tree / "src").glob("*/*.c"))) == len(FUNCTIONS)

# a fresh clone: no assembly, assets or build output, only the sources and your copy of the game
Expand Down
35 changes: 35 additions & 0 deletions tests/test_n64code.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,38 @@ def test_yaz0():
# "abcabcabc": three literals, then a back-reference 3 back, 6 long (0x40 >> 4 = 4, + 2)
data = b"Yaz0" + (9).to_bytes(4, "big") + bytes(8) + bytes([0b11100000]) + b"abc" + bytes([0x40, 0x02])
assert yaz0(data) == b"abcabcabc"


def test_discovered_code_preserves_the_original_explicit_bss_extent(tmp_path, monkeypatch):
import yaml
from splat.scripts.split import initialize_segments
from splat.util import options

from decompnet.consoles.n64 import code
from decompnet.rom.export import _segments

# The original main range includes data and code later split into their own segments. Its explicit BSS
# still starts after all that ROM-backed memory, not after the first newly discovered code segment.
config = {"name": "synthetic", "options": {"platform": "n64", "basename": "synthetic",
"base_path": str(tmp_path), "target_path": "rom.z64", "compiler": "GCC"}, "segments": [
{"name": "main", "type": "code", "start": 0, "vram": 0x80000400, "bss_size": 0x80,
"subsegments": [[0, "asm"], [0x40, "data"], {"type": "bss", "vram": 0x80001400}]},
{"type": "bin", "start": 0x1000}, [0x2000]]}
path = tmp_path / "synthetic.yaml"
path.write_text(yaml.safe_dump(config, sort_keys=False))
(tmp_path / "rom.z64").write_bytes(bytes(0x2000))
monkeypatch.setattr(code, "unpack_code", lambda _: [])
monkeypatch.setattr(code, "find_segments", lambda *_: [(0x200, 0x400, 0x80000600)])
code.add_segments(path, tmp_path, "rom.z64", log=lambda _: None)
changed = yaml.safe_load(path.read_text())
options.initialize(changed, [path])
main = initialize_segments(changed["segments"])[0]
bss = next(s for s in main.subsegments if s.type == "bss")
assert (main.rom_end, main.vram_end) == (0x200, 0x80001480)
assert (bss.vram_start, bss.vram_end) == (0x80001400, 0x80001480)
exported = _segments(changed)[0]
assert exported["bss_size"] == 0x80
assert changed["segments"][0]["bss_size"] == 0xE80 # export does not mutate splat's parent extent
# Repeating discovery must not grow the BSS a second time.
code.add_segments(path, tmp_path, "rom.z64", log=lambda _: None)
assert yaml.safe_load(path.read_text())["segments"][0]["bss_size"] == 0xE80
Loading