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
51 changes: 51 additions & 0 deletions docs/HIROM_ALIASES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# HiROM aliases and execution contexts

Standard HiROM maps the upper halves of banks 00/40/80/C0 to the same ROM
bytes. The same relationship repeats for the other bank indices. Lower
halves alias only between full ROM banks 40–7D and C0–FD; 7E/7F are WRAM.
FE/FF have no whole-bank XOR-80 execution mirror.

The Python emitter and native analyzer expand authored cfg contracts into
valid physical aliases before analysis. Local entries, hooks and declared
exit contracts take precedence over inherited ones. Unspecified aliases
inherit in sorted source-bank order. Friendly names keep their original
owner. RAM relocation and auto-vector configuration are not cloned.

Inherited contracts must actually apply at the alias: add an explicit local
entry/exit declaration when execution differs. Inferred exit widths and
compiled functions remain tied to their execution bank. For example, the
same `JMP $1000` bytes at 00:9000 and C0:9000 target RAM and ROM respectively.
The runtime may reuse an XOR-80 body within a low-bank pair or a full-bank
pair only after verifying the live cartridge ROM pointers. It never uses
that check to substitute a full-bank body for a low-bank body.

Inherited explicit indirect-dispatch targets follow the opcode's target width:
near JMP/JSR targets resolve in the executing bank, even when the cfg spells
the address in an authored physical alias. Long transfers retain their absolute
target banks. Null dispatch slots remain null. This distinction is enforced in
both decoders before native analysis or emission, so an alias cannot call a
full-bank body in place of its separately analyzed low-bank continuation.

Coverage reports include `rom_pc24` for physical grouping while retaining
the raw target, execution variant, outcomes and qualification provenance.
A successful C0 run cannot qualify bank 00. Coverage remains an analysis
seed and never overrides structural analysis or replay requirements.

Disassembly layout materialization and source export accept standard LoROM
and HiROM, including full-bank HiROM addresses below $8000. Authority ingestion
deduplicates identical physical instruction spans and detects conflicts or
code/data overlap across aliases. Entry widths retain their explicit HiROM
execution bank. The v1 authority schema describes one unconditional physical
instruction layout: alternate-width interpretations with different byte
boundaries need separate authority inputs, not a silently selected layout.

