The sweep after honouring BP/RHBP came back clk 16.5-17.6 MHz, 0/8 seeds
passing against the 24 MHz target -- from ~29 MHz before. The build was
unusable.
Cause: the sanitised bounds were combinational, so
bp/rhbp -> comparators -> Mux -> subtract -> pg_total -> add -> raw
-> compare -> free -> compare -> have_room
became ONE path where it had been constant-folded. Two fixes were needed, and
the first alone was not enough:
1. Register pg_first/pg_last/pg_total and rrp_s. They change only when the GC
writes BP/RHBP/RRP, so a cycle of staleness costs nothing, and `free`
already reaches the FSM through the registered have_room. This got it to
~20 MHz, still failing.
2. Narrow them from 8 bits to 4. Ring pages are 1-15. An 8-bit variable-vs-
variable comparison is a carry chain where variable-vs-CONSTANT folds into
a couple of LUTs -- precisely what making the bounds dynamic had thrown
away. geom_ok guarantees bp < rhbp <= 0x0F, which makes the narrow slice
safe.
Now 26.90 and 28.50 MHz on seeds 1-2, against 28.00/29.21 for the build
without the geometry change.
Attribution needed an A/B: the register-space decode (item 4) landed in the
same stretch and had never been swept on its own. Measured separately it
closes 28.0-29.2 MHz and costs nothing, so the whole regression was the
assembler. The critical-path report named rx_frame_assembler.py:215 -- the
rrp clamp -- which is what pointed at the comparator width rather than at the
arithmetic everyone would suspect first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Compat TODO item 2, the last silent-corruption risk. RXFrameAssembler took
the RX ring as a compile-time constant (pages 1-15), so a driver configuring a
SMALLER ring was ignored and we kept writing pages it would never read, with
nothing reporting an error. The bounds now come from bp/rhbp, wired in BBATop
from the GC's BP (0x0A/0B) and RHBP (0x1A/1B) registers, and both the page wrap
and the free-space calculation use them.
Garbage is rejected rather than obeyed: the driver's values are used only when
bp >= 1 (page 0 is the TX buffer), rhbp <= 0x0F and rhbp > bp. Otherwise the
hardcoded layout stands and geom_honoured goes low, so a mismatch is
observable. Without that check BP=0 or an RHBP below BP gives a zero or
negative ring size and the assembler writes outside it.
⚠️ Recorded as a known limitation: a ring must be LARGER than
_MAX_FRAME_PAGES (6) or nothing is ever stored, because a frame's length is
unknown until EOF so the worst case (1518+4 = 6 pages) is reserved up front.
Found the honest way -- the test was written with RHBP=4 first and passed
VACUOUSLY, storing nothing at all. Making that work needs a length-aware
reservation, which is a bigger change than this one.
T7 (8-page ring honoured, frames only in pages 1-8, and at least one actually
stored) and T7b (all three garbage cases rejected, fallback still stores).
Mutation-tested: wrapping at the hardcoded page 15 writes pages 9-11 and fails
T7; removing the sanity check fails all four T7b assertions.
Item 1 (RX filtering) remains the only open gap.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Compat TODO items 3 and 4. Every register libogc2 writes now stores writes and
reads back, with the documented hardware reset defaults: NCRB (0x01), BP
(0x0A/0B), TLBP (0x0C/0D), RXINTT (0x14/15), RHBP (0x1A/1B), NWAYC (0x30,
default 0x84), GCA (0x32), MISC (0x3D, default 0x3C), MISC2 (0x50). BP and
RHBP reset to OUR layout (page 1, page 15) so a driver reading them back
without writing sees something consistent with where the ring actually is.
Previously all of these read 0x00 and dropped writes, so a driver doing
read-modify-write or sanity-checking a default saw zero -- and libogc2 does
read NWAYC before writing it.
Deliberately still reading 0x00: LTPS, LRPS, TWP, TRP, TXFIFOCNT. Those are
STATUS registers, not configuration, so storing what was written to them would
be a lie rather than a default. Drivers read LRPS/LTPS only on error bits and
0x00 reads as "no error", which is truthful for a device that does not detect
them.
NCRB, BP and RHBP are also exported as BBARegisterFile.ncrb/.bp/.rhbp for the
RX-filtering work (item 1) and the ring-geometry check (item 2). Nothing
consumes them yet -- the decode lands first so the behaviour that uses it is a
separate, testable change.
T15 checks the defaults, replays libogc2's twelve init writes in its order and
reads every one back, and asserts the NCRB export is 0x52 with PR=0, PM=0,
AB=1 -- i.e. that the real BBA delivers own-MAC + broadcast only, which is the
fact item 1 has to act on. Mutation-tested: dropping NWAYC's 0x84 default
fails it, as does dropping NCRB writes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records the differences between this implementation and what real GC software
expects, with the evidence and the driver source for each, so they are worked
through rather than rediscovered. Open items: no hardware RX filtering,
hardcoded ring geometry, NCRB undecoded, and registers that read 0x00 where
real hardware has non-zero defaults. Also records what is deliberately NOT to
be implemented (the device-initiated challenge/response) and that two of our
registers (TWD, HIPR) do not exist on real hardware.
Notes explicitly that games use the official Nintendo SDK driver, which none of
the three readable drivers is, so "no open driver does this" is weaker evidence
than "the hardware has no such register".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>