From a0e610f51b77ec3d2e0ecca2f4ef9ec3dae53969 Mon Sep 17 00:00:00 2001 From: RetroGameDeveloper <40120498+RetroGameDeveloper@users.noreply.github.com> Date: Fri, 23 Oct 2026 21:56:26 +0100 Subject: [PATCH 1/2] Preserve explicit BSS ranges when discovering N64 code --- src/decompnet/consoles/n64/code.py | 14 ++++++++++++ src/decompnet/rom/export.py | 9 +++++++- tests/test_n64code.py | 35 ++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/decompnet/consoles/n64/code.py b/src/decompnet/consoles/n64/code.py index a778a94f2..8626360d1 100644 --- a/src/decompnet/consoles/n64/code.py +++ b/src/decompnet/consoles/n64/code.py @@ -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 @@ -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 diff --git a/src/decompnet/rom/export.py b/src/decompnet/rom/export.py index 55e756ebf..a923f9217 100644 --- a/src/decompnet/rom/export.py +++ b/src/decompnet/rom/export.py @@ -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 diff --git a/tests/test_n64code.py b/tests/test_n64code.py index 401f4a76a..c2a2ce1c5 100644 --- a/tests/test_n64code.py +++ b/tests/test_n64code.py @@ -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 From 72cac6c9df215754aae698283cafc68b3ed26bb3 Mon Sep 17 00:00:00 2001 From: RetroGameDeveloper <40120498+RetroGameDeveloper@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:00:19 +0100 Subject: [PATCH 2/2] Retain known startup aliases in standalone ROM exports --- src/decompnet/rom/export.py | 7 ++++++- tests/test_export.py | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/decompnet/rom/export.py b/src/decompnet/rom/export.py index a923f9217..9cde5b60d 100644 --- a/src/decompnet/rom/export.py +++ b/src/decompnet/rom/export.py @@ -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") @@ -259,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(): diff --git a/tests/test_export.py b/tests/test_export.py index 513c22503..ab45eb77e 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -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