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.00sh3.pdf— SH-3/SH-3E/SH3-DSP Software Manualsh4_software_manual.pdf— SH-4 Software Manual rev 6.00sh4a_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.
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) |
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.
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:2230–2340) 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 with0xe0000000. 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.
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" value0x80000000for both positive and negative overflow — so a large positive float comes back negative. - AArch64 (
fcvtzs) saturates, so the same program returns0x7FFFFFFF— 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 NINF → 0x80000000
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.
// sh4.cpp:2288
if (FP_RFD(m) == 0) return; // PR=1
...
if (FP_RFS(m) == 0) return; // PR=0Per 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.
// 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.
// 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 - FRnThe 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.
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, correctBut 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.
// 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.
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*4so 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.
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.
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; // RTEDRC (sh.cpp:3726 and sh.cpp:3467):
UML_MOV(block, mem(&m_sh2_state->sr), I0); // no & SH_FLAGSSH_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.
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.
These are currently harmless — desc.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.
1111nnnnmmmm1111: interpreter callsdbreak()→machine().debug_break()(sh4.cpp:2961); DRC returnstrueand emits nothing, i.e. a silent NOP (sh4.cpp:5083).1111nnnn110[01]1101: interpreterdbreak()(sh4.cpp:2444); DRC returnsfalse, which routes tocfunc_unimplementedand 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.
Not results, but noted while reading:
ORI(sh.cpp:1307) chargesicount -= 2andORM(sh.cpp:1314) charges nothing. The manual givesOR #imm,R0= 1 cycle andOR.B #imm,@(R0,GBR)= 3 (sh2.pdf p.4689/9161). The two are swapped.ANDI/ANDM,XORI/XORMandTSTI/TSTMare all correct, so this is an isolated slip. The DRC getsORIright.JMP(sh.cpp:778) has itsicount--commented out with the note "not in SH4 implementation?"; SH-2JMPis 2 cycles.sh_fe.cppalso leavesJMPandJSRat the defaultcycles = 1.
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.
#else
UML_MOV(block, FP_RFS(REG_N), 0); // sh4.cpp:5445, 5457
#endifFP_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.
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.
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.cpp — ADDC/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 interpreter — SHAD 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.
- #8, then #7 — the only findings with a confirmed failing test behind them
(
sh2teston Saturn).MOVAis a two-line change; the dynamic-branch case needs the sentinel/runtime-address scheme sketched above. Fixing both should turn the first threesh2testlines green, and gives a concrete regression test for the rest. - #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 — bounded work, removes a host-dependent behaviour difference that will otherwise poison any FP regression testing.
- #9 — small and mechanical, removes an interpreter/DRC divergence.
- #4, #5, #6 — small FP correctness fixes.
- #2 — needs a design decision; worth settling before chasing further FP bugs since it sets the accuracy floor for all of them.
- #11 — cheap now, dangerous later.
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 neighbouringMAC.* Read Order (Open Bus)andMAC.* 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 insh7604.cpp), not instruction semantics.DIVU— the SH-2 on-chip divider unit peripheral, not theDIV0U/DIV1instructions, 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.