Skip to content

Instantly share code, notes, and snippets.

@rb6502
Created August 6, 2026 17:04
Show Gist options
  • Select an option

  • Save rb6502/8c60b0c062b2394d1bed2720b2b12997 to your computer and use it in GitHub Desktop.

Select an option

Save rb6502/8c60b0c062b2394d1bed2720b2b12997 to your computer and use it in GitHub Desktop.
MAME SH audit report

MAME SuperH core audit — instruction semantics (results & flags)

Date: 2026-08-06 Scope: src/devices/cpu/sh/ — interpreter and DRC, SH-1/SH-2/SH-3/SH-4. Only instruction behaviour (computed results, T/Q/M/S flags, register writeback) was in scope for this pass. Exceptions, MMU, TLB, interrupt delivery, and peripherals were deliberately not audited except where they change an instruction's result.

References used (all from /Volumes/data/EmuDocs/SH/):

  • sh2.pdf — SH-1/SH-2/SH-DSP Software Manual rev 5.00
  • sh3.pdf — SH-3/SH-3E/SH3-DSP Software Manual
  • sh4_software_manual.pdf — SH-4 Software Manual rev 6.00
  • sh4a_software_manual.pdf (cross-check only)

Files covered: sh.cpp (shared interpreter + shared DRC generators), sh2.cpp, sh4.cpp, sh_fe.cpp, sh2fe.cpp, sh4fe.cpp, sh4comn.cpp, sh7021.cpp, sh7709s.cpp.


Summary

The SH-2 integer core is in good shape: every arithmetic/logic/shift/compare op in sh.cpp was checked line-by-line against the manual pseudocode and the interpreter matches, including the fiddly ones (ADDC, ADDV, SUBC, SUBV, NEGC, DIV0S, DIV1, DMULS, MAC.L, MAC.W, CMP/STR, SWAP.B, XTRCT). The shared DRC generators for those same ops are also correct, including the carry-flag plumbing (UML_CARRY/ADDC/SUBB/ROLC/RORC) and the branch-PC arithmetic.

The problems are concentrated in the SH-4 floating-point unit, and one of them is severe: a mistyped UML_TEST mask makes every double-precision FP instruction in the DRC silently execute as single-precision on the wrong 32-bit halves.

# Issue Where Severity
1 UML_TEST(m_fpu_pr, 0) — PR=1 branch is unreachable; all double-precision FP is wrong under DRC sh4.cpp ×11 Critical
2 FPSCR.RM / FPSCR.DN never honoured (SH-4 resets to round-to-zero + DAZ) sh4.cpp, DRC High
3 FTRC does not saturate; result is host-dependent sh4.cpp interp + DRC High
4 FDIV by zero leaves FRn/DRn unmodified instead of producing ±INF sh4.cpp:2288 Medium
5 FSQRT PR=1 computes with sqrtf() (single precision) sh4.cpp:2323 Medium
6 DRC FNEG implemented as 0.0 - x; wrong for ±0 and NaN sh4.cpp:5372 Medium
7 DRC dynamic branches pass a garbage ovrpc into the delay slot sh.cpp ×5 High — confirmed by test
8 DRC MOVA ignores ovrpc — wrong in a delay slot sh.cpp:3376 High — confirmed by test
9 SH-2 DRC LDC.L @Rm+,SR / RTE don't mask SR with SH_FLAGS sh.cpp:3467, 3726 Medium
10 SH-4 MAC.L with S=1 uses SH-2 semantics (clobbers MACH[31:16]) sh.cpp:910 Low
11 sh_fe.cpp register-dependency descriptions are wrong in ~10 places sh_fe.cpp Low (latent)
12 Undefined-opcode handling diverges between interpreter and DRC sh4.cpp Low
13 ORI/ORM cycle counts are swapped; JMP undercharges sh.cpp Low (timing)
14 SH-3 executes SH-4 FPU opcodes instead of trapping sh4.cpp:2940 Low
15 DRC FLDI0/FLDI1 big-endian path won't compile sh4.cpp:5445, 5457 Low (latent)

