Reversing the SH-4 fpu with the help of three AI models

Posted in dreamcast, fpu

Intro

The SH-4 in the Dreamcast has four ‘special’ floating-point instructions that exist to accelerate 3d graphics:

  • FIPR: 4-element inner product
  • FTRV: 4x4 matrix times vector
  • FSCA: sine and cosine approximation of a 16-bit angle
  • FSRRA: square root reciprocal approximation

FIPR and FTRV are documented by the Ranesas/Hitachi manuals, while FSCA and FSRRA are documented for SH4A, which has a different pipeline. They are all “approximate” instructions.

It’s been a puzzle to me, and others in the Dreamcast emulation scene, on how exactly those instructions can be replicated and what is their likely internal structure.

For emulation purposes, it is not critical to get them bit exact, however doing so can fix many corner cases such as game intros. Game intros are often recorded from a developer/tester playing the game, and then just playback the inputs to re-create the intro sequence. Small bit errors from these instructions accumulate and often lead to intros not playing back correctly.

I used Fable 5.1, Opus 5, and GPT 6 Astra to brute force the problem space, and some intuition based on the docs and likely hardware structures I had.

At this point, I also have to thank MetalliC for providing 2.15 bilion fsrra input/output pairs, and reminding me of this open question!

You can find the bit-exact C solution here

Overall shape of the reversed pipeline

Pipeline Diagram

The TL;DR is: The hardware consists of one block of four truncated Booth multipliers, four aligners, a four-input adder and a single rounding stage, with latches that enable hold and feedback in the multipliers.

FIPR uses it once, FTRV is a microprogram that uses it four times, FSCA and FSRRA are microprograms that do a piecewise cubic, feeding product registers back as operands, and use a what seems to be 17-word and a 34-word coefficient ROMs.

FSCA and FSRRA are almost free. They add ~3.6 kbit of ROM, and a few feedback multiplexers.

This has been validated over

  • FIPR: 32M vectors + 5.2M edge vectors (DN = 1), 1M denormal-range vectors (DN = 0), for both rounding modes
  • FTRV: 524,288 matrices x 4 rows, for both rounding modes
  • FSCA: All 64Ki angles, for both rounding modes
  • FSRRA: [1,4) range in both rounding modes, all positive normal inputs for round to nearest.

In addition, 3114 test vectors were used to characterize the FPSCR behavior.

How “approximate” are they?

The graphs bellow showcase the block as implemented, vs a non-truncated multiplier implementation, and against the ground truth for FSCA and FSRRA.

Share of correctly rounded results per instruction

FIPR error histogram: hardware versus IEEE float sequence

FTRV error histogram for a rotate+translate matrix row

FIPR and FTRV are more accurate than doing the same arithmetic with IEEE multiplies and adds. A program that computes a0*b0 + a1*b1 + a2*b2 + a3*b3 with ordinary float operations rounds seven times; the SH-4 block keeps 28-bit products and rounds once.

For FSCA and FSRRA the truncated multipliers don’t cause noticeable precision loss.

FSCA error across the first quadrant

FSCA’s is less than 1.88 units of 2^-24, and 54 % of all angles are correctly rounded. This is over the 64Ki input though, which is already a quantized input angle.

FSRRA error over the mantissa range

FSRRA also stays below 2 ULP everywhere (worst case 1.99), with 67 % of inputs correctly rounded.

What does the truncation buy?

While the true chip implementation is unknown, here are some approximate numbers via yosys/nand2.

Rough gate counts

The SH-4 block is about 30 % smaller than the correctly rounded fused design, and 28 % smaller than the IEEE composition. About 5,000 of the 8,000 gates saved are in the four multipliers; the rest comes from everything behind them: 28-bit instead of 48-bit products mean the aligners, the four-input adder, the leading-zero counter and the normalising shifter are all some 20 bits narrower.

What about speed?

The truncated multipliers don’t have much of a speed advantage.

Logic depth per pipeline stage

Note these are a simplification, a 90s chip implementation differs from the yosys/nand2 proxy.

Edge case behavior

