Files
virtualjaguar-libretro/docs/gpu-timing-spec.md
Joseph MattielloandClaude Opus 5.5 4bdc77972a docs(timing): S2 write-up -- report the CF canary failure, rerun disclosure, provenance tags
Records the Checkered Flag canary as a failed sanity check (knife edge kept
as a hypothesis), discloses the one re-run of the injection's low level,
scopes VJ_S2_INJECT to GPU waits, tags the ARB.NET excerpts as relayed by
a summarizing fetch (not verbatim, not simulated), and labels the 68K
overshoot mechanism as read from code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 01:26:17 -04:00

549 lines
29 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Jaguar GPU (TOM RISC) Cycle-Level Timing Specification
Ground-truth timing rules for the GPU cost model in virtualjaguar-libretro
(issues #401 / #313: emulated GPU finishes work 2-4x faster than hardware).
**Primary source:** `jag_sim/netlists/tom/*.NET` — these are the ORIGINAL
Flare/Atari design source files (netlist language with the designers' own
comments, dated 1991-92, including dated bug-fix notes). They are not
reverse-engineered: they are the files the TOM silicon was compiled from.
The MiSTer core (`Jaguar_MiSTer/rtl/Rework/*.v`, e.g. `sboard.v` module
`_sboard`) and `jag_sim/verilog/tom/*.v` are 1:1 mechanical translations of
these netlists into structural Verilog (Torlus's translator), so the .NET
files are strictly more readable and equally authoritative. The DSP in JERRY
uses the same pipeline blocks (`jag_sim/netlists/jerry/SBOARD.NET`,
`EXECON.NET`, `DSP_MEM.NET`, `DSP_GATE.NET`), so every rule below applies to
the DSP as well, with "local RAM" = 8K DSP RAM and the gateway talking to
JERRY's bus interface.
All costs are in **GPU ticks = system clocks** (26.590906 MHz NTSC; the GPU,
blitter, OP and memory controller all run in this one clock domain;
the 68000 runs at sysclk/2). Evidence tags:
- `[RTL file:lines]` — read directly from the netlist source.
- `[RTL-derived]` — follows from the netlist state machines by short hand
analysis (1-2 flops of reasoning); high confidence.
- `[JTRM prose]` — stated in the Technical Reference Manual.
- `[inference — needs simulation]` — bounded but exact count should be
confirmed with a Verilator run of jag_sim (recipe in §9).
Source citations below are relative to the root of a checkout of
[ElectronAsh/jag_sim](https://github.com/ElectronAsh/jag_sim) (the
`netlists/tom/*.NET` and `netlists/jerry/*.NET` files are the original
Flare design sources, comments intact). Clone it anywhere to follow
along; nothing in this repository depends on its location.
---
## 1. Pipeline shape (what the model must represent)
Three overlapping stages [PREFETCH.NET:9-21, EXECON.NET:8-18, SBOARD.NET:7-27]:
1. **Prefetch** — a 32-bit queue holding **two 16-bit instructions**; program
words are always fetched **in pairs from a long-word boundary**; a fetch
is initiated whenever there is room for two more instructions
[PREFETCH.NET:14-17, 227-241].
2. **Decode/execute** — one instruction is decoded through the microcode ROM
into a latched microword and executed in **one tick** when no wait is
asserted (`exe = vins & !wait & exec`) [EXECON.NET:80-93, INS_EXEC.NET:267-284].
3. **Write-back** — results (ALU, move, immediate, internal load, external
load, divide) are written to the register file **in a later tick**, and the
scoreboard *conceals* the write behind the read ports when it can; when it
can't, it stalls execution [SBOARD.NET:414-449, 523-541].
The emulator does not need to model the structure — only the stall rules
below, which are exactly what the structure produces.
---
## 2. Baseline issue rate
**R1. Every instruction issues in 1 tick when no hazard applies.**
ALU ops (ADD/SUB/logic/shifts/SAT/MOVE/CMP/BTST...), MULT/IMULT (the 16x16
multiplier is combinational — `MP16` [ARITH.NET:103-107]), MOVEQ, MOVETA,
MOVEFA, NOP: **1 tick**. Confirms JTRM "all instructions execute in one
cycle unless noted". [RTL: EXECON.NET:80-93; ARITH.NET:103-110]
Confidence: pinned.
IMULTN/IMACN/RESMAC also issue 1/tick — the product/accumulate is pipelined
(`macop_p`, `macop_pp` [ARITH.NET:71-73]) and the sequence is made atomic
with the next instruction [INS_EXEC.NET:210-223], so a MAC chain sustains
1 tick per IMACN. Confidence: pinned.
---
## 3. Scoreboard interlocks (the missing 30-50%)
The scoreboard compares the pending write-back address at pipe stage 1
against the source and destination fields of the **current** instruction and
suspends execution on a match [SBOARD.NET:20-26, 144-156].
**R2. ALU result-use interlock: +1 tick.**
If instruction N+1 *reads* the register written by instruction N (ALU-class
result, `reswr`), N+1 stalls exactly **1 tick** (`aludwait = alu_wback &
(dst-match & dst-read-enabled | src-match & src-read-enabled)`)
[SBOARD.NET:132-156]. The pending window is one stage deep (single
`alu_wbaddr` register), so an instruction two slots later pays nothing.
Note: the *destination* operand of a normal ALU op is also a **read** (it is
`dstrrd`), so `ADD r1,r2 ; ADD r2,r3` stalls, and so does
`ADD r1,r2 ; ADD r3,r2`. MOVE/MOVEQ write without reading dst (`dstrwr`,
`dstwen`) — `ADD r1,r2 ; MOVEQ #0,r2` does NOT match on dst-read but the
write-write is resolved by write-back priority, not a stall
[SBOARD.NET:106-130, INS_EXEC.NET:305-318]. Confidence: pinned.
**R3. Flags interlock: +1 tick.**
An instruction that *uses* the ALU flags issued immediately after one that
*loads* them stalls 1 tick (`flagwait = flag_pend & flag_depend`,
`flag_pend` = `flagld` delayed one tick) [SBOARD.NET:405-412].
`flag_depend` = conditional JUMP/JR (condition field non-zero) or ADDC/SUBC
[INS_EXEC.NET:448-457]. **The canonical `CMP ; JR cc` pair therefore costs
+1 tick.** Put one instruction between the compare and the branch to avoid
it. Confidence: pinned.
**R4. Write-back port steal: +1 tick (second-order).**
A write-back is invisible when it can go out on a register port the current
instruction is not using, or whose address matches the operand being read.
If the current instruction reads **both** ports and the pending write-back
address matches neither, or if **two** write-backs mature in the same tick,
execution stalls 1 tick (`wbwait = (!wbdeq & !wbseq & bothen & wback) +
mult_wback`) [SBOARD.NET:523-541, 414-449]. This mostly fires in
load-heavy code where an internal/external-load write-back lands while a
two-operand instruction executes. Confidence: pinned (condition), frequency
in real code needs simulation.
---
## 4. Loads and stores
Decoded `memrw` instructions request a local-bus cycle; the local bus
arbitration priority is (highest first): **I/O (68K register access) >
gateway (returning external program data) > data (load/store) > program
fetch** [GPU_MEM.NET:30-35, 121-127]. A local memory transfer is a one-tick
cycle following the arbitrated request [GPU_MEM.NET:128-143].
**R5. Internal load (GPU local RAM / GPU-visible internal regs): 3-tick
latency, non-blocking, max 1 in flight.**
"Internal loads take three ticks, but only one of these may be pending.
(This also applies to stores.)" [SBOARD.NET:177-184 — designer's words].
Timeline: T0 `exe` + `datreq`; T1 RAM cycle + `datack`; T2 data pipelined
(`mem_data` [GPU_MEM.NET:249-252]) and written back. Execution continues
underneath. Stalls only when:
- the load's **target register is read** before write-back (`ldwait` via
`dlaeq/slaeq` while the load engine is not idle) [SBOARD.NET:236-245];
- **another load/store issues** while the engine is busy (`ldwaitt[2] =
!idle & memrw`) [SBOARD.NET:244] — so back-to-back local loads sustain
**1 load per 2 ticks**;
- `LOAD rX,rY ; <op using rY>` immediately: ~2 stall ticks
[RTL-derived].
Confidence: pinned.
**R6. Indexed addressing (`(R14/R15+n)`, `(R14/R15+Rn)`) — address
precompute adds 2 ticks before the memory request.**
These opcodes set `precomp` (instructions 43,44,49,50,58-61
[INS_EXEC.NET:361-365]); the load engine walks `comp1`, `comp2` (address
through the ALU) before raising the request [SBOARD.NET:223-258, 264-271].
Indexed **local** load: request at T2, RAM cycle T3, data T4 → engine busy
~4 ticks; a dependent use right after costs ~3-4 stalls. Confidence:
RTL-derived (state machine explicit); exact overlap with next instruction —
needs simulation for the ±1.
**R7. Indexed STORE: +1 tick on the instruction itself.**
"An extra wait state is generated for computed address cycles, to read this
data" (`compdwait`) [EXECON.NET:173-189]. So `STORE rN,(R14+n)` costs 2
ticks of issue even before any bus effect. Plain STORE issues in 1 tick.
Confidence: pinned.
**R8. Local STORE: fire-and-forget, engine busy ~2 ticks.**
Store data is latched and the RAM cycle happens in the background; the next
instruction runs unless it is itself a load/store (R5 busy rule).
Confidence: pinned.
**R9. External LOAD (DRAM/cart/TOM regs): non-blocking, up to 2 in flight,
latency = local handoff (2) + bus grant + memory cycle + return (2).**
Mechanism: the local cycle "completes with no data" (`del_xld`
[GPU_MEM.NET:183-192]), the **gateway** takes over and raises a bus request,
and the scoreboard tracks the outstanding target register(s)
[SBOARD.NET:273-343, GATEWAY.NET:75-137]. Costs:
- Pipeline keeps executing. Stall only when: target register of either
outstanding load is read (`xlddwait`) [SBOARD.NET:325-339]; a **third**
load is issued (`twold`) ; **any load/store issues while the gateway is
active** (`mbusywait = memrw & (gate_active + twold) ...`); or a **store
issues while any external load is pending** (`datwe_raw & (oneld+twold)`)
[SBOARD.NET:345-357].
- Absolute-best-case latency from `exe` to register-usable, bus already
owned by TOM, DRAM page hit: **~7-8 ticks** — T0 exe, T1 local serv
(del_xld), T2 gateway active + breq, T3 arbitration latch (`arben` at ack
boundary [ARB.NET:119-124]), T4-T5 DRAM page-mode cycle
[MEM.NET:150-165,185-197], T5 data latched (`ddatld`), T6 `xld_ready`
(FD1Q delay [GATEWAY.NET:160-168]), T7 write-back. [inference from RTL
chain — needs simulation for the exact constant]
- Add **bus-grant latency** if TOM does not own the bus (§7).
- Add DRAM page-miss / ROM wait states (§8).
Confidence: mechanism and stall conditions pinned; the constant
"7-8" needs simulation (bound: 6-9).
**R10. External STORE: non-blocking issue (1 tick), but the gateway stays
`gate_active` for the whole bus transaction; the *next* load/store stalls
until it completes.** Write data is enabled the tick after the bus ack
[GATEWAY.NET:253-260]. So isolated external stores are nearly free to the
GPU; a burst of external stores runs at bus speed (one per grant+cycle).
Confidence: pinned (mechanism); per-store bus cost from §7/§8.
---
## 5. Jumps
**R11. The instruction after a jump ALWAYS executes (mandatory delay slot).**
The jump is latched and only passed to the prefetcher when the next
instruction is ready, precisely so programmers can rely on the delay slot
[PREFETCH.NET:67-99 — designer's words: "To allow programmers to rely on the
instruction after jump being executed"]. Jumps are atomic with the following
instruction (no interrupt between) [INS_EXEC.NET:141-208].
**R12. Taken JUMP (Rn): ~4 ticks for {jump + delay slot} when running from
local RAM** = 1 (jump) + 1 (delay slot, useful work) + **~2 dead ticks**
while the queue is flushed (`force0` [PREFETCH.NET:129]) and the pair at the
target is fetched (request, RAM cycle+ack, queue-ready). Net penalty vs.
straight-line: **+2 ticks**. [RTL-derived from PREFETCH queue/ack chain —
needs simulation for the exact refill count (bound 2-3)]
**R13. Taken JR (relative): +1 tick over JUMP.**
The relative target is computed from the *pipelined* source one tick later
(`jrel`), and `insrdy` is masked while `jrel` is active
[PREFETCH.NET:82-99, 174-179]. Net penalty **+3 ticks**. Same confidence
as R12.
**R14. Not-taken JR/JUMP: 1 tick** (condition evaluates false, no queue
flush) — plus the R3 flags interlock (+1) if it directly follows the
flag-setting op. Confidence: pinned.
If the jump target/stream is in external memory, replace the 2-tick refill
with a full external program fetch (§7, §8; and R18).
---
## 6. Divide, MMULT, MOVEI
**R15. DIV: 1 tick issue + 16 ticks background; result usable at tick ~18.**
Non-restoring divider computes **2 bits per tick**; `div_active` lasts
**16 ticks** after `div_start` [DIVIDE.NET:7-10, 240-262]; write-back is
requested in the first tick after `div_active` drops [SBOARD.NET:386-396].
Stalls only when: the destination register is read while the divide runs
(`divdwait`), or a **second DIV issues before completion** (`diviwait`)
[SBOARD.NET:369-403]. Reading the remainder register (`G_REMAIN`) is an
internal load and should also only be done after completion. So
`DIV r1,r2 ; <17 independent instructions>` hides the divide completely;
`DIV r1,r2 ; use r2` costs ~17 stall ticks. Confidence: pinned.
**R16. MOVEI: 3 ticks minimum** (opcode + two immediate words each consumed
on `insrdy` through states `imm1`, `imm2` [EXECON.NET:118-133]); it also
drains 2 extra words from the prefetch queue, so out of local RAM with the
queue previously full it is exactly 3; when fetch-starved it is longer.
Interrupts cannot split it [INS_EXEC.NET:141-208]. Confidence: pinned.
**R17. MMULT (systolic matrix multiply): per-element pipelined memory read +
MAC; N-element column ≈ 2 ticks/element + setup; it holds the data-request
path (`mtx_mreq` ORed into `datreq` [INS_EXEC.NET:601-606]) and other memory
ops stall behind it (`mbusywait` gate) and interrupts are locked out
(`mtx_atomic`).** [SYSTOLIC.NET; INS_EXEC.NET:155-158, 234-241]
Confidence: mechanism pinned; per-element constant [inference — needs
simulation].
---
## 7. External bus: arbitration, grant latency, blitter/OP interaction
**R18. Bus priority (highest → lowest), definitive list**
[ARB.NET:85-95, matches JTRM]:
| pr | master |
|----|--------|
| 10 | daisy-chained external bus master |
| 9 | DRAM refresh |
| 8 | DSP (high priority, DMAEN) |
| 7 | **GPU (high priority, DMAEN)** |
| 6 | Blitter (high priority, BUSHI) |
| 5 | **Object Processor (video fetch)** |
| 4 | DSP (normal) |
| 3 | 68K under interrupt |
| 2 | **GPU (normal)** |
| 1 | Blitter (normal) |
| 0 | 68K (always requesting; default owner) |
**R19. Re-arbitration happens at every memory-cycle ack** (`arben = q2 &
ack` — ownership can change only when TOM holds the bus off the 68K and the
memory state machine finishes a cycle) [ARB.NET:105-124]. Consequences:
- **GPU (2) vs. blitter at normal priority (1): the GPU WINS.** A running
blit does NOT block a GPU external access beyond the *current* memory
cycle: the blitter requests the bus continuously for the whole blit
(`blit_breq` active while outer loop busy [OUTER.NET:134-141]) but the
GPU's pending request preempts it at the next ack boundary. GPU grant
latency over a normal-priority blit ≈ remainder of the in-flight bus
cycle (0..cycle_len-1) + 1 arbitration tick. The blit stretches by the
GPU's cycles (this is the "GPU steals blitter cycles" direction, not the
reverse).
- **BUSHI blits (pr 6) beat normal GPU (2):** the GPU's external access
waits until the blit **finishes** (or until refresh preempts) unless the
GPU sets DMAEN (pr 7 > 6), which is the documented escape hatch: FLAGS
bit 15 gives GPU data transfers DMA priority [GATEWAY.NET:66-73,272-283].
- **OP video fetch (5) beats normal GPU (2):** during the active-display
portion of a line, OP object fetches win every arbitration; GPU external
accesses effectively wait for horizontal gaps. On text/HUD-heavy lines
this is the dominant stall for GPU external traffic. [RTL: priority list;
magnitude needs simulation/measurement]
- **68K interaction:** the 68K is the default owner. When no internal
master held the bus, TOM must raise BR and wait for BG
(state machine 0→1→2 [ARB.NET:33-45]); the 68K grants between its own
bus cycles — add **~2-12 sysclks** (a 68K bus cycle is 4 CPU clocks = 8
sysclks) for the handoff when the 68K was running. When a daisy-chained
master releases, the bus returns to the 68K first [ARB.NET:126-134].
[RTL + 68K datasheet; exact distribution needs simulation]
**R20. BUS_HOG (G_CTRL bit 11): after an external program fetch the GPU
holds its bus request for 4 extra ticks** so consecutive external fetches
chain without re-arbitrating [GATEWAY.NET:262-270]. Confidence: pinned.
**R21. GPU program fetch and data load may both be pending externally at
once** (gateway handles one bus transaction at a time; program returns via
`gatereq` onto the local bus, stealing one local-bus tick from the next
fetch/data cycle) [GPU_MEM.NET:16-28, 150-213]. Confidence: pinned.
---
## 8. Memory-cycle lengths (TOM memory controller)
State machine in [MEM.NET:150-273]; one state = 1 sysclk; `ack` overlaps the
idle/branch state, so charge the states between grant and ack:
| cycle | length (sysclks) |
|---|---|
| DRAM read/write, **page hit** (`match`) | **2** (states 3a,3b) per 1-4 sub-cycles depending on bus width vs transfer width |
| DRAM **page miss**: RAS precharge + row access first | + (2..4) + (1..3) = **+3..+7** by `dramspeed` (MEMCON1): speed0/1 → 4+3, speed2 → 3+2, speed3 → 2+1 [MEM.NET:167-183, 339-361] |
| CAS-before-RAS refresh | ~9 states, steals the bus at pr 9 [MEM.NET:199-215] |
| ROM (cart) | 2 + wait{2,3,5,7} by `romspeed` (MEMCON2); fastrom skips waits [MEM.NET:217-234, 428-442] |
| fast internal device (TOM/JERRY regs) | write 1, read 2 [MEM.NET:236-249] |
| external I/O device | 2 + wait{1,3,7,15} by `iospeed` [MEM.NET:251-266] |
A 64-bit-wide DRAM bank transfers 64 bits per column sub-cycle; narrower
widths run multiple sub-cycles (`MEMWIDTH` breaks the transfer up, `nextc`
per sub-cycle) — e.g. a phrase access to 16-bit cart ROM = 4 sub-cycles.
The GPU's external data accesses are ≤32-bit (`msize`), i.e. one sub-cycle
on the 64-bit DRAM. Actual `dramspeed/romspeed` values are whatever the
boot ROM programs into MEMCON1/MEMCON2 — read them from the emulated
registers rather than hardcoding. Confidence: state machine pinned; consult
MEMCON defaults at runtime.
**R22. Executing GPU code FROM external memory:** every 2 instructions cost
a full external bus transaction (grant + cycle + gateway return + 1 local
bus tick), lower priority than data loads; with BUS_HOG the transactions
chain. Order of magnitude: **6-15+ sysclks per instruction pair** vs 1 from
local RAM. [RTL-derived; needs simulation for tight numbers]
---
## 9. Fetch starvation from local RAM (why 1/tick is the *ceiling*)
The GPU local RAM (1K x 32, single port [GPU_MEM.NET:325-334]) serves
program fetch at the LOWEST local priority [GPU_MEM.NET:30-35]. A pair
fetch (2 instructions) takes a request tick + a service tick, so
straight-line code exactly keeps the 2-deep queue full: **sustained 1
instr/tick is achievable**, and every scoreboard stall gives the prefetcher
headroom. But each load/store *to local RAM* also consumes one RAM service
tick at higher priority, and 32-bit instructions (MOVEI) drain 3 words, so
dense load/store+MOVEI sequences intermittently empty the queue (1-tick
bubbles). [RTL-derived; per-pattern exact bubbles need simulation]
---
## 10. Interrupt entry
The interrupt control unit hijacks the instruction stream and injects the
call sequence (`intser` overriding the prefetch output
[INS_EXEC.NET:225-247]); it cannot break MOVEI, MMULT, IMULTN/IMACN pairs,
or jump+delay-slot (atomic terms [INS_EXEC.NET:141-223]). Entry cost = the
injected sequence (store return address, redirect) ≈ 4-6 ticks + refetch at
the vector. [inference — needs simulation]
---
## 11. Contrast: current emulator model (what to change)
`src/tom/gpu.c` today:
- `gpu_opcode_cycles[64]` is **all 1s** (gpu.c:178-188) — correct only for
R1; DIV, MOVEI, indexed stores, jumps are all charged 1.
- `GPU_EXT_ACCESS`/`bus_arbiter_charge_access` (gpu.c:59-63) charges a flat
external-access cost — the only non-unit cost in the model, and it
charges it **synchronously** (blocking), whereas hardware external loads
are split-transaction/non-blocking (R9) — but since VJ retires the load
instantly, a blocking charge of the full latency is actually the right
*shape* for an event-driven core; the missing part is everything else.
- **No scoreboard interlocks (R2-R4), no flags interlock (R3), no load
latency (R5/R6), no jump costs (R12-R13), no divide latency (R15), no
MOVEI cost (R16).**
### Minimal implementable rule set (per-opcode add-on costs, GPU ticks)
For an interpreter that retires one instruction at a time, model the
scoreboard as "last writer" state:
```
state: last_alu_dst (reg# or NONE), last_flags_set (bool),
load_busy_until (tick), load_dst (reg#), div_busy_until (tick),
div_dst (reg#), ext_pending (0..2) with dst regs, gate_active_until
per instruction:
cost = 1
if reads(last_alu_dst): cost += 1 # R2
if uses_flags && last_flags_set: cost += 1 # R3
if reads(load_dst) && now < load_ready: cost += load_ready - now # R5
if reads(div_dst) && now < div_ready: cost += div_ready - now # R15
if is_div && now < div_done: cost += div_done - now # R15 second div
if is_memop && engine_busy: cost += busy_remaining # R5/R9/R10
if is_store && ext_pending: cost += until_ext_done # R9
opcode extras: MOVEI +2; indexed STORE +1; JUMP taken +2; JR taken +3;
(delay slot then executes normally)
loads: local LOAD ready at now+3 (indexed now+5); external LOAD ready at
now + 2 + grant_latency + mem_cycle + 2 (do not block issue)
update last_alu_dst := dst for ALU-class writes else NONE
update last_flags_set := instruction sets flags (and not held in a wait)
```
`grant_latency` and `mem_cycle` come from the existing bus_arbiter, which
should implement the R18 priority table with re-arbitration at cycle
boundaries (R19) — notably GPU-over-blitter preemption at normal priority
and OP wins during active display.
Expected effect: typical GPU kernels (dependent ALU chains + CMP/JR loops +
loads) gain ~40-100% more cycles per instruction, which is the right order
to close the 2-4x gap of #401 without touching machine timing.
---
## 12. What could NOT be pinned by reading — Verilator recipe
Needs simulation (all bounded above, exact constants wanted):
1. Taken JUMP/JR dead-tick count from local RAM (bound 2-3 / 3-4) — R12/R13.
2. External load end-to-end constant with idle bus (bound 6-9 sysclks) and
its distribution vs. 68K ownership — R9/R19.
3. MMULT per-element rate — R17.
4. Interrupt entry cost — §10.
5. Fetch-starvation bubbles for MOVEI/load-dense code — §9.
6. GPU grant-latency distribution while a normal blit / BUSHI blit / OP
fetch is running — R19.
### Recipe (jag_sim, already cloned here)
- **Tool:** Verilator; harness exists at `jag_sim/verilog/verilator/`
(`verilate.sh` builds top module **`emu`** from `jag_sim/verilog/jaguar.sv`
with `-I../base -I../tom -I../jerry`, plus `main.cpp`, `bios.cpp`,
`ssram.cpp`, `dram` model in `dram.cpp`; boot ROM image `verilog/os.mem`).
- **Faster targeted alternative:** verilate only the GPU island — top =
`gpu` (from `verilog/tom/gpu.v`, translation of GPU.NET) plus `gpu_mem`,
`gateway`; drive `clk[0]`, tie the coprocessor-bus `ack` to a scripted
bus model. That avoids booting the whole console.
- **Loading a GPU kernel (full-chip route):** in `main.cpp`'s DRAM/68K
path, poke the kernel into GPU RAM ($F03000+) through the 68K I/O port
(or bypass: write the words directly into the `gpu_ram` memory array in
the verilated model — `Vemu___024root` exposes it), write G_PC = $F03010,
then set GPUGO in G_CTRL ($F02114 bit 0).
- **Instrument:** per-tick sample of the netlist-named signals (Verilator
`--public` or `/*verilator public*/` on the translated nets):
`gpu.ins_exec.execon.exe` (instruction retires), `sbwait`, `aludwait`,
`flagwait`, `ldwait`, `xlddwait`, `mbusywait`, `wbwait`, `divdwait`
(inside `sboard`), `progreq/progack`, `datreq/datack` (gpu_mem),
`gpu_breq/gpu_back`, `mreq/ack` (TOM mem). Count ticks between `exe`
pulses per kernel = ground-truth CPI; the wait lines attribute every
bubble to a rule above.
- **Kernels to run** (assemble with any Jaguar RISC assembler, e.g. rmac):
a) 64x dependent ADD chain (R2); b) same with interleaving (R1);
c) CMP/JR loop (R3+R12-14); d) LOAD/use distance sweep 0..4 (R5);
e) indexed load/store sweep (R6/R7); f) DIV then use at distance
0/8/17 (R15); g) MOVEI burst (R16); h) external load loop with 68K
stopped vs. spinning (R9/R19); i) kernel while a large SRCEN+DSTEN blit
runs, BUSHI off/on (R19); j) same during active display vs. VBLANK (OP).
---
## 13. Cross-check corpus
- JTRM "Software Reference" pipeline prose agrees with R1/R2/R11/R15 (it
documents the scoreboard, the jump delay slot, and divide 2 bits/cycle).
Where JTRM is silent (exact stall counts, arbitration boundaries), the
netlists above are the only written authority.
- MiSTer `rtl/Rework/{sboard,execon,prefetch,gateway,mem,arb}.v` — same
logic, usable to confirm the translation introduced no timing edits
(the only GE-tagged functional edits found: `mem.v` DRAM `ram_rdy`
synchronization for the FPGA's external RAM — MEM.NET:286-288,364-369 —
which slightly ALTERS MiSTer's DRAM pacing vs. real silicon; do not treat
MiSTer DRAM cycle counts as authentic, use the §8 state machine).
---
## 14. Verilator-pinned results (2026-08-11, jag_sim netlist GPU)
Micro-kernels driven through the verilated GPU island; full method and
raw counts in the calibration session's VERILATOR-RESULTS.md.
- **R9 resolved — external LOAD latency: `L = 7 + D` sysclks** (slope
exactly 1 across D=1..8; identical for use-distance k=0..5, latency
`max(k+1, L)`). DRAM page hit (D=2) => **9**; at this model's
page-miss-average D=5 => **12**. The §4 bound of 6-9 resolves to its
top end and slightly beyond at miss.
- **§9 resolved — fetch starvation is ~zero for load-dense code**
(+0.016 cyc/instr vs pure ALU). The exact rule: a run of `g`
consecutive local-RAM loads costs `g - 1` extra clocks — first load
free (2-deep prefetch queue), each additional back-to-back load +1.
- **NEW RULE (was not in §3): write-back port conflict dominates real
IPC.** Every register-writing instruction in a disjoint-register
stream retires at 2.00 ticks/instr (NOP streams: exactly 1.00; a
following NOP does not conceal it). This is the JTRM "Register
Write-Back" dual-port-RAM rule firing on essentially every
back-to-back ALU pair: concealment requires the next instruction to
read the write-back target or read fewer than two registers. Real
compiled GPU code therefore sits near **0.5 IPC**, and §2's 1/tick
is only reachable by non-register-writing streams.
---
## 15. Bus grant latency under load (S2, 2026-10-13) — read-derived, NOT simulated
Section 12 item 6 asked for a Verilator-pinned grant-latency distribution.
**It could not be simulated:** the `vsim/` island rig behind §14 is in no
checkout and not on the LAN build host, Verilator is not installed there,
and that rig tied `gpu_back` granted with no ARB/MEM logic, so even
recovered it cannot measure grant latency; the full-chip `emu` route needs
a jag_sim checkout + a 68000 model on the build host. The function below is
therefore assembled from the JTRM (rev 8 text, page numbers verified in
the PDF text), the ARB.NET state machine (excerpts, see †; not
simulated) and
measured traffic; `docs/timing-campaign-plan.md` S2 has the result and
`test/tools/contention_model.py` the arithmetic. Evidence tags as in the
preamble.
Priorities below use the **JTRM's numbering (1 = highest)**, the reverse
of R18's table in §7.
| Term | Rule | Constant(s) | Evidence |
|---|---|---|---|
| Who beats whom | refresh 2 > OP 6 > DSP-normal 7 > CPU-under-interrupt 8 > GPU-normal 9 > blitter-normal 10 > CPU 11 | — | `[JTRM p.8]` + ARB.NET `req[]` chain (same order)† |
| Re-arbitration point | ownership changes only at a memory-cycle ack | — | `[JTRM p.29]` "ownership can be lost during a request (but not during a memory cycle)"; ARB.NET `arben = q2 & ack`† |
| OP holds its burst | RELEASE clear => bus held between data fetches; Doom: RELEASE clear on 100% of objects (measured) | wait = B²/2T, B = OP+refresh per line (low: per-object bursts) | `[JTRM p.19, p.7]`; burst contiguity *inferred* |
| Refresh | grouped after the OP's STOP; CLK/(64·(REFRATE+1)), REFRATE=5 measured | 3 clk/refresh (JTRM table) .. 9 (MEM.NET CBR walk, §8) | `[JTRM p.11, p.24]` vs §8 — unresolved, bounded |
| Blitter vs GPU/DSP | normal-priority blit preempted at the next ack; BUSHI never set by Doom (0/378 blits per field) | wait = U_blit · c/2, c = 2..5 | `[JTRM p.8, p.69, p.75]` |
| DSP vs GPU | DSP-normal beats GPU-normal; 16-bit external interface => 2 transactions per long | wait = U_dsp · L/2, L = 1..2 transactions × price × hold 1..2 | `[JTRM p.8, p.97]`; JERRY-side hold *inferred* |
| 68K default owner | Idle->Request needs a local request; Request->Hold waits for the 68000's BG; Hold->Idle as soon as no local request remains (no hold-off counter) => every access arriving while the bus is parked on the 68K pays BR->BG | P_idle × H, H = 2 / 6 / 12 sysclks | ARB.NET state table†; H from R19's 68000-datasheet bound — *not verified locally, not simulated*. Verdict-insensitive: the term is 0.02-0.18 field/flip for Doom |
| 68K stretch | lowest priority; stalls for every foreign tenure while it needs the bus | duty (measured 81%) × others' occupancy + 0..4 sysclk resync per tenure | `[JTRM p.8]`; resync *inferred* |
| GPU spin occupancy | hardware-rate main-RAM polls (4 loads/iteration at L = 7+D) | utilisation 0.25 / 0.40 / 0.60 while spinning | §14 + loop shape, *inferred* (the emulated poll rate is unphysical and not used) |
† ARB.NET was consulted through a summarizing web fetch of the public
jag_sim repository (no local checkout; it would not relay verbatim text):
the quoted expressions are excerpts it relayed — not verbatim, not
simulated.
Doom attract demo, per external access (stock config): GPU work access
waits 4.7 / 7.4 / 11.2 sysclks (OP+refresh dominate; handoff is small
because GPU render traffic coincides with blitter tenure, which keeps the
bus off the 68K); DSP 16-bit transaction 2.2 / 3.4 / 9.5. Checkered Flag
(2.5x Doom's OP traffic, lines 33-61 carry ~490 sysclks of OP fetch) puts
the total GPU wait at 9 / 36 / 42 sysclks per access.