Critical

1. SH-4 DRC: the double-precision dispatch test is a no-op

Eleven FP generators select between the PR=0 (single) and PR=1 (double) paths like this:

UML_TEST(block, mem(&m_sh2_state->m_fpu_pr), 0);   // sh4.cpp:5092 and 10 others
UML_JMPc(block, COND_Z, compiler.labelnum);

UML TEST is an AND-with-mask that sets Z and S (uml.cpp:191, OPINFO2(TEST, "!test", 4|8, false, NONE, SZ, ...)), exactly as it is used correctly elsewhere in this same file (UML_TEST(block, mem(&sr), SH_T)). With a mask of 0 the result is always zero, so Z is always set and the branch is always taken. The PR=1 arm is dead code in every one of these:

Line Instruction Effect when FPSCR.PR=1
5092 FADD FSADD on m_fr[n]/m_fr[m]
5108 FSUB FSSUB on the wrong halves
5124 FMUL FSMUL on the wrong halves
5140 FDIV FSDIV on the wrong halves
5156 FCMP/EQ single-precision compare of the wrong halves
5177 FCMP/GT single-precision compare of the wrong halves
5341 FLOAT int→float into m_fr[n] instead of int→double
5358 FTRC float→int from the wrong half
5376 FNEG single-precision negate of the wrong half
5395 FABS AND m_fr[n], 0x7fffffff — clears bit 31 of the low mantissa word

Why "the wrong halves": with PR=1 on a little-endian host, LDS Rm,FPSCR / LDS.L @Rm+,FPSCR call sh4_swap_fp_couples() (sh4comn.cpp:245) so that *(double*)(m_fr + n) is a valid native double — i.e. m_fr[n] holds the low mantissa word and m_fr[n+1] holds sign+exponent. FPS32(n) is m_fs_regmap[n], which points at m_fr[n]. So the generated code reinterprets the bottom 32 mantissa bits of DRn as a float and operates on that.

FMAC (line 5282) has the same broken test with the opposite polarity (COND_NZ), so it is never skipped — the DRC executes FMAC even when PR=1, where the interpreter correctly does nothing (sh4.cpp:2315).

Fix: change the mask from 0 to 1 (m_fpu_pr is normalised to 0/1 at sh4.cpp:1501/1557), or use UML_CMP(..., 0). Note that fixing the test will expose the PR=1 arms for the first time, so they need review as a unit — FABS's PR=1 arm (sh4.cpp:5399) looks right, FCMP's use of FPD32(REG_M & 14) is redundant but harmless (FPD32 already masks).

Grepping all of src/devices/cpu/ finds this UML_TEST(x, 0) pattern only at these 11 SH-4 sites, which is good evidence it is a typo rather than an idiom.

Impact: any SH-4 title that sets FPSCR.PR. Single-precision code (the majority of Dreamcast/Naomi rendering math) is unaffected. Because the interpreter handles PR=1 correctly, this also shows up as a -drc/-nodrc divergence, which is a useful way to confirm it in a specific title.


High

2. FPSCR.RM and FPSCR.DN are ignored

SH-4 resets FPSCR to H'00040001 (manual §2, and MAME does set this at sh4.cpp:2571), i.e. RM = 01 = round toward zero and DN = 1 = denormals treated as zero. Most SH-4 software never changes it.

Neither bit is consulted anywhere in the arithmetic:

  • Interpreter FADD/FSUB/FMUL/FDIV/FSQRT/FCNVSD (sh4.cpp:22302340) use plain C operators, so they inherit the host mode (round-to-nearest-even, denormals enabled).
  • DRC UML_FSADD/FDADD/... likewise use whatever the host FPU control word says.
  • The single exception is FCNVDS (sh4.cpp:2210), which fakes round-to-zero by masking the low mantissa word with 0xe0000000. That mask is actually correct — float keeps double mantissa bits 51..29, so clearing low-word bits 28..0 is the right truncation — but it is the only place RM is looked at.