A few behaviors the manual does not mention:

  • Every finite result of all four instructions raises the inexact cause and flag, even 1.0 * 1.0;
  • Any zero result of FIPR/FTRV also raises underflow, even 0 * 0
  • FSCA and FSRRA trap for any operand due to the inexact flag
  • FIPR traps unconditionally on any of the overflow / underflow / inexact enables, FTRV additionally on the invalid enable
  • With FPSCR.PR = 1 all four instructions are complete no-ops, with registers and status untouched, but they still trap if the right enable bit is set
  • With FPSCR.DN = 0, FIPR and FTRV do not raise an FPU error on denormals: they compute with them and return denormal results.
  • FSRRA, does trap on a positive denormal
  • FTRV Booth-recodes the matrix element; FIPR recodes its second operand. This is important for bit-precision.
  • Two infinities of the same sign in FIPR take the sign of product 0, whatever product 0 is. -inf - inf can be +inf.

Pipeline details

FIPR (1 pitch / 3 latency) / FTRV (4 pitch / 7 latency)

four 24 x 24 truncated Booth multipliers      -> four 28-bit product registers  (+2^19, drop 20 bits)
four aligners (shift to the largest exponent, invert if negative: no guard bits, plain truncation)
one four-input adder
one normalise / round stage                   -> the only rounding in the whole instruction

FSRRA (3 pitch / 6 latency)

node = rounded top 5 bits of the mantissa, dx = the signed remainder
step 1   dx * dx -> sq                dx * REC1 -> linear term
step 2   sq * C2 -> square term       sq * C3   -> I
step 3   I  * dx -> cubic term        
adder: C0 - linear + square - cubic, round once

FSCA (4 pitch; sine latency 6, cos latency 7 reusing the held latches)

step 1   dx * dx -> sq      dx * L[cos word]        dx * KC (KC ~ 4 pi, one global constant)
step 2   sq * dx -> cube    (dx * KC) * dx -> J
step 3   cube * Q[cos word] J * L[sin word]
adder: C0 + linear - even - cubic

FSCA rom: 1,071 bits; each word holds values derived from one sin(m pi/32). FSRRA rom: 2,550 bits

Timeline

(This is partially transcribed from the Claude/GPT logs)

The effort took about two days, roughly 8M tokens of model compute, and significant cpu time to run the exhaustive searches and simulations. I was attentive to the models around 20 hours during that span.

Fable 5.1 was used as the primary interactive research tool, through the claude code vscode plugin, and GPT 6 Astra / codex was used to cross verify results. Opus 5 was used in “hypercode/swarm” mode to launch two wide explorations in the problem space (~6M tokens were spent this way).

Prehistory

An 8.5 GB dump of FSRRA over every positive float, captured from a Dreamcast years ago, and a 32,768-entry FSCA from nullDC.

Day 1, 00:58 — “can you write a c++ model for the fsrra instruction, that matches it bit for bit” (Claude Fable 5.1)

Within hours: the result depends only on the mantissa and the exponent’s parity; 17 nodes per parity; a cubic; products truncated separately; round to nearest on two guard bits. Fitted coefficients reach 99.63 %. Every miss is exactly one ULP.

07:13 — the question that set the direction

“could this re-use the same multiplies used for fipr internally -on top of the table-? fipr is ab+cd+ef+gh”

09:27-11:00 — FIPR first, and soft-float only

A first hardware test showed the manual’s FIPR pseudo-code is wrong in places (thanks to Bruce for running it for me!)

I had to get my lazy ass to setup my Dreamcast with a bba for the rest of the testing.

After a false start with Python floats:

“let’s take a step back. Let’s characterize fipr first, and use c++ and soft fp / bit math, not python / native floats to make sure we get this right. I think a dump of 64 M or so of fipr vectors is in order, first?”

That helped the model to correctly validate the implementation using soft-floats. On the same day: 32M vectors, 0 mismatches, and the truncated Booth array as the first fully understood piece of silicon.

11:00-17:00 — FSCA resists

Fitted constants, per-node corrections, piecewise-linear tables, Taylor series, exotic separate units. The model got closer and less believable. I pulled the claude back twice/

“my goal for now is understanding how the hardware worked, not a similar-but patched implementation. I think we’re getting something fundamentally wrong”

“can you write a harness to measure the latency and pitch of instructions from the hw? I do not trust the sim.”

The timing harness gave hard numbers which later fixed the number of multiply steps each microprogram may use.

At this point I handed over to GPT Astra, which validated the work so far found a couple of bugs. Still no breakthrough.

US 6,038,582 shows the block: four multipliers, aligners, a four-input adder, feedback paths. With the FIPR array plugged into the FSCA terms with zero free parameters, whole-dump mismatches dropped from 490 to 176; FSRRA went from 0.345 % to 0.021 % once its cubic was evaluated coefficient-first through a feedback latch.

