Fix the 0/8 timing failure the ring-geometry change caused

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>
This commit is contained in:
2026-10-10 14:16:43 +02:00
parent 147ef8fcfc
commit 6b2e6724a2
2 changed files with 70 additions and 14 deletions
+28
View File
@@ -87,6 +87,34 @@ fallback layout 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.
⚠️ **This change cost a 0/8 timing failure before it was tuned, and the shape
of that is worth remembering.** The first version computed the sanitised
bounds combinationally, which put
```
bp/rhbp -> comparators -> Mux -> subtract -> pg_total -> add -> raw
-> compare -> free -> compare -> have_room
```
in ONE combinational path where it had previously been constant-folded. `clk`
fmax collapsed **29 → 17 MHz and all eight seeds failed** the 24 MHz target.
Two things were needed:
1. **Register the bounds** (`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`. That alone got it to ~20 MHz — still failing.
2. **Narrow them to 4 bits.** 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 — which is exactly what
was lost by making the bounds dynamic. `geom_ok` guarantees
`bp < rhbp <= 0x0F`, which is what makes the narrow slice safe.
Result: 26.9–28.5 MHz on the two sampled seeds, against 28.0–29.2 for the
build without this change. **Always sweep after touching the assembler's
ring arithmetic** — and note the register-space decode (item 4) was measured
separately and cost nothing, so attributing a regression needs the A/B.
For the record, libogc2 writes `TLBP = 0x0000`, `BP = 0x0001`,
`RHBP = 0x000F` — exactly the old hardcoded layout, which is why this never
showed up in practice.