Consequence: last-bit differences on essentially every FP operation versus real hardware, which accumulate through iterative code (matrix concatenation, physics integration) and are a plausible source of "drifts over time" geometry bugs. It also means results are not reproducible across hosts with different default FP environments.

This is a design-level gap rather than a one-line fix; the realistic options are to set the host rounding mode when FPSCR.RM changes (and restore around the emulation loop), or to implement the ops via soft-float. Worth deciding on before chasing individual FP bugs, since it's the noise floor for all of them.

3. FTRC does not saturate — and the wrong answer differs by host

SH-4 manual §9 (ftrc_invalid, sh4_software_manual p.304):

void ftrc_invalid(int sign, int *FPUL) {
    set_V();
    if ((FPSCR & ENABLE_V) == 0) {
        if (sign == 0)  *FPUL = 0x7fffffff;
        else            *FPUL = 0x80000000;
    }
    ...
}

so an out-of-range positive operand must produce 0x7FFFFFFF.

MAME interpreter (sh4.cpp:2085, 2090):

*((int32_t *)&m_sh2_state->m_fpul) = (int32_t)FP_RFS(n);

An out-of-range float→int conversion is UB in C++, and in practice:

  • x86-64 (cvttss2si) yields the "integer indefinite" value 0x80000000 for both positive and negative overflow — so a large positive float comes back negative.
  • AArch64 (fcvtzs) saturates, so the same program returns 0x7FFFFFFF — the correct answer, by accident.

The DRC has the identical problem: UML_FSTOINT(..., ROUND_TRUNC) (sh4.cpp:5366) lowers to the same host instruction.

So this is both a correctness bug and a host-architecture-dependent behaviour difference in MAME, which makes any regression testing of SH-4 FP titles unreliable between an Intel Mac and an Apple Silicon Mac. NaN also maps to NINF0x80000000 per the manual, which x86 happens to match.

Fix: range-check before converting, per ftrc_single_type_of / ftrc_double_type_of in the manual, in both the interpreter and the cfunc/generator.


Medium

4. FDIV by zero produces no result at all

// sh4.cpp:2288
if (FP_RFD(m) == 0) return;      // PR=1
...
if (FP_RFS(m) == 0) return;      // PR=0

Per the FDIV special-cases table (sh4_software_manual p.269), NORM ÷ ±0 raises the divide-by-zero exception, and with the exception disabled (the reset state) the architectural result is ±INF. ±0 ÷ ±0 is Invalid → qNaN. MAME instead leaves the destination register holding its previous value, which is neither.

Note that the DRC does the IEEE-correct thing here (UML_FSDIV/UML_FDDIV produce ±INF), so this is also an interpreter/DRC divergence — and one of the few places where the DRC is the more accurate of the two.

Same shape of problem in FSQRT (sh4.cpp:2330, 2336) and FSRRA (sh4.cpp:2347): a negative operand returns with the register untouched, where the manual (FSQRT special cases, p.286) requires Invalid → qNaN. FSQRT(-0.0) happens to be handled correctly since -0.0 < 0 is false.

5. FSQRT in double-precision mode computes a single-precision square root

// sh4.cpp:2332
FP_RFD(n) = sqrtf(FP_RFD(n));

sqrtf is the float overload — the double operand is rounded to float, the sqrt is computed in single precision, and the result is widened back to double. FSQRT DRn therefore delivers ~24 bits of mantissa instead of 53. Should be sqrt() (or std::sqrt). The PR=0 arm at line 2338 is correct.

6. DRC FNEG is not a sign-bit flip