What remained was small and stubborn: 147 of 32,768 FSCA samples, all in the even (cosine-like) term.

Day 1 19:06 -> Day 2 10:14 — the Opus 5 swarms

A couple of fresh Opus 5 sessions were told to re-validate everything and try for a breakthrough. They ran several multi-agent workflows ~ 250 sub-agents in total. Each family of hypotheses was paired with an adversarial verifier. It localised the whole residual to one node-independent latch (“J”), but made no further breakthrough.

Handing over from there to GPT Astra the question got reframed : “change the question from ‘what correction produces J?’ to ‘which hardware assumption makes J look wrong?’”

The swarms concluded: “the missing quantity must be a stored value” which was rigorous but conditional on a model that still contained a wrong assumption. Score: still 147.

Day 2 10:15 — “something structuraly wrong… some ‘bithack’” (Claude Fable 5.1)

“FSCA, FSRRA and FIPR are very likely to share the same pipeline, just enhanced. Can you implement FIPR, FSCA, FSRRA as modular system verilog, and then based on that try to identify what simplifications or structural differences could lead to a model that matches”

Writing the three instructions as RTL on one datapath forced every term into “a product register, shifted, into a latch”, reproduced FIPR exactly including its latency, and turned the models’ sign conventions into one hardware rule. Then several variations were attempted with no breakthrough.

13:21 — the redirect

“so this looks like a dead end, for now. would investigating further the fsrra path (to either find a common structure, or discover a new structure it may be using that may be also used by the cosine) make sense?”

It did. FSRRA has 512 times more data per node than FSCA and no sign asymmetry. After fixing three wrong table entries, its residual showed a clean scaling law: the error grew as 2^k with the model’s normalisation exponent and vanished where the exponent was smallest. The silicon never normalises that coefficient; it is a plain 14-bit fixed-point field. Fixing it exposed a second error it had been hiding: the cubic chain reads the same square latch as the square term. FSRRA went exact on 16.7M inputs in both rounding modes, then on all 2.13 billion inputs of the old 8.5 GB dump.

However, Fable 5.1 failed to figure out fsca through the new discoveries. So I handed over to Astra.

15:21 -> 15:26 — five minutes (GPT Astra)

“Make breakthrough progress, by combining all previous results and new knoewledge from frssa structure.”

From the Astra log:

  • 15:21 — baseline reproduces; “One gap remains in the handover: forming the even term from parallel products of ROM fields and the signed offset, with both feedback latches shifted by three.”
  • 15:25 — “reusing the complementary word’s linear coefficient in place of the assumed sine field lowers one candidate to 86 violations.”
  • 15:26 — “zero sine or cosine mismatches across all 65,536 angles in RN and RZ.”

The field all previous attempts had modelled as a stored sine for the even term didn’t exist. The ROM’s linear coefficient (sin * pi/2) is reused, the global constant is an economised 4 pi instead of 2 pi^2, and the latches follow FSRRA’s shift-by-three structure. The earlier notes even contained indications towards this: the “sine field” was the one coefficient the data barely pinned. Lesson learned: a weakly pinned field is a candidate for not existing.

Day 2 evening — flags, traps and the console again

“can you characterize the hardware, by writting a test for that”

Six small programs with an exception handler that records a trap and skips the instruction measured everything the captures never covered: special operands, every FPSCR setting, PR = 1, DN = 0, FTRV. That is where the surprises in the summary came from, and where FIPR’s denormal arithmetic (1M vectors, exact at the first natural hypothesis) and FTRV’s operand roles were figured out.

The human in the loop

While i’d like to think i was very useful during this, i gave few indicators and I think I actually led the research astray by focusing too much on fsca, thinking its smaller range would be simpler to crack first.

I also underestimated how “stuck” the models can get around these kind of problems, trying to solve them via mathematical/statistical analysis rather than reasoning “what would a 90s enigneer do?”.

The agents contributed massive volume and rigour: roughly a thousand throwaway source files (38,000 lines of C and C++ analysis tools), exhaustive scans over millions of structures, exact solvers, adversarial verification, in-place retraction of their own wrong claims, and the stamina to try the 657,800th configuration.

What is still open

  • How Hitachi generated the ROM constants. Several fields are biased by a few units. Why?
  • Interactions against the parallel IEEE pipeline(s) on the chip
  • How was this actually implemented in silicon