From de69d6280038dffd75c6d2344b0b6ca2d9de86ca Mon Sep 17 00:00:00 2001 From: Derek Hensley Date: Fri, 17 May 2024 06:00:45 -0700 Subject: [PATCH] New Romheader format (#1628) --- Makefile | 2 +- include/rom_header.h | 145 +++++++++++++++++++++++++++++++++++++++++ tools/disasm/disasm.py | 66 ++++++++++++++----- 3 files changed, 196 insertions(+), 17 deletions(-) create mode 100644 include/rom_header.h diff --git a/Makefile b/Makefile index 113be97f90..be9071dee4 100644 --- a/Makefile +++ b/Makefile @@ -417,7 +417,7 @@ $(BUILD_DIR)/src/boot/z_std_dma.o: $(BUILD_DIR)/dmadata_table_spec.h $(BUILD_DIR)/src/dmadata/dmadata.o: $(BUILD_DIR)/dmadata_table_spec.h $(BUILD_DIR)/asm/%.o: asm/%.s - $(AS) $(ASFLAGS) $< -o $@ + $(CPP) $(CPPFLAGS) -Iinclude $< | $(AS) $(ASFLAGS) -o $@ $(BUILD_DIR)/assets/%.o: assets/%.c $(CC) -c $(CFLAGS) $(MIPS_VERSION) $(OPTFLAGS) -o $@ $< diff --git a/include/rom_header.h b/include/rom_header.h new file mode 100644 index 0000000000..1069ae86fe --- /dev/null +++ b/include/rom_header.h @@ -0,0 +1,145 @@ +#ifndef ROM_HEADER_H +#define ROM_HEADER_H + +/* Storage medium IDs, used internally in MEDIUM below */ + +#define STORAGE_MEDIUM_CARTRIDGE "N" +#define STORAGE_MEDIUM_CARTRIDGE_EXPANDABLE "C" +#define STORAGE_MEDIUM_DISK "D" +#define STORAGE_MEDIUM_DISK_EXPANSION "E" + +/* Region IDs, used internally in REGION below */ + +#define REGION_CODE_ALL "A" +#define REGION_CODE_JP "J" +#define REGION_CODE_US "E" +#define REGION_CODE_PAL "P" +#define REGION_CODE_GATEWAY "G" +#define REGION_CODE_LODGENET "L" + +/** + * Magic value to determine if the ROM is byteswapped. + * + * This is not totally reliable since the PI Domain 1 Latency could also hold this value, however generally it can be + * assumed that this is not the case. + */ +#define ENDIAN_IDENTIFIER \ + .byte 0x80 + +/** + * Configures the timings for PI Domain 1. This determines how fast the PI hardware will read from the ROM. IPL2 reads + * this configuration using the slowest possible configuration before switching to the provided configuration. + */ +#define PI_DOMAIN_1_CFG(lat, pwd, pgs, rls) \ + .byte (((rls) & 3) << 4) | ((pgs) & 0xF); \ + .byte (pwd) & 0xFF; \ + .byte (lat) & 0xFF + +/** + * Some older libultra versions use this field to set osClockRate. It does not have any other meaningful effect, the + * clock rate of physical units does not actually change to match this value. + */ +#define SYSTEM_CLOCK_RATE_SETTING(num) \ + .word (num) + +/** + * Indicates the entrypoint address of the program that IPL3 will load the boot segment to and execute. + * This should be >= 0x80000400. + */ +#define ENTRYPOINT(sym) \ + .word (sym) + +/** + * Indicates the hardware revision the program is designed for (hw_major, hw_minor) + * and what libultra version (os_ver) it uses. + * + * The hardware revision for a retail N64 is (2,0). + * The libultra version may be a single letter, without quotes. + */ +#define LIBULTRA_VERSION(hw_major, hw_minor, os_ver) \ + .half 0; \ + .byte (hw_major) * 10 + (hw_minor); \ + _os_ver_start = .; \ + .ascii #os_ver ; \ + .if (. - _os_ver_start) != 1; \ + .error "OS version should be just one letter"; \ + .endif + +/** + * Leaves space to insert the ROM Checksum value. IPL3 computes a checksum over ROM data in the range 0x1000 to 0x101000 + * and compares it to this value, if the results differ it will refuse to boot the program. + * + * This macro just writes 8 bytes of 0. The correct checksum value is filled in after the full ROM image is available to + * compute the checksum with. + */ +#define CHECKSUM() \ + .word 0, 0 + +/** + * For unused header space. Fills num bytes with 0. + */ +#define PADDING(num) \ + .fill (num) + +/** + * Defines the ROM name. This should be a string that is at most 20 characters long, a null terminator is not required. + * If a name is less than 20 characters, the remaining space will be padded with spaces. + */ +#define ROM_NAME(name) \ + _name_start = .; \ + .ascii name; \ + .if (. - _name_start) > 20; \ + .error "ROM name too long, must be at most 20 characters"; \ + .endif; \ + .if (. - _name_start) < 20; \ + .fill 20 - (. - _name_start), 1, 0x20; \ + .endif + +/** + * Identifies the storage medium the program intends to use. + * + * Should be one of: + * - CARTRIDGE + * - CARTRIDGE_EXPANDABLE + * - DISK + * - DISK_EXPANSION + */ +#define MEDIUM(type) \ + .ascii STORAGE_MEDIUM_##type + +/** + * Two-letter game identifier. Should be wrapped in quotes. + */ +#define GAME_ID(id) \ + _game_id_start = .; \ + .ascii id ; \ + .if (. - _game_id_start) != 2; \ + .error "Game ID should be two letters"; \ + .endif + +/** + * Identifies the region the game is made for. + * + * Should be one of: + * - ALL + * - JP + * - US + * - PAL + * - GATEWAY + * - LODGENET + * + * Note: Often flashcarts and emulators will read this value to determine whether to act as an NTSC or PAL system and + * will adjust the VI timings appropriately, which may be used to determine target FPS. + * This can lead to glitchy video output on hardware if the program configures a video mode other than the native + * video mode for the hardware version. + */ +#define REGION(name) \ + .ascii REGION_CODE_##name + +/** + * Identifies the game revision number. Can be between 0 and 255. + */ +#define GAME_REVISION(num) \ + .byte (num) + +#endif diff --git a/tools/disasm/disasm.py b/tools/disasm/disasm.py index 4a91303bba..3ab718c313 100755 --- a/tools/disasm/disasm.py +++ b/tools/disasm/disasm.py @@ -1756,16 +1756,48 @@ def get_overlay_sections(vram, overlay): [bss_end_vram, bss_end_vram, "reloc", None, overlay[header_loc:], None], ] +def get_storage_medium(id): + if id == 'N': + return "CARTRIDGE" + elif id == 'C': + return "CARTRIDGE_EXPANDABLE" + elif id == 'D': + return "DISK" + elif id == 'E': + return "DISK_EXPANSION" + else: + return "UNKNOWN" + +def get_region(id): + if id == 'A': + return "ALL" + elif id == 'J': + return "JP" + elif id == 'E': + return "US" + elif id == 'P': + return "PAL" + elif id == 'G': + return "GATEWAY" + elif id == 'L': + return "LODGENET" + else: + return "UNKNOWN" def disassemble_makerom(section): os.makedirs(f"{ASM_OUT}/makerom/", exist_ok=True) if section[2] == "rom_header": ( + endian, pi_dom1_reg, + pi_dom1_pwd, + pi_dom1_lat, clockrate, entrypoint, - revision, + pad_ver, + hw_ver, + os_ver, chksum1, chksum2, pad1, @@ -1776,26 +1808,28 @@ def disassemble_makerom(section): cart_id, region, version, - ) = struct.unpack(">IIIIIIII20sII2s1sB", section[4]) + ) = struct.unpack(">BBBBIIHBBIIII20sII2s1sB", section[4]) + out = f"""/* * The Legend of Zelda: Majora's Mask ROM header */ -.word 0x{pi_dom1_reg:08X} /* PI BSD Domain 1 register */ -.word 0x{clockrate:08X} /* Clockrate setting */ -.word 0x{entrypoint:08X} /* Entrypoint function (`entrypoint`) */ -.word 0x{revision:08X} /* Revision */ -.word 0x{chksum1:08X} /* Checksum 1 */ -.word 0x{chksum2:08X} /* Checksum 2 */ -.word 0x{pad1:08X} /* Unknown */ -.word 0x{pad2:08X} /* Unknown */ -.ascii "{rom_name.decode('ascii')}" /* Internal ROM name */ -.word 0x{pad3:08X} /* Unknown */ -.word 0x{cart:08X} /* Cartridge */ -.ascii "{cart_id.decode('ascii')}" /* Cartridge ID */ -.ascii "{region.decode('ascii')}" /* Region */ -.byte 0x{version:02X} /* Version */ +#include "rom_header.h" + +/* 0x00 */ ENDIAN_IDENTIFIER +/* 0x01 */ PI_DOMAIN_1_CFG({pi_dom1_lat}, {pi_dom1_pwd}, {pi_dom1_reg & 0xF}, {(pi_dom1_reg >> 4) & 3}) +/* 0x04 */ SYSTEM_CLOCK_RATE_SETTING(0x{clockrate:X}) +/* 0x08 */ ENTRYPOINT(0x{entrypoint:08X}) +/* 0x0C */ LIBULTRA_VERSION({hw_ver // 10}, {hw_ver % 10}, {chr(os_ver)}) +/* 0x10 */ CHECKSUM() +/* 0x18 */ PADDING(8) +/* 0x20 */ ROM_NAME("{rom_name.decode('ascii').rstrip()}") +/* 0x34 */ PADDING(7) +/* 0x3B */ MEDIUM({get_storage_medium(chr(cart))}) +/* 0x3C */ GAME_ID("{cart_id.decode('ascii')}") +/* 0x3E */ REGION({get_region(region.decode('ascii'))}) +/* 0x3F */ GAME_REVISION({version}) """ with open(ASM_OUT + "/makerom/rom_header.s", "w") as outfile: outfile.write(out)