// sh4.cpp:5372
UML_MOV(block, I0, 0);
UML_FSFRINT(block, F1, I0, SIZE_DWORD);
UML_FSSUB(block, FPS32(REG_N), F1, FPS32(REG_N));   // FRn = 0.0 - FRn

The SH-4 instruction table (p.253/p.255) defines FNEG FRn as FRn ^ H'80000000 and FNEG DRn as DRn ^ H'8000000000000000 — a pure bit operation that never signals and never changes the payload of a NaN.

0.0 - x differs from -x for x = +0.0: IEEE gives +0.0, but FNEG(+0.0) must be -0.0. It can also quiet a signalling NaN and change the sign of a NaN. The interpreter's unary - (sh4.cpp:2122/2126) is correct, so this is a clean DRC/interpreter divergence. Cheapest fix is to mirror FABS and XOR the sign word directly.

7. DRC register-indirect branches hand a garbage PC to the delay slot

ovrpc exists so that a PC-relative load in a delay slot uses the branch destination as its PC base (SH-2 manual, MOVA note: "If this instruction is placed immediately after a delayed branch instruction, the PC must point to (the starting address of the branch destination) + 2"). PC-relative addressing generally is handled correctly — I checked the arithmetic for MOVWI, MOVLI, MOVA, BRA, BSR, BT, BF, BTS, BFS, BRAF, BSRF, JSR, RTS in both interpreter and DRC and it all matches the manual's PC = instruction_address + 4 convention.

The static branches pass a genuine compile-time constant:

// sh.cpp:2672 (BRA), 2686 (BSR), 3250 (BTS), 3271 (BFS)
m_sh2_state->ea = (desc->pc + 2) + disp * 2 + 2;
generate_delay_slot(block, compiler, desc, m_sh2_state->ea - 2);   // = target - 2, correct

But the dynamic branches pass a field of the live CPU state, read on the host at compile time:

sh.cpp:3517  BSRF: generate_delay_slot(block, compiler, desc, m_sh2_state->target);
sh.cpp:3583  RTS : generate_delay_slot(block, compiler, desc, m_sh2_state->target);
sh.cpp:3681  BRAF: generate_delay_slot(block, compiler, desc, m_sh2_state->target);
sh.cpp:3831  JSR : generate_delay_slot(block, compiler, desc, m_sh2_state->target - 4);
sh.cpp:4046  JMP : generate_delay_slot(block, compiler, desc, m_sh2_state->target);

m_sh2_state->target at compile time is whatever the last-executed dynamic branch left behind — it has nothing to do with this block. (Note also the inconsistent -4 on JSR versus no adjustment on the other four, which suggests this was never really thought through.)

If the delay slot contains MOV.W @(disp,PC),Rn or MOV.L @(disp,PC),Rn, then with the default SH2DRC_STRICT_PCREL off the generator performs read_word(scratch) / read_long(scratch) at compile time from that garbage address and bakes the value in as a constant (sh.cpp:2660, 2713). That is silently wrong and non-deterministic — it depends on recompile timing, so it can break save states and makes bugs irreproducible.

Confirmed in hardware testing — see "Confirmation" below. My first assessment of this was that it was "vanishingly rare because no compiler emits it"; that was wrong, and the sh2test results disprove it. Rated High.

The branch target genuinely isn't a compile-time constant, but it doesn't need to be: all five generators already emit UML_MOV(mem(&m_sh2_state->target), ...) before calling generate_delay_slot, so the delay-slot code can legitimately read target at run time. See the fix sketch below.

8. DRC MOVA ignores ovrpc entirely

// sh.cpp:3376
case  7: // MOVA(opcode & 0xff);
    scratch = (opcode & 0xff) * 4;
    scratch += ((desc->pc + 4) & ~3);

Unlike MOVWI (case 9) and MOVLI (case 13), this never looks at ovrpc. In a delay slot the interpreter computes ((target + 2) & ~3) + disp*4 (correct per the manual note quoted above), while the DRC computes ((slot_addr + 4) & ~3) + disp*4. Definite interpreter/DRC divergence, deterministic, and trivially fixable by copying the ovrpc handling from case 9.

