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]>
Doom's critical (GPU) chain gains 0.31/0.46/0.68 field per flip from the
latency function x measured traffic (every bound < 1.0); injected into the
emulator it moves the present rate +0.01..+0.05 (stock) / +0.16..+0.46
(all timing options). The function is assembled from JTRM rev 8 and the
ARB.NET state machine (not simulated: no Verilator rig or jag_sim checkout
on any host) and is written up as gpu-timing-spec.md section 15. Also
corrects plan premises H-A (94% of GPU external traffic is spin polls),
H-B (68K work is 0.31 field/flip) and H-C (DSP traffic is large but its
chain has 1.25 fields of slack), and records the 6% 68K slice-overshoot.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Simulated the jag_sim netlist GPU under Verilator to close the two open
items in docs/gpu-timing-spec.md section 12, and found a third rule the
netlist reading had missed entirely.
- External LOAD latency pinned: L = 7 + D sysclks (D = memory
controller occupancy), slope exactly 1 across D=1..8 and identical
for use-distance 0..5. Page hit => 9. The model's fixed overhead is
retuned to match (EXT_RETURN 2 -> 3: issue 2 + grant 2 + return 3).
- Fetch starvation resolved as ~zero for load-dense code (+0.016
cyc/instr): the real rule is that a run of g consecutive local loads
costs g-1 extra clocks, which the existing local-engine model (one op
per 2 ticks) already reproduces. No code change.
- NEW: write-back port conflict. Every register-writing instruction in
a DISJOINT-register stream retires at 2.00 ticks (NOP streams: 1.00;
a following NOP does not conceal it). This is the JTRM 'Register
Write-Back' dual-port rule -- concealment needs the next instruction
to read the write-back target or read fewer than two registers. Real
GPU code therefore runs near 0.5 IPC, not the 1/tick the spec
assumed. Modeled: a two-read instruction whose reads miss the
previous instruction's destination now stalls 1.
Validation: the independent-ALU microbench moves 22 -> 37 cyc/iter
(1.85 ticks/instr vs the netlist's measured 2.00 -- the remainder is
the loop's non-register-writing ops, as expected). Dependency-heavy
code is largely unaffected because the RAW interlock already charged
those pairs, which is why Doom's GPU load barely moves (66% -> 69% of a
field) while the synthetic independent stream nearly doubles. Hover
Strike 1.33 -> 1.43 fields/flip. Full make test green; default-off
unchanged.
Verilator method and raw counts: docs/gpu-timing-verilator-results.md.
Co-Authored-By: Claude Fable 5 <[email protected]>
Copilot review on #407.
The model charged pipeline interlocks, MOVEI/indexed extras, jump
refill and delay-slot cycles through gpu_bus_stall -- which GPUExec
treats as WALL time and rescales when risc_clock_scale != 1x. That is
right for DRAM latency (silicon does not speed up when the RISC is
overclocked) and backwards for anything internal to the RISC, which
scales WITH the processor: one interlock cycle became two at 2x.
Split the accumulators per the bus_arbiter.h cycle-domain contract:
gpu_bus_stall keeps external memory latency (rescaled), the new
gpu_pipe_core_stall carries RISC-internal cost (deducted unscaled).
Pending results now record their domain (gpu_reg_ready_ext), so a
scoreboard wait on an external load bills as wall time while a wait on
a local load or divide bills as core time -- local RAM is clocked at
the GPU clock (JTRM Memory Interface).
Verified: microbenches byte-identical at 1x (ALU 22, load-use 19,
stores 35 cyc/iter); the interlock-dominated ALU bench now scales
1.998x from 1x to 2x RISC clock, as a core-time-bound kernel must.
Full make test green.
Also from the same review:
- gpu_pipe_stall_total / gpu_pipe_ext_total documented as monotonic
since GPUReset and reset there, NOT in GPUPipeTimingReset(): that
runs on every interrupt bank switch, so clearing them there would
blank the counters many times a frame and leave probes differencing
against a moving zero.
- present_rate_probe header rewritten to describe what it actually
does (frame-hash sampling, not OLP watching), and it now reports
fields/flip only -- the hard-coded 5.006 s/window was wrong for PAL
and approximate for NTSC.
- docs/gpu-timing-spec.md: replaced the local scratch path with the
upstream jag_sim repository reference.
Co-Authored-By: Claude Fable 5 <[email protected]>
New core option virtualjaguar_gpu_pipeline_timing (EXPERIMENTAL,
default off; off = byte-identical). Implements the instruction-level
GPU costs the emulator has always skipped, per the Flare netlist
sources (docs/gpu-timing-spec.md):
- register score-board: reading a pending load/divide result stalls to
its completion; one-deep ALU result-use interlock (+1); flags
interlock (+1 for CMP;JRcc / ADDC/SUBC after a flag-setter)
- single external gateway, split-transaction, two pending slots:
back-to-back external loads/stores serialize at bus speed; a store
drains pending loads; local loads have 3-tick latency on a
one-op-per-2-ticks engine
- DIV 16 background ticks (use blocks); MOVEI 3 ticks; indexed
load +2 / store +1; taken JUMP +2 / JR +3, and the inlined
delay-slot instruction is now charged at all under the option
(it was free)
Validated on the new synthetic acid ROMs (cyc/iter, off -> on):
external load-use 5 -> 19, external stores 7 -> 35, independent ALU
19 -> 22 (correctly ~unchanged), render-bound repro 6.5 -> 2.1
completions/field. Full make test green; default-off untouched.
Doom findings (probes included: gpu_pipe_probe, present_rate_probe,
gpu_bench_read, counter_rate_scan): VI delivery and ticcount are
exactly 1/field (correct); the demo presents every 2.00 fields vs
hardware's ~4 -- the pass is gate-floored in our core because even the
charged render still fits inside 2 fields. The model charges 65% of a
field per frame on Doom (16-21k external accesses); closing the last
~2x needs the two sim-pinned terms from the spec: fetch-starvation
bubbles and OP-vs-GPU bus grant contention during active display
(Verilator recipe in spec section 12). Also adds INT1 per-source
perf counters (BENCH_PROFILE) used to falsify the VI-double-fire
theory.
Issue #401, continues #313.
Co-Authored-By: Claude Fable 5 <[email protected]>