These changes do not generalize aliasing for SA-1, S-DD1 or other banked
mapping. Synthetic tests cover mapper boundaries, cfg precedence, short-jump
semantics, inherited near/long dispatches, native/emitter agreement, source
export and authority overlays. The reporter qualified corrected head `0dae9c8`
with Secret of Mana boot, field/flight, cutscene and shop replays: state, video
and master-clock checkpoints matched the hand-alias baseline in compiled and
interpreter-only runs. Matched warmed Release timings were within noise, with
no reported startup or working-set increase. See the
[replay and runtime report](https://github.com/RetroPortingToolKit/snesrecomp/pull/199#issuecomment-6400790503).
This evidence is title-specific and does not claim to fix SD3 issue #80.
19 changes: 5 additions & 14 deletions recompiler-rs/src/bin/analyze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ use snesrecomp_analyzer::decoder::{
};
use snesrecomp_analyzer::insn::Mode;
use snesrecomp_analyzer::rom::{
addr_to_rom_offset, detect_rom_mapping, is_rom_address, load_rom, vector_table_offset,
RelocRegion, RomMapping,
addr_to_rom_offset, detect_rom_mapping, execution_mirror_bank as mirror_bank, is_rom_address,
load_rom, vector_table_offset, RelocRegion, RomMapping,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
Expand Down Expand Up @@ -167,18 +167,6 @@ fn filename_bank(path: &Path) -> Option<u32> {
u32::from_str_radix(body, 16).ok()
}

fn mirror_bank(mapping: RomMapping, bank: u32) -> Option<u32> {
if mapping == RomMapping::Sa1 {
return None;
}
let bank = bank & 0xFF;
if bank < 0x40 || (0x80..0xC0).contains(&bank) {
Some(bank ^ 0x80)
} else {
None
}
}

fn mirror_pc24(mapping: RomMapping, pc24: u32) -> Option<u32> {
mirror_bank(mapping, (pc24 >> 16) & 0xFF).map(|bank| (bank << 16) | (pc24 & 0xFFFF))
}
Expand Down Expand Up @@ -281,6 +269,9 @@ fn load_inputs(cfg_dir: &Path, rom: &mut Vec<u8>, all_cfg_roots: bool) -> Result
cfg.bank = bank as i32;
cfgs.push(cfg);
}
if mapping == RomMapping::HiRom {
cfgs = snesrecomp_analyzer::hirom_cfg::expand(cfgs);
}
expand_auto_vectors(&mut cfgs, rom.as_slice());

let mut roots = architectural_roots(rom.as_slice());
Expand Down
56 changes: 42 additions & 14 deletions recompiler-rs/src/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ use std::sync::{Arc, Mutex};

use crate::insn::{decode_insn, Insn, Mode};
use crate::rom::{
addr_in_reloc_region, addr_to_rom_offset, is_rom_address, RelocRegion, RomMapping,
addr_in_reloc_region, addr_to_rom_offset, execution_mirror_bank, is_rom_address, RelocRegion,
RomMapping,
};

pub const PHP_STACK_MAX_DEPTH: usize = 8;
Expand Down Expand Up @@ -697,6 +698,10 @@ fn resolve_indirect_dispatch_targets(
// Preserve an explicit null slot instead of turning it
// into bank:$0000; every downstream consumer skips zero.
Some(0)
} else if mapping == RomMapping::HiRom && !is_long_dispatch(insn, bases) {
// A copied physical contract may name its authored bank;
// a near transfer still executes in this bank's PB.
Some((bank << 16) | (target & 0xFFFF))
} else if target > 0xFFFF {
Some(target)
} else {
Expand Down Expand Up @@ -871,7 +876,7 @@ fn labeled_successors(
let mut skip = map.get(&tp).copied();
if skip.is_none() {
let tbank = (tp >> 16) & 0xFF;
if mapping != RomMapping::Sa1 && (tbank < 0x40 || (0x80..0xC0).contains(&tbank)) {
if execution_mirror_bank(mapping, tbank).is_some() {
skip = map.get(&(tp ^ 0x800000)).copied();
}
}
Expand All @@ -887,8 +892,7 @@ fn labeled_successors(
let mut hit = cem.get(&(tp, post_m, post_x)).copied();
if hit.is_none() {
let tbank = (tp >> 16) & 0xFF;
if mapping != RomMapping::Sa1 && (tbank < 0x40 || (0x80..0xC0).contains(&tbank))
{
if execution_mirror_bank(mapping, tbank).is_some() {
hit = cem.get(&(tp ^ 0x800000, post_m, post_x)).copied();
}
}
Expand All @@ -904,8 +908,7 @@ fn labeled_successors(
let mut mode_set: Option<Vec<(u8, u8)>> = cmm.get(&(tp, post_m, post_x)).cloned();
if mode_set.is_none() {
let tbank = (tp >> 16) & 0xFF;
if mapping != RomMapping::Sa1 && (tbank < 0x40 || (0x80..0xC0).contains(&tbank))
{
if execution_mirror_bank(mapping, tbank).is_some() {
mode_set = cmm.get(&(tp ^ 0x800000, post_m, post_x)).cloned();
}
}
Expand Down Expand Up @@ -2515,7 +2518,7 @@ fn lookup_exit_mx(
let key = (pc24 & 0xFFFFFF, m & 1, x & 1);
map.get(&key).copied().or_else(|| {
let bank = (pc24 >> 16) & 0xFF;
if mapping != RomMapping::Sa1 && (bank < 0x40 || (0x80..0xC0).contains(&bank)) {
if execution_mirror_bank(mapping, bank).is_some() {
map.get(&((pc24 ^ 0x800000) & 0xFFFFFF, m & 1, x & 1))
.copied()
} else {
Expand All @@ -2539,7 +2542,7 @@ fn lookup_exit_mx_modes(
let key = (pc24 & 0xFFFFFF, m & 1, x & 1);
let mut result = map.get(&key).cloned().or_else(|| {
let bank = (pc24 >> 16) & 0xFF;
if mapping != RomMapping::Sa1 && (bank < 0x40 || (0x80..0xC0).contains(&bank)) {
if execution_mirror_bank(mapping, bank).is_some() {
map.get(&((pc24 ^ 0x800000) & 0xFFFFFF, m & 1, x & 1))
.cloned()
} else {
Expand Down Expand Up @@ -3481,18 +3484,14 @@ pub(crate) fn compute_deps(
if let Some(tp) = tp {
cem_keys.push((tp, ins.m_flag, ins.x_flag));
let tbank = (tp >> 16) & 0xFF;
if graph.rom_mapping != RomMapping::Sa1
&& (tbank < 0x40 || (0x80..0xC0).contains(&tbank))
{
if execution_mirror_bank(graph.rom_mapping, tbank).is_some() {
cem_keys.push((tp ^ 0x800000, ins.m_flag, ins.x_flag));
}
}
for tp in dispatch_targets {
cem_keys.push((tp, ins.m_flag, ins.x_flag));
let tbank = (tp >> 16) & 0xFF;
if graph.rom_mapping != RomMapping::Sa1
&& (tbank < 0x40 || (0x80..0xC0).contains(&tbank))
{
if execution_mirror_bank(graph.rom_mapping, tbank).is_some() {
cem_keys.push((tp ^ 0x800000, ins.m_flag, ins.x_flag));
}
}
Expand Down Expand Up @@ -4533,6 +4532,35 @@ mod tests {
);
}

#[test]
fn hirom_explicit_dispatch_uses_near_pb_and_preserves_long_banks() {
for bank in [0, 0x40, 0x80, 0xc0] {
for opcode in [0x6c, 0x7c, 0xfc, 0xdc] {
let rom = rom_at_8000(&[opcode, 0x10, 0x80]);
let insn = decode_insn(&rom, 0, 0x8000, bank, 1, 1).unwrap();
let expected = if opcode == 0xdc {
vec![0, 0xc09010, 0xc19020]
} else {
vec![0, (bank << 16) | 0x9010, (bank << 16) | 0x9020]
};
assert_eq!(
resolve_indirect_dispatch_targets(
&rom,
RomMapping::HiRom,
bank,
&insn,
3,
&[],
&[0, 0xc09010, 0xc19020],
&[],
),
Some(expected),
"bank={bank:02x}, opcode={opcode:02x}"
);
}
}
}

#[test]
fn computed_pei_rts_is_not_a_callable_exit_mode() {
let rom = rom_at_8000(&[
Expand Down
Loading
Loading