Confirmed in hardware testing — see below.


Confirmation: sh2test on Saturn, 2025-10-01 build

A CPU test program run on the Saturn target reports, as its first three results:

MOVA                      FAIL 1223
MOV.W @(disp, PC), Rn     FAIL 1385
MOV.L @(disp, PC), Rn     FAIL 1801

All three pass with -nodrc, i.e. they are DRC-only. These are precisely items #7 and #8.

The MOVA line is the diagnostic one. Outside a delay slot the two paths compute the same address:

// sh.cpp:3376, DRC
scratch = ((desc->pc + 4) & ~3) + (opcode & 0xff) * 4;
// sh.cpp:1243, interpreter, where pc == A + 2
ea = ((pc + 2) & ~3) + disp * 4;          // == ((A + 4) & ~3) + disp*4

so the only way DRC MOVA can diverge from the interpreter is inside a delay slot, where it ignores ovrpc. That pins the failing test to delay-slot PC-relative addressing and rules out any other mechanism for that line.

The other plausible explanation for the MOV.W/MOV.L lines — that the DRC constant- folds the literal at compile time (sh.cpp:2660, 2713) and returns a stale value after the test rewrites its literal pool — is ruled out: saturn.cpp:978 sets SH2DRC_STRICT_VERIFY|SH2DRC_STRICT_PCREL, and with STRICT_PCREL on both generators emit a genuine runtime read. So the address is wrong, which means ovrpc is wrong, which means a dynamic-branch delay slot (RTS/JMP/JSR/BRAF/BSRF, or RTE).

Note that STRICT_PCREL is not set by every SH-2 driver — cv1k.cpp, coolridr.cpp, feversoc.cpp and deco_mlc.cpp use SH2DRC_FASTEST_OPTIONS. On those, the same delay slot would be constant-folded at compile time from the garbage address, which is both wrong and nondeterministic across recompiles.

Fix sketch

MOVA is mechanical — mirror cases 9 and 13:

case  7: // MOVA(opcode & 0xff);
    scratch = ((((ovrpc == 0xffffffff) ? desc->pc : ovrpc) + 4) & ~3)
            + (opcode & 0xff) * 4;
    UML_MOV(block, R32(0), scratch);
    return true;

For BRA (which passes ovrpc = target - 2) this yields ((target + 2) & ~3) + disp*4, matching both the interpreter and the manual's delay-slot note.

For the dynamic branches, add a distinguished ovrpc sentinel (say 0xfffffffe) meaning "PC base is in m_sh2_state->target at run time", have all five generators pass it — normalising JSR's stray - 4 (sh.cpp:3831) so they all just set target — and have cases 7/9/13 emit the runtime form when they see it:

// MOVA, dynamic case:  R0 = ((target + 2) & ~3) + disp*4
UML_ADD(block, I0, mem(&m_sh2_state->target), 2);
UML_AND(block, I0, I0, ~3);
UML_ADD(block, R32(0), I0, (opcode & 0xff) * 4);

// MOVWI / MOVLI, dynamic case: force the STRICT_PCREL-style runtime read
// off the same computed address rather than folding a constant.

This is sound because target already holds the branch destination by the time the delay slot runs: every one of the five emits its UML_MOV(mem(&target), ...) before calling generate_delay_slot (sh.cpp:3510, 3581, 3679, 3827, 4044), and a branch inside a delay slot is an illegal slot instruction, so nothing else can clobber it.

RTE (sh.cpp:3456) needs separate handling: it calls generate_delay_slot before popping PC/SR from the stack, so the destination genuinely isn't available yet. Either reorder the pops ahead of the delay slot or leave RTE to the interpreter. The interpreter is already correct here because the main loop assigns pc = m_delay before executing the slot instruction.

9. SH-2 DRC loads SR without masking

