Files
psxrecomp/docs/COUNTED_MIPS_RELOCATIONS.md

3.9 KiB

Counted MIPS relocation containers

tools/counted_mips_relocations.py reads image containers with this layout:

Area Little-endian words
Header Member count
Records Image offset, image size, relocation-stream offset for each member
Images Concatenated image bytes
Streams Two-bit tagged relocation words, each stream ending in 0xFFFFFFFF

Image offsets are relative to the image area. Stream offsets are relative to the area following the sum of the image sizes. Both areas must partition exactly; overlapping members, invalid targets, duplicate writes, missing terminators and unaccounted bytes are rejected.

The tag's low two bits select the operation; the remaining bits are the aligned image offset. Offset zero is valid. Kind 1 consumes an additional addend word.

Kind Operation
0 Add the image's RAM base to an absolute 32-bit word
1 Preserve the instruction's upper half; insert (base + addend + 0x8000) >> 16, with 32-bit wrapping
2 Preserve the instruction's upper half; add the base to its low half
3 Preserve the opcode; relocate the masked 26-bit jump target
from counted_mips_relocations import parse, relocate

members = parse(container_bytes)
image = relocate(members[0], independently_verified_image_base)

This is a separate contract from mips_tagged_relocations.py: that format's leading image-size word and full-instruction jump addition do not describe these containers. Neither reader decompresses data.

The container has no destination addresses. relocate takes an aligned KSEG0 main-RAM image base; this is not the allocation base of a container whose headers remain in memory. Callers must establish placement and executable/data boundaries from the original loader before constructing native AOT producers. The original-disc AOT pipeline accepts counted_relocated_members with containers: [{file, sha256, members: [...]}]. Each member pins its index, image_size, relocation_count and nonempty placements: [{load_addr, ...}]. Every member must be classified, including an explicit reason for exclusions. The placement is the image address, which can differ from the container's allocation address by the header size. Native emission uses relocated original bytes and the normal guarded dispatcher. A newly encountered heap placement continues through the interpreter until its own native variant is established.

entry_pointer_offsets declares image-relative callback fields proven by the loader's consumers. Reading those fields after relocation lets the same export declaration apply at multiple bases. Null callbacks are ignored; unaligned, out-of-image and excluded required entries fail extraction. Script pointers and other data fields must not be nominated as MIPS entry points merely because they look like addresses.

The generic disc discoverer now recognizes complete counted containers and writes <output>.relocatable.json alongside its positioned recipes. The sidecar records original hashes, member extents, sizes and relocation counts with placement_required: true. It does not infer a heap address or claim native coverage. This exposes unresolved loader work during initial discovery instead of waiting for gameplay to reveal interpreter callbacks.

The implementation was compared byte for byte with the owned USA MediEvil II relocation routine (0x80078328) for 24 modules at two distinct RAM bases: 48 complete-image comparisons passed. Owned bytes and diagnostic captures are not part of this repository. Synthetic contract tests cover multiple members, all four relocation kinds, opcode preservation and malformed inputs:

python -m unittest discover -s tools/tests -p test_counted_mips_relocations.py
python -m unittest discover -s tools/tests -p test_counted_relocation_discovery.py
python -m unittest discover -s tools/tests -p test_aot_overlay_pipeline.py