mirror of
https://github.com/libretro/parallel-ps2.git
synced 2026-11-09 17:28:25 -10:00
SPR.cpp becomes C. Hw.cpp's MFIFO entry points and vucpu_execute_block get C linkage, THREAD_VU1 is asked through emu_config_thread_vu1(), and VU.h and VUmicro.h lose their // comments, which a C89 lane read into vu1RunCycles. Copies between the scratchpad and memory now stay inside what they address: a run past the end of main RAM reads on from the zero source, one through VU0's 4 KiB windows mirrors instead of running into VU1, the scratchpad's own window wraps, the scratchpad copied onto itself goes front to back, interleave transfers to or from a bus error no longer copy through NULL, and a source-chain tag at a bus error raises BEIS and stops the channel instead of being read through NULL. tests/eespr replays both channels against the transcript the C++ made and checks each of these.
458 lines
14 KiB
C++
458 lines
14 KiB
C++
/* The scratchpad DMA channels (pcsx2/SPR.c), driven with seeded
|
|
* transfers in both directions: normal, chain and interleave mode, into
|
|
* and out of main memory, the zero sink, the scratchpad's own window and
|
|
* VU memory, through the MFIFO and around it.
|
|
*
|
|
* Each scenario fills the scratchpad and two windows of main memory with
|
|
* quadwords that are all well-formed tags, so whatever either channel
|
|
* takes for a tag points somewhere sane, sets a channel up and then pokes
|
|
* it in a random order: kicks, interrupt deliveries, restarts. Every
|
|
* register the channels own, every call they make outward, their saved
|
|
* state and the memory they write go into a running hash, written out
|
|
* every so many scenarios. The transcript is compared with expected.txt,
|
|
* which this harness made from the C++ SPR.c was converted from
|
|
* (EESPR_TREE=<that checkout>): the conversion changes none of it.
|
|
* Transfers stay clear of the ends of the memories, where the C++ ran a
|
|
* copy past the array it was in, and of the bus errors it dereferenced;
|
|
* the C build checks those separately, in fixes(). */
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "Common.h"
|
|
#include "Config.h"
|
|
#include "R5900.h"
|
|
#include "Dmac.h"
|
|
#include "VUmicro.h"
|
|
#include "MTVU.h"
|
|
#include "SPR.h"
|
|
|
|
/* ---- the record ---------------------------------------------------- */
|
|
|
|
static unsigned long long s_hash = 1469598103934665603ULL;
|
|
|
|
static void mix(unsigned long long v)
|
|
{
|
|
int i;
|
|
for (i = 0; i < 8; i++)
|
|
{
|
|
s_hash ^= (v >> (i * 8)) & 0xff;
|
|
s_hash *= 1099511628211ULL;
|
|
}
|
|
}
|
|
|
|
static void mix_bytes(const void* p, size_t n)
|
|
{
|
|
const u8* b = (const u8*)p;
|
|
size_t i;
|
|
for (i = 0; i < n; i++)
|
|
{
|
|
s_hash ^= b[i];
|
|
s_hash *= 1099511628211ULL;
|
|
}
|
|
}
|
|
|
|
static u32 s_rng = 0xbb67ae85u;
|
|
static u32 rnd(void)
|
|
{
|
|
s_rng ^= s_rng << 13;
|
|
s_rng ^= s_rng >> 17;
|
|
s_rng ^= s_rng << 5;
|
|
return s_rng;
|
|
}
|
|
static u32 rnd_n(u32 n) { return rnd() % n; }
|
|
|
|
/* ---- what the unit reaches ----------------------------------------- */
|
|
|
|
alignas(16) cpuRegisters cpuRegs;
|
|
alignas(16) VURegs vuRegs[2];
|
|
alignas(4096) u8 eeHw[PS2MEM_HARDWARE];
|
|
EEVM_MemoryAllocMess* eeMem;
|
|
static u8* s_vumem;
|
|
|
|
/* THREAD_VU1 reads EmuConfig in the C++; the C asks through a call. */
|
|
static bool s_thread_vu1;
|
|
alignas(16) u8 s_emuconfig[sizeof(Pcsx2Config)];
|
|
asm(".globl EmuConfig\n.set EmuConfig, s_emuconfig");
|
|
#ifndef EESPR_CXX_TREE
|
|
bool emu_config_thread_vu1(void) { return s_thread_vu1; }
|
|
#endif
|
|
static void set_thread_vu1(bool on)
|
|
{
|
|
Pcsx2Config* c = (Pcsx2Config*)s_emuconfig;
|
|
s_thread_vu1 = on;
|
|
c->Cpu.Recompiler.EnableVU1 = on;
|
|
c->Speedhacks.vuThread = on;
|
|
c->Cpu.Recompiler.EnableVu1SoftFloat = false;
|
|
}
|
|
|
|
static int s_pending[2];
|
|
|
|
void CPU_INT(EE_EventType n, s32 ecycle)
|
|
{
|
|
mix(0x01); mix((u32)n); mix((u32)ecycle);
|
|
if (n == DMAC_FROM_SPR) s_pending[0] = 1;
|
|
if (n == DMAC_TO_SPR) s_pending[1] = 1;
|
|
}
|
|
void hwDmacIrq(int n) { mix(0x02); mix((u32)n); }
|
|
bool hwMFIFOWrite(u32 addr, const u128* data, uint qwc)
|
|
{
|
|
mix(0x03); mix(addr); mix(qwc);
|
|
/* The source is the scratchpad: never past its end. */
|
|
mix_bytes(data, (size_t)(qwc > 0x400 ? 0x400 : qwc) * 16);
|
|
return true;
|
|
}
|
|
void hwMFIFOResume(u32 transferred) { mix(0x04); mix(transferred); }
|
|
void _vu0FinishMicro(void) { mix(0x05); }
|
|
void vucpu_execute_block(const VUmicroCpu* cpu, int startUp) { mix(0x06); mix((u32)cpu->idx); mix((u32)startUp); }
|
|
void mtvu_wait_vu(void) { mix(0x07); }
|
|
static void vu_exec(u32 cycles) { mix(0x08); mix(cycles); }
|
|
static void vu0_clear(u32 addr, u32 size) { mix(0x09); mix(addr); mix(size); }
|
|
static void vu1_clear(u32 addr, u32 size) { mix(0x0a); mix(addr); mix(size); }
|
|
static VUmicroCpu s_vu0cpu = { 0, 0, NULL, NULL, NULL, vu_exec, vu0_clear, NULL };
|
|
static VUmicroCpu s_vu1cpu = { 1, 0, NULL, NULL, NULL, vu_exec, vu1_clear, NULL };
|
|
const VUmicroCpu* CpuVU0 = &s_vu0cpu;
|
|
const VUmicroCpu* CpuVU1 = &s_vu1cpu;
|
|
alignas(16) u8 s_vu1thread[sizeof(VU_Thread)];
|
|
asm(".globl vu1Thread\n.set vu1Thread, s_vu1thread");
|
|
|
|
/* The savestate calls: saving mixes what the unit keeps. */
|
|
static const u8* s_load;
|
|
void SaveState_FreezeMem(SaveStateBase* s, void* data, int size)
|
|
{
|
|
if (s->is_saving)
|
|
mix_bytes(data, (size_t)size);
|
|
else if (s_load)
|
|
{
|
|
memcpy(data, s_load, (size_t)size);
|
|
s_load += size;
|
|
}
|
|
}
|
|
bool SaveState_FreezeTag(SaveStateBase* s, const char* src) { (void)s; (void)src; return true; }
|
|
|
|
/* ---- the layout ---------------------------------------------------- */
|
|
|
|
static const u32 kMainA = 0x00200000;
|
|
static const u32 kMainB = 0x01e00000;
|
|
static const u32 kWindow = 0x10000;
|
|
static const u32 kMargin = 0x8000;
|
|
|
|
/* A main-memory address with room after it for any one transfer. */
|
|
static u32 main_addr(void)
|
|
{
|
|
return (rnd() & 1 ? kMainA : kMainB) + (rnd_n((kWindow - kMargin) >> 4) << 4);
|
|
}
|
|
|
|
/* Where data may go or come from: main memory (also through a kseg
|
|
* mirror), the zero sink, VU memory, low enough that a short run stays
|
|
* inside it. Not the scratchpad's own window: a copy of the scratchpad
|
|
* onto itself overlaps, which the C++ left to memcpy; fixes() has it. */
|
|
static u32 any_addr(void)
|
|
{
|
|
u32 v;
|
|
switch (rnd_n(11))
|
|
{
|
|
case 0: v = 0x08000000u | (rnd_n(0x1000) << 4); break;
|
|
case 1: v = 0x11000000u | (rnd_n(0x40) << 4); break; /* VU0 micro */
|
|
case 2: v = 0x11004000u | (rnd_n(0x40) << 4); break; /* VU0 data */
|
|
case 3: v = 0x11008000u | (rnd_n(0x200) << 4); break; /* VU1 micro */
|
|
case 4: v = 0x1100c000u | (rnd_n(0x200) << 4); break; /* VU1 data */
|
|
case 5: v = 0x20000000u | main_addr(); break;
|
|
default: v = main_addr(); break;
|
|
}
|
|
/* Now and then not on a quadword: the DMAC drops the low bits. */
|
|
return v | (rnd_n(4) == 0 ? rnd() & 0xc : 0);
|
|
}
|
|
|
|
/* A quadword that reads as a sane tag whichever way it is taken: a short
|
|
* count, any ID, maybe IRQ, and an address the other side can use. For
|
|
* SPR1 (source chain) the address may become TADR, so it stays in main
|
|
* memory where every quadword is such a tag. */
|
|
static void safe_tag(u8* q, bool source)
|
|
{
|
|
u32 w[4];
|
|
w[0] = rnd_n(9) | (rnd_n(8) << 28) | (rnd_n(6) == 0 ? 0x80000000u : 0);
|
|
w[1] = source ? main_addr() : any_addr();
|
|
w[2] = rnd();
|
|
w[3] = rnd();
|
|
memcpy(q, w, 16);
|
|
}
|
|
|
|
static void fill_tags(u8* p, u32 bytes, bool source)
|
|
{
|
|
u32 i;
|
|
for (i = 0; i < bytes; i += 16)
|
|
safe_tag(p + i, source);
|
|
}
|
|
|
|
static void reset_world(void)
|
|
{
|
|
memset(eeHw, 0, sizeof(eeHw));
|
|
memset(&cpuRegs, 0, sizeof(cpuRegs));
|
|
memset(s_pending, 0, sizeof(s_pending));
|
|
fill_tags(eeMem->Scratch, PS2MEM_SCRATCH, false);
|
|
fill_tags(&eeMem->Main[kMainA], kWindow, true);
|
|
fill_tags(&eeMem->Main[kMainB], kWindow, true);
|
|
fill_tags(s_vumem, VU_MEMORY_RESERVE_SIZE, false);
|
|
memset(eeMem->ZeroWrite, 0, 0x1000);
|
|
vuRegs[0].VI[REG_VPU_STAT].UL = rnd() & 0x101;
|
|
set_thread_vu1(rnd_n(3) == 0);
|
|
}
|
|
|
|
static void mix_state(void)
|
|
{
|
|
SaveStateBase s;
|
|
mix_bytes(&eeHw[0xd000], 0x500);
|
|
mix_bytes(&eeHw[0xe000], 0x100);
|
|
mix(cpuRegs.cycle);
|
|
mix(vuRegs[1].cycle);
|
|
memset(&s, 0, sizeof(s));
|
|
s.is_saving = true;
|
|
sprFreeze(&s);
|
|
}
|
|
|
|
static void mix_memory(void)
|
|
{
|
|
mix_bytes(eeMem->Scratch, PS2MEM_SCRATCH);
|
|
mix_bytes(&eeMem->Main[kMainA], kWindow);
|
|
mix_bytes(&eeMem->Main[kMainB], kWindow);
|
|
mix_bytes(eeMem->ZeroWrite, 0x1000);
|
|
mix_bytes(s_vumem, VU_MEMORY_RESERVE_SIZE);
|
|
}
|
|
|
|
static void random_dmac(void)
|
|
{
|
|
dmacRegs.ctrl._u32 = rnd() & 0x1ff;
|
|
dmacRegs.stadr._u32 = main_addr();
|
|
dmacRegs.sqwc._u32 = rnd_n(9) | (rnd_n(9) << 16);
|
|
if (rnd_n(3) == 0)
|
|
{
|
|
/* An MFIFO ring in main memory, sometimes with no size. */
|
|
dmacRegs.rbor._u32 = (rnd() & 1 ? kMainA : kMainB) + 0x1000;
|
|
dmacRegs.rbsr._u32 = rnd_n(4) == 0 ? 0 : 0xff0;
|
|
}
|
|
}
|
|
|
|
/* A channel: interleave and chain need no tag in the way; normal mode
|
|
* moves a short run. */
|
|
static void lay(DMACh* ch, bool source)
|
|
{
|
|
const u32 mod = rnd_n(5);
|
|
ch->chcr._u32 = 0x100 | (mod < 2 ? CHAIN_MODE << 2 : mod < 4 ? NORMAL_MODE : 2 << 2);
|
|
if (rnd_n(2)) ch->chcr.TIE = 1;
|
|
if (source && rnd_n(4) == 0) ch->chcr.TTE = 1;
|
|
if (rnd_n(3) == 0) ch->chcr._u32 |= (rnd_n(8) << 28) | (rnd() & 0x80000000u);
|
|
ch->qwc = ch->chcr.MOD == CHAIN_MODE && rnd_n(2) ? 0 : rnd_n(40);
|
|
/* Interleave runs madr across TQWC+SQWC per block: main memory. */
|
|
ch->madr = ch->chcr.MOD == 2 ? main_addr() : any_addr();
|
|
ch->sadr = rnd_n(0x400) << 4;
|
|
ch->tadr = main_addr();
|
|
random_dmac();
|
|
/* Straight into the MFIFO ring, from near the scratchpad's end. */
|
|
if (!source && dmacRegs.rbor._u32 && rnd_n(3) == 0)
|
|
{
|
|
ch->madr = dmacRegs.rbor._u32 + (rnd_n(4) << 4);
|
|
ch->sadr = 0x3f00 + (rnd_n(16) << 4);
|
|
}
|
|
}
|
|
|
|
static void step(int dir)
|
|
{
|
|
DMACh* ch = dir ? &spr1ch : &spr0ch;
|
|
switch (rnd_n(6))
|
|
{
|
|
case 0:
|
|
case 1:
|
|
if (dir) dmaSPR1(); else dmaSPR0();
|
|
break;
|
|
case 2:
|
|
case 3:
|
|
if (s_pending[dir])
|
|
{
|
|
s_pending[dir] = 0;
|
|
if (dir) SPRTOinterrupt(); else SPRFROMinterrupt();
|
|
}
|
|
break;
|
|
case 4:
|
|
if (!ch->chcr.STR && rnd_n(2))
|
|
{
|
|
ch->chcr.STR = 1;
|
|
if (rnd_n(2)) ch->qwc = rnd_n(12);
|
|
/* A fresh address, so no run creeps up on a window's end. */
|
|
ch->madr = ch->chcr.MOD == 2 ? main_addr() : any_addr();
|
|
if (dir) dmaSPR1(); else dmaSPR0();
|
|
}
|
|
break;
|
|
default:
|
|
if (rnd_n(4) == 0) random_dmac();
|
|
if (rnd_n(4) == 0) vuRegs[0].VI[REG_VPU_STAT].UL = rnd() & 0x101;
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void run(FILE* out)
|
|
{
|
|
int dir, scen, i;
|
|
static const char* names[] = { "spr0", "spr1" };
|
|
|
|
for (dir = 0; dir < 2; dir++)
|
|
{
|
|
for (scen = 0; scen < 1500; scen++)
|
|
{
|
|
reset_world();
|
|
lay(dir ? &spr1ch : &spr0ch, dir == 1);
|
|
mix_state();
|
|
for (i = 0; i < 30; i++)
|
|
{
|
|
step(dir);
|
|
mix_state();
|
|
}
|
|
mix_memory();
|
|
if ((scen + 1) % 50 == 0)
|
|
fprintf(out, "%s %4d %016llx\n", names[dir], scen + 1, s_hash);
|
|
}
|
|
mix_bytes(eeMem->Main, PS2MEM_MAIN_RAM);
|
|
fprintf(out, "%s all %016llx\n", names[dir], s_hash);
|
|
}
|
|
}
|
|
|
|
/* ---- what the conversion fixed ------------------------------------- */
|
|
|
|
#ifndef EESPR_CXX_TREE
|
|
static int s_failed;
|
|
|
|
static void expect(bool ok, const char* what)
|
|
{
|
|
if (!ok)
|
|
{
|
|
fprintf(stderr, "eespr: %s\n", what);
|
|
s_failed = 1;
|
|
}
|
|
}
|
|
|
|
static void fill_bytes(u8* p, u8 v, size_t n) { memset(p, v, n); }
|
|
|
|
static void fixes(void)
|
|
{
|
|
u32 i;
|
|
bool ok;
|
|
|
|
/* fromSPR into VU0 data memory, more than its 4 KiB: the window
|
|
* mirrors, and VU1's micro memory after it is left alone. */
|
|
reset_world();
|
|
fill_bytes(vuRegs[1].Micro, 0x5a, VU1_PROGSIZE);
|
|
for (i = 0; i < 0x2000; i++) eeMem->Scratch[i] = (u8)(i * 7);
|
|
memset(&spr0ch, 0, sizeof(spr0ch));
|
|
spr0ch.chcr._u32 = 0x100;
|
|
spr0ch.qwc = 0x200; /* 8 KiB */
|
|
spr0ch.madr = 0x11004000;
|
|
spr0ch.sadr = 0;
|
|
dmaSPR0();
|
|
ok = true;
|
|
for (i = 0; i < VU1_PROGSIZE; i++) ok = ok && vuRegs[1].Micro[i] == 0x5a;
|
|
expect(ok, "fromSPR stays inside VU0 data memory");
|
|
expect(vuRegs[0].Mem[0] == eeMem->Scratch[0x1000] && vuRegs[0].Mem[0xfff] == eeMem->Scratch[0x1fff],
|
|
"fromSPR mirrors through the VU0 data window");
|
|
expect(spr0ch.qwc == 0 && spr0ch.madr == 0x11006000, "fromSPR moves the whole run");
|
|
|
|
/* toSPR from the last quadwords of main RAM reads on from the zero
|
|
* source, not from the scratchpad behind it. */
|
|
reset_world();
|
|
memset(eeMem->ZeroRead, 0, 32);
|
|
memset(&eeMem->Main[PS2MEM_MAIN_RAM - 16], 0x66, 16);
|
|
memset(eeMem->Scratch, 0x11, 64);
|
|
memset(&spr1ch, 0, sizeof(spr1ch));
|
|
spr1ch.chcr._u32 = 0x101;
|
|
spr1ch.qwc = 2;
|
|
spr1ch.madr = PS2MEM_MAIN_RAM - 16;
|
|
spr1ch.sadr = 0x100;
|
|
dmaSPR1();
|
|
expect(eeMem->Scratch[0x100] == 0x66 && eeMem->Scratch[0x110] == 0 && eeMem->Scratch[0x11f] == 0,
|
|
"toSPR reads past main RAM from the zero source");
|
|
|
|
/* Into the scratchpad's own window near its end: it wraps rather
|
|
* than running into the ROM after it. */
|
|
reset_world();
|
|
memset(eeMem->ROM, 0x44, 64);
|
|
memset(&spr0ch, 0, sizeof(spr0ch));
|
|
spr0ch.chcr._u32 = 0x100;
|
|
spr0ch.qwc = 4;
|
|
spr0ch.madr = 0x70003fe0u;
|
|
spr0ch.sadr = 0x1000;
|
|
dmaSPR0();
|
|
expect(eeMem->ROM[0] == 0x44 && eeMem->ROM[63] == 0x44, "fromSPR into its own window stays in the scratchpad");
|
|
|
|
/* The scratchpad onto itself, one quadword on: the DMAC copies front
|
|
* to back, so the first quadword is repeated down the run. */
|
|
reset_world();
|
|
memset(&spr0ch, 0, sizeof(spr0ch));
|
|
spr0ch.chcr._u32 = 0x100;
|
|
spr0ch.qwc = 4;
|
|
spr0ch.madr = 0x70000110u;
|
|
spr0ch.sadr = 0x100;
|
|
{
|
|
u8 first[16];
|
|
memcpy(first, &eeMem->Scratch[0x100], 16);
|
|
dmaSPR0();
|
|
ok = true;
|
|
for (i = 0; i < 5; i++) ok = ok && memcmp(&eeMem->Scratch[0x100 + i * 16], first, 16) == 0;
|
|
expect(ok, "fromSPR onto the scratchpad copies front to back");
|
|
}
|
|
|
|
/* Interleave with nothing behind the address: the C++ copied from
|
|
* and to NULL. */
|
|
reset_world();
|
|
memset(&spr1ch, 0, sizeof(spr1ch));
|
|
spr1ch.chcr._u32 = 0x100 | (2 << 2);
|
|
spr1ch.qwc = 4;
|
|
spr1ch.madr = 0x12000000;
|
|
dmacRegs.sqwc._u32 = 1 | (2 << 16);
|
|
dmaSPR1();
|
|
expect(spr1ch.qwc == 0, "toSPR interleave from a bus error finishes");
|
|
memset(&spr0ch, 0, sizeof(spr0ch));
|
|
spr0ch.chcr._u32 = 0x100 | (2 << 2);
|
|
spr0ch.qwc = 4;
|
|
spr0ch.madr = 0x12000000;
|
|
dmaSPR0();
|
|
expect(spr0ch.qwc == 0, "fromSPR interleave to a bus error finishes");
|
|
|
|
/* A source-chain tag address with nothing behind it: BEIS, and the
|
|
* channel stops; the C++ read the tag through NULL. */
|
|
reset_world();
|
|
memset(&spr1ch, 0, sizeof(spr1ch));
|
|
spr1ch.chcr._u32 = 0x100 | (CHAIN_MODE << 2);
|
|
spr1ch.tadr = 0x12000000;
|
|
dmaSPR1();
|
|
expect(dmacRegs.stat.BEIS && !spr1ch.chcr.STR, "toSPR chain stops on a bus error at TADR");
|
|
}
|
|
#endif
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
FILE* out;
|
|
|
|
eeMem = (EEVM_MemoryAllocMess*)calloc(1, sizeof(*eeMem));
|
|
s_vumem = (u8*)calloc(1, VU_MEMORY_RESERVE_SIZE + 0x10000);
|
|
if (!eeMem || !s_vumem || argc < 2)
|
|
return 2;
|
|
vuRegs[0].Micro = s_vumem;
|
|
vuRegs[0].Mem = s_vumem + VU0_PROGSIZE;
|
|
vuRegs[1].Micro = s_vumem + VU0_PROGSIZE + VU0_MEMSIZE;
|
|
vuRegs[1].Mem = s_vumem + VU0_PROGSIZE + VU0_MEMSIZE + VU1_PROGSIZE;
|
|
out = fopen(argv[1], "w");
|
|
if (!out)
|
|
return 2;
|
|
run(out);
|
|
fclose(out);
|
|
#ifndef EESPR_CXX_TREE
|
|
fixes();
|
|
free(eeMem);
|
|
free(s_vumem);
|
|
return s_failed;
|
|
#else
|
|
free(eeMem);
|
|
free(s_vumem);
|
|
return 0;
|
|
#endif
|
|
}
|