Interpreter (sh2.cpp:224, sh2.cpp:244):

m_sh2_state->sr = read_long(m_sh2_state->ea) & SH_FLAGS;   // LDC.L @Rm+,SR
m_sh2_state->sr = read_long(m_sh2_state->ea) & SH_FLAGS;   // RTE

DRC (sh.cpp:3726 and sh.cpp:3467):

UML_MOV(block, mem(&m_sh2_state->sr), I0);   // no & SH_FLAGS

SH_FLAGS is (M|Q|I|S|T) = 0x000003F3. The sibling generator generate_group_4_LDCSR (sh.cpp:3713) does mask, so this is an oversight rather than a deliberate choice. Reserved SR bits then survive into STC SR,Rn, and any code that round-trips SR through memory will read back different values under DRC than under the interpreter. In normal operation the pushed value is already masked, so this only bites when SR is loaded from data the CPU didn't push itself — but that's exactly what an OS context-switch does.

10. SH-4 MAC.L with S=1 uses the SH-2 saturation rule

sh.cpp:910 implements the SH-2 manual's version verbatim (which it matches exactly — including the fact that the SH-2 manual really does write Res2 += (MACH & 0x0000FFFF) with no sign extension). The SH-4 manual (p.324) differs in three ways:

if (MACH & 0x00008000);
else Res2 += MACH | 0xFFFF0000;          /* MAME omits this term  */
Res2 += MACH & 0x00007FFF;               /* MAME uses & 0x0000FFFF */
if (((long)Res2 < 0) && (Res2 < 0xFFFF8000)) { Res2 = 0xFFFF8000; ... }
                                         /* MAME uses 0x00008000  */
MACH = (Res2 & 0x0000FFFF) | (MACH & 0xFFFF0000);
                                         /* MAME overwrites all of MACH */

The MACH-high-half preservation is the clearly-real difference and the one I'd fix; the sign-extension term as printed looks like a manual typo (adding MACH|0xFFFF0000 for a positive accumulator makes no arithmetic sense) and I would not implement it as written without hardware confirmation. Low severity: saturating MAC (S=1) is rare outside DSP-style code. MAC.W was cross-checked against both manuals and matches.


Low / latent

11. sh_fe.cpp register-dependency descriptions

These are currently harmlessdesc.regreq is computed by drcfe.ipp:362 but no SH generator ever reads it, so no register write is ever elided. They only affect DRC debug logging today. They are, however, exactly the kind of thing that turns into a silent miscompile the day someone adds a regreq-driven optimisation, so they're worth correcting while they're cheap:

Line Instruction Problem
580 STS MACH,Rn set_mach_modified() — should be set_mach_used() (direction reversed)
645–646 STC VBR,Rn set_r_used(N) + set_vbr_modified() — both reversed; should be set_r_modified(N) + set_vbr_used()
650 BRAF Rn reads REG_M; BRAF is 0000nnnn00100011, so REG_N
890 JMP @Rn reads REG_M; JMP is 0100nnnn00101011, so REG_N
424 CMP/EQ #imm,R0 set_r_used(REG_M) — the operand is R0; REG_M here is part of the immediate
314–318 DIV1 no set_sr_used(), but DIV1 reads T, Q and M
429–443 BT/BF/BT/S/BF/S no set_sr_used(), but they read T
807–810 LDS.L @Rn+,MACL marks REG_M used and modified; only REG_N is involved
494–500 AND.B/XOR.B/OR.B #imm,@(R0,GBR) set_sr_modified() (only TST.B touches T) and no set_writes_memory()
261–265 XTRCT grouped with MOV.x Rm,@-Rn, so gets a spurious set_writes_memory()
475–477 MOVA set_reads_memory() — MOVA computes an address, it doesn't load
612–620 MAC.L no set_r_modified() for the two post-increments, no set_reads_memory()
672–683 RTE no set_sr_modified()

sh4fe.cpp describes nothing at all for the SH-4-specific and FPU opcodes (it returns true with a // FIXME at the top of the file acknowledging this). Same reasoning applies — inert today, a trap later.

12. Undefined-opcode handling diverges between interpreter and DRC

  • 1111nnnnmmmm1111: interpreter calls dbreak()machine().debug_break() (sh4.cpp:2961); DRC returns true and emits nothing, i.e. a silent NOP (sh4.cpp:5083).
  • 1111nnnn110[01]1101: interpreter dbreak() (sh4.cpp:2444); DRC returns false, which routes to cfunc_unimplemented and raises an illegal-instruction exception (sh4.cpp:5308).

Beyond the divergence, having an emulated instruction drop the user into the debugger is a debugging artifact that shouldn't be in a shipping core.

13. Cycle counts

Not results, but noted while reading:

  • ORI (sh.cpp:1307) charges icount -= 2 and ORM (sh.cpp:1314) charges nothing. The manual gives OR #imm,R0 = 1 cycle and OR.B #imm,@(R0,GBR) = 3 (sh2.pdf p.4689/9161). The two are swapped. ANDI/ANDM, XORI/XORM and TSTI/TSTM are all correct, so this is an isolated slip. The DRC gets ORI right.
  • JMP (sh.cpp:778) has its icount-- commented out with the note "not in SH4 implementation?"; SH-2 JMP is 2 cycles. sh_fe.cpp also leaves JMP and JSR at the default cycles = 1.

14. SH-3 gets an SH-4 FPU

execute_one_f000 and generate_group_15 are defined on sh34_base_device (sh4.cpp:2940, sh4.cpp:5064) with a // the SH3 doesn't have these? comment, so an SH-3 device executes SH-4 FPU opcodes rather than treating them as illegal. Only matters for code that probes for an FPU.

15. DRC FLDI0/FLDI1 big-endian path is dead code that wouldn't compile

#else
    UML_MOV(block, FP_RFS(REG_N), 0);          // sh4.cpp:5445, 5457
#endif

FP_RFS is the interpreter macro (sh4comn.h:26), expanding to a float lvalue, not a uml::parameter. Only reachable on a big-endian host, so it has never been compiled.

16. Cosmetic

generate_checksum_block's full-verification path uses SCALE_x4 where the loose path uses SCALE_x2 (sh.cpp:2364 vs 2353). Harmless — the index is a literal 0 — but inconsistent.


Verified correct

Recording these so the next pass doesn't re-walk them.

Interpreter, sh.cpp — checked against sh2.pdf §6 pseudocode, all match: ADD, ADDI, ADDC, ADDV, AND, ANDI, ANDM, CMP/EQ, CMP/GE, CMP/GT, CMP/HI, CMP/HS, CMP/PL, CMP/PZ, CMP/STR, CMP/EQ #imm, DIV0S, DIV0U, DIV1 (all four Q/M quadrants and the final T = (Q==M)), DMULS.L, DMULU.L, DT, EXTS.B/W, EXTU.B/W, MAC.L (SH-2 form), MAC.W, every MOV addressing mode including the m == n special cases for @Rm+ and @-Rn, MOVT, MUL.L, MULS.W, MULU.W, NEG, NEGC, NOT, OR, ORI, ORM, ROTCL, ROTCR, ROTL, ROTR, SETT/CLRT/CLRMAC, SHAL, SHAR, SHLL/SHLR and the 2/8/16 variants, SUB, SUBC, SUBV, SWAP.B, SWAP.W, TAS.B, TST, TSTI, TSTM, XOR, XORI, XORM, XTRCT.

Branch/PC arithmetic — the whole set was re-derived against the manual's "PC = instruction address + 4" pseudocode convention (MAME keeps m_sh2_state->pc two bytes behind that, and the delay-slot loop sets pc = m_delay before executing the slot instruction, which is exactly what the MOVA delay-slot note requires). BRA, BSR, BRAF, BSRF, BT, BF, BT/S, BF/S, JMP, JSR, RTS, MOVA, MOV.W @(disp,PC), MOV.L @(disp,PC) all check out in the interpreter, and in the DRC apart from items 7 and 8 above.

Shared DRC generators, sh.cppADDC/SUBC carry plumbing via UML_CARRY + ADDC/SUBB + SETc/ROLINS; ROTCL/ROTCR via ROLC/RORC; SHLL/SHLR/ ROTL/ROTR relying on SETc(COND_C) (confirmed valid — uml.cpp:197-203 declares SZC output flags for all of SHL/SHR/SAR/ROL/ROR/ROLC/RORC); NEGC; CMPSTR; DIV0S; SWAPB; XTRCT; DT; CMPPZ/CMPPL; TAS; MULU/MULS /DMULU/DMULS operand widths and macl/mach destinations. Scratch-register use is safe: only R0/R1/R2 are ever mapped to I-registers (I4/I5/I6, sh.cpp:167-176), and the generators only scribble on I0–I3 and I7.

SH-4 interpreterSHAD and SHLD match the manual pseudocode exactly, including the shift-count-zero and negative-count cases; STC/LDC Rm_BANK picks the non-current bank correctly; FPSCR masking to 0x003FFFFF; SH34_FLAGS covers exactly the defined SR bits; FTRV matrix orientation matches the manual (FRn+i = Σj XF[4j+i] * FRn+j); FIPR register decode; FSCA angle scaling; the ^ m_fpu_pr single-word access trick used by FMOV/FLDS/FSTS/FLDI0/FLDI1 is the correct inverse of sh4_swap_fp_couples(); FCNVDS's 0xe0000000 round-to-zero mask is right.

Not audited: exceptions and illegal-slot handling, the SH-4 MMU/UTLB, SH-4 store queues beyond noting PREFM's "good enough for GD-ROM" comment, cache emulation, interrupt priority, all peripheral blocks, and the disassembler.


Suggested order of attack

  1. #8, then #7 — the only findings with a confirmed failing test behind them (sh2test on Saturn). MOVA is a two-line change; the dynamic-branch case needs the sentinel/runtime-address scheme sketched above. Fixing both should turn the first three sh2test lines green, and gives a concrete regression test for the rest.
  2. #1 — one-character fix per site, biggest correctness win on SH-4, and the PR=1 arms need a read-through once they become live.
  3. #3 — bounded work, removes a host-dependent behaviour difference that will otherwise poison any FP regression testing.
  4. #9 — small and mechanical, removes an interpreter/DRC divergence.
  5. #4, #5, #6 — small FP correctness fixes.
  6. #2 — needs a design decision; worth settling before chasing further FP bugs since it sets the accuracy floor for all of them.
  7. #11 — cheap now, dangerous later.

Still unexplained by this audit

The same sh2test run also reports failures I did not predict and which are outside what this pass covered. Recording them so they aren't mistaken for known issues:

  • MAC.W Math / MAC.L Math — I verified both against the SH-2 manual pseudocode and believe the interpreter is right, so these need their own investigation. Note the neighbouring MAC.* Read Order (Open Bus) and MAC.* Read Order (Cache) failures: MAC reads @Rn+ before @Rm+ in MAME, matching the manual, but the real ordering interacts with the bus and cache, which MAME does not model.
  • WDT Interval Timer, FRT (SH-2 M), FRT (SH-2 S) — peripherals (sh7604_wdt.cpp, the FRT in sh7604.cpp), not instruction semantics.
  • DIVU — the SH-2 on-chip divider unit peripheral, not the DIV0U/DIV1 instructions, which I verified as correct.
  • MA/IF Contention — pipeline/bus timing, not modelled.

Whether these are also DRC-only was not stated; if any of them additionally pass with -nodrc, that would point somewhere this audit did not look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment