Skip to content

Instantly share code, notes, and snippets.

@louisswarren
Last active September 3, 2023 03:10
Show Gist options
  • Save louisswarren/9c171da1309b6d7c2e578d22ed4ee744 to your computer and use it in GitHub Desktop.
Save louisswarren/9c171da1309b6d7c2e578d22ed4ee744 to your computer and use it in GitHub Desktop.
PSX emulation
/* Usage:
X(opnum, type, opname, opdesc, opfmt, code...)
*/
#define FMT_ad " %1$06x"
#define FMT_rs " $%1$02d"
#define FMT_rt " $%2$02d"
#define FMT_rd " $%3$02d"
#define FMT_im " %3$04x"
#define FMT_ims " %4$4d"
#define FMT_sh " %4$2d"
#define FMT_cd " CP0$%3$02d"
#define FMT_im_rs " %4$4d($%1$02d)"
#define RESET_BUS(r) do {\
slot->reg = (uint8_t)(slot->reg * (slot->reg != rt - cpu->gr));\
} while (0)
#define INSTRUCTIONS_X \
X( 2, JMP, "J", "Jump", FMT_ad, \
pre_branch_pause("Jumping ..."); \
next = (pc & 0xf0000000) | (ad << 2); \
) \
X( 3, JMP, "JAL", "Jump And Link", FMT_ad, \
pre_branch_pause("Jumping ..."); \
cpu->gr[31] = next; \
next = (pc & 0xf0000000) | (ad << 2); \
) \
X( 4, IMM, "BEQ", "Branch On Equal", FMT_rs FMT_rt FMT_ims, \
pre_branch_pause("Jumping if %u == %u ...", *rs, *rt); \
if (*rs == *rt) next = pc + 4 * (int16_t)im; \
) \
X( 5, IMM, "BNE", "Branch On Not Equal", FMT_rs FMT_rt FMT_ims, \
pre_branch_pause("Jumping if %u != %u ...", *rs, *rt); \
if (*rs != *rt) next = pc + 4 * (int16_t)im; \
) \
X( 6, IMM, "BLEZ", "Branch On <= 0", FMT_rs FMT_ims, \
pre_branch_pause("Jumping if %u <= 0...", *rs); \
if ((int32_t)*rs <= 0) next = pc + 4 * (int16_t)im; \
) \
X( 7, IMM, "BGTZ", "Branch On > 0", FMT_rs FMT_ims, \
pre_branch_pause("Jumping if %u > 0...", *rs); \
if ((int32_t)*rs > 0) next = pc + 4 * (int16_t)im; \
) \
X( 8, IMM, "ADDI", "Add Immediate", FMT_rt FMT_rs FMT_ims, \
if (((int16_t)im > 0 && *rs + (int16_t)im < *rs) || \
((int16_t)im < 0 && *rs + (int16_t)im > *rs)) { \
die("Trapped overflow"); \
} \
*rt = *rs + (int16_t)im; \
RESET_BUS(rt); \
) \
X( 9, IMM, "ADDIU", "Add Immediate Unsigned", FMT_rt FMT_rs FMT_ims, \
*rt = *rs + (int16_t)im; \
RESET_BUS(rt); \
) \
X( 12, IMM, "ANDI", "And Immediate", FMT_rt FMT_rs FMT_im, \
*rt = *rs & im; \
RESET_BUS(rt); \
) \
X( 13, IMM, "ORI", "Or Immediate", FMT_rt FMT_rs FMT_im, \
*rt = *rs | im; \
RESET_BUS(rt); \
) \
X( 15, IMM, "LUI", "Load Upper Immediate", FMT_rt FMT_im, \
*rt = (uint32_t)im << 16; \
RESET_BUS(rt); \
) \
X( 32, IMM, "LB", "Load Byte", FMT_rt FMT_im_rs, \
cpu->load_slot[!cpu->flipflop].reg = (uint8_t)(rt - cpu->gr); \
cpu->load_slot[!cpu->flipflop].val = \
(int8_t)load8(cpu, *rs + (int16_t)im); \
) \
X( 35, IMM, "LW", "Load Word", FMT_rt FMT_im_rs, \
cpu->load_slot[!cpu->flipflop].reg = (uint8_t)(rt - cpu->gr); \
cpu->load_slot[!cpu->flipflop].val = load32(cpu, *rs + (int16_t)im); \
) \
X( 36, IMM, "LBU", "Load Byte Unsigned", FMT_rt FMT_im_rs, \
cpu->load_slot[!cpu->flipflop].reg = (uint8_t)(rt - cpu->gr); \
cpu->load_slot[!cpu->flipflop].val = load8(cpu, *rs + (int16_t)im); \
) \
X( 40, IMM, "SB", "Store Byte", FMT_rt FMT_im_rs, \
if (!FLAG_ISOLATE_CACHE(cpu)) \
store8(cpu, *rs + (int16_t)im, (uint8_t)*rt); \
else debug(" (cache)"); \
) \
X( 41, IMM, "SH", "Store Halfword", FMT_rt FMT_im_rs, \
if (!FLAG_ISOLATE_CACHE(cpu)) \
store16(cpu, *rs + (int16_t)im, (uint16_t)*rt); \
else debug(" (cache)"); \
) \
X( 43, IMM, "SW", "Store Word", FMT_rt FMT_im_rs, \
if (!FLAG_ISOLATE_CACHE(cpu)) \
store32(cpu, *rs + (int16_t)im, *rt); \
else debug(" (cache)"); \
) \
/* Register instructions */ \
X( 64| 0, REG, "SLL", "Shift Left Logical", FMT_rd FMT_rt FMT_sh, \
*rd = *rt << sh; \
RESET_BUS(rd); \
) \
X( 64| 8, REG, "JR", "Jump Register", FMT_rs, \
next = *rs; \
) \
X( 64| 9, REG, "JALR", "Jump And Link Register", FMT_rd FMT_rs, \
*rd = next; \
next = *rs; \
) \
X( 64|32, REG, "ADD", "Add", FMT_rd FMT_rs FMT_rt, \
if (*rs + *rt < *rs) { \
die("Trapped overflow"); \
} \
*rd = *rs + *rt; \
RESET_BUS(rd); \
) \
X( 64|33, REG, "ADDU", "Add unsigned", FMT_rd FMT_rs FMT_rt, \
*rd = *rs + *rt; \
RESET_BUS(rd); \
) \
X( 64|36, REG, "AND", "And", FMT_rd FMT_rs FMT_rt, \
*rd = *rs & *rt; \
RESET_BUS(rd); \
) \
X( 64|37, REG, "OR", "Or", FMT_rd FMT_rs FMT_rt, \
*rd = *rs | *rt; \
RESET_BUS(rd); \
) \
X( 64|43, REG, "SLTU", "Set Less Than Unsigned", FMT_rd FMT_rs FMT_rt, \
*rd = *rs < *rt; \
RESET_BUS(rd); \
) \
/* Co-processor 0 instructions */ \
X(128| 0, CP0, "MFC0", "Move from COP0", FMT_rt FMT_cd, \
cpu->load_slot[!cpu->flipflop].reg = (uint8_t)(rt - cpu->gr); \
cpu->load_slot[!cpu->flipflop].val = *cd; \
) \
X(128| 4, CP0, "MTC0", "Move to COP0", FMT_rt FMT_cd, \
*cd = *rt; \
) \
//#undef Fad
//#undef Frs
//#undef Frt
//#undef Frd
//#undef Fim
//#undef Fsh
//
//#undef Fim_Frs
CFLAGS = -Wall -Warray-bounds=2 -Wcast-align=strict -Wcast-qual -Wconversion -Wno-sign-conversion -Wdangling-else -Wdate-time -Wdouble-promotion -Wextra -Wfloat-conversion -Wformat-overflow=2 -Wformat-signedness -Wformat-truncation=2 -Wformat=2 -Winit-self -Wjump-misses-init -Wlogical-op -Wmissing-include-dirs -Wnested-externs -Wnull-dereference -Wpacked -Wpedantic -Wredundant-decls -Wshadow -Wshift-negative-value -Wshift-overflow=2 -Wstrict-aliasing -Wstrict-overflow=2 -Wstrict-prototypes -Wstringop-overflow=4 -Wstringop-truncation -Wswitch-default -Wswitch-enum -Wuninitialized -Wunsafe-loop-optimizations -Wunused -Wuse-after-free=3 -Wwrite-strings -fanalyzer -fmax-errors=2 -pedantic-errors
.PHONY: default
default: test
.PHONY: test
test: xods
./$< < /dev/null | diff expected.log - | head -n40
xods.o: xods.c instructions.h
expected.log: xods
./$< > $@
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <assert.h>
#include "instructions.h"
#define debug(...) do { \
printf(__VA_ARGS__); \
fflush(stdout); \
} while (0)
#define error(...) do { \
fprintf(stderr, __VA_ARGS__); \
fflush(stderr); \
} while (0)
#define die(...) do { \
fprintf(stderr, __VA_ARGS__); \
fprintf(stderr, "\n"); \
exit(1); \
} while (0)
// Break glass in case of infinite loops
#if 0
#define pre_branch_pause(...) do { \
printf("\n" __VA_ARGS__); \
getchar(); \
} while (0)
#else
#define pre_branch_pause(...) (void)0
#endif
static uint32_t breakpoints[] = {
0,
0xbfc06ecc,
};
#define MEMMAP_RES 12
uint32_t ADDR_BIOS = 0xbfc00000;
struct load_slot {
uint8_t reg;
uint32_t val;
};
struct cpu {
uint8_t flipflop;
struct load_slot load_slot[2];
uint32_t pc;
uint32_t next_instr;
uint32_t next_instr_addr; // Debug
uint32_t hi;
uint32_t lo;
uint32_t gr[32];
uint32_t c0[32];
uint8_t ram[2048 << 10];
uint8_t exp[8192 << 10];
uint8_t scr[ 1 << 10];
uint8_t reg[ 8 << 10];
uint8_t bio[ 512 << 10];
uint8_t pio[ 512];
uint8_t *memmap[1 << (32 - MEMMAP_RES)];
};
#define FLAG_ISOLATE_CACHE(c) ((c)->c0[12] & 0x00010000)
void
dump_reg(const struct cpu *cpu)
{
for (int i = 0; i < 8; ++i) {
fprintf(stdout,
"$%02d=%08x\t"
"$%02d=%08x\t"
"$%02d=%08x\t"
"$%02d=%08x\n",
0 + i, cpu->gr[ 0 + i],
8 + i, cpu->gr[ 8 + i],
16 + i, cpu->gr[16 + i],
24 + i, cpu->gr[24 + i]);
}
}
enum memory_segment {
KUSEG = 0,
KSEG0 = 4,
KSEG1 = 5, // Not cached
KSEG2 = 7,
};
uint8_t *
addr(const struct cpu *cpu, uint32_t n)
{
// enum memory_segment seg = n >> 29;
n &= 0x1fffffff;
uint8_t *page = cpu->memmap[n >> MEMMAP_RES];
/* Can't access unmapped memory */
assert(page);
return &page[n & ((1 << MEMMAP_RES) - 1)];
}
uint8_t
load8(const struct cpu *cpu, uint32_t ad)
{
const uint8_t *p = addr(cpu, ad);
return p[0];
}
uint16_t
load16(const struct cpu *cpu, uint32_t ad)
{
/* Can't access unaligned memory */
assert(!(ad & 1));
const uint8_t *p = addr(cpu, ad);
return p[0] | p[1] << 8;
}
uint32_t
load32(const struct cpu *cpu, uint32_t ad)
{
/* Can't access unaligned memory */
assert(!(ad & 3));
const uint8_t *p = addr(cpu, ad);
return p[0] | p[1] << 8 | p[2] << 16 | p[3] << 24;
}
void
store8(struct cpu *cpu, uint32_t ad, uint8_t x)
{
/* Expansion 1 & 2 addresses.
* It's never correct to set these to anything else.
*/
assert(ad != 0x1f801000 || x == 0x00);
assert(ad != 0x1f801004 || x == 0x00);
uint8_t *p = addr(cpu, ad);
p[0] = (uint8_t)(x >> 0) & 0xff;
}
void
store16(struct cpu *cpu, uint32_t ad, uint16_t x)
{
/* Expansion 1 & 2 addresses.
* It's never correct to set these to anything else.
*/
assert(ad != 0x1f801000 || x == 0x0000);
assert(ad != 0x1f801004 || x == 0x2000);
/* Can't access unaligned memory */
assert(!(ad & 1));
uint8_t *p = addr(cpu, ad);
p[0] = (uint8_t)(x >> 0) & 0xff;
p[1] = (uint8_t)(x >> 8) & 0xff;
}
void
store32(struct cpu *cpu, uint32_t ad, uint32_t x)
{
/* Expansion 1 & 2 addresses.
* It's never correct to set these to anything else.
*/
assert(ad != 0x1f801000 || x == 0x1f000000);
assert(ad != 0x1f801004 || x == 0x1f802000);
/* Can't access unaligned memory */
assert(!(ad & 3));
uint8_t *p = addr(cpu, ad);
p[0] = (uint8_t)(x >> 0) & 0xff;
p[1] = (uint8_t)(x >> 8) & 0xff;
p[2] = (uint8_t)(x >> 16) & 0xff;
p[3] = (uint8_t)(x >> 24) & 0xff;
}
void
reset(struct cpu *cpu)
{
cpu->flipflop = 0;
cpu->load_slot[0].reg = 0;
cpu->load_slot[0].val = 0;
cpu->load_slot[1].reg = 0;
cpu->load_slot[1].val = 0;
cpu->pc = ADDR_BIOS;
cpu->gr[0] = 0;
// Not sure if this is needed
cpu->gr[31] = cpu->pc;
// Set some canary values
for (size_t i = 1; i < 31; ++i)
cpu->gr[i] = 0x89ABCDEF;
for (size_t i = 0; i < 32; ++i)
cpu->c0[i] = 0x89ABCDEF;
// Set the status register to 0
cpu->c0[12] = 0;
cpu->hi = 0x89ABCDEF;
cpu->lo = 0x89ABCDEF;
memset(cpu->ram, 0x7f, sizeof(cpu->ram));
memset(cpu->exp, 0xff, sizeof(cpu->exp));
memset(cpu->scr, 0x7f, sizeof(cpu->scr));
memset(cpu->reg, 0x7f, sizeof(cpu->reg));
memset(cpu->bio, 0x7f, sizeof(cpu->bio));
memset(cpu->pio, 0x7f, sizeof(cpu->pio));
FILE *f = fopen("scph1001.bin", "r");
if (!f)
die("Failed to open BIOS");
if (!fread(cpu->bio, sizeof(cpu->bio), 1, f))
die("Failed to read BIOS");
if (fclose(f))
die("Failed to close BIOS");
/* Preload first instruction */
cpu->next_instr_addr = cpu->pc;
cpu->next_instr = load32(cpu, cpu->pc);
cpu->pc += 4;
}
void
setup_memmap(struct cpu *cpu)
{
memset(cpu->memmap, 0, sizeof(cpu->memmap));
#define create_memmap(start, mem) \
for (size_t i = 0; i < sizeof(mem); i += 1 << MEMMAP_RES) \
cpu->memmap[((start) + i) >> MEMMAP_RES] = mem + i
create_memmap(0x00000000, cpu->ram);
create_memmap(0x1f000000, cpu->exp);
create_memmap(0x1f800000, cpu->scr);
create_memmap(0x1f801000, cpu->reg);
create_memmap(0x1fc00000, cpu->bio);
create_memmap(0xfffe0000 & 0x1fffffff, cpu->pio);
#undef create_memmap
assert(&cpu->ram[0] == addr(cpu, 0x00000000));
assert(&cpu->ram[0] == addr(cpu, 0x80000000));
assert(&cpu->ram[0] == addr(cpu, 0xa0000000));
assert(&cpu->ram[4] == addr(cpu, 0x00000004));
assert(&cpu->ram[4] == addr(cpu, 0x80000004));
assert(&cpu->ram[4] == addr(cpu, 0xa0000004));
assert(&cpu->bio[4] == addr(cpu, 0x1fc00004));
assert(&cpu->bio[4] == addr(cpu, 0x9fc00004));
assert(&cpu->bio[4] == addr(cpu, 0xbfc00004));
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wformat"
#pragma GCC diagnostic ignored "-Wformat-extra-args"
void
do_cycle(struct cpu *cpu)
{
uint32_t instr = cpu->next_instr;
uint32_t instr_addr = cpu->next_instr_addr;
cpu->next_instr_addr = cpu->pc;
cpu->next_instr = load32(cpu, cpu->pc);
uint32_t next = cpu->pc + 4;
cpu->flipflop ^= 1;
struct load_slot *slot = &cpu->load_slot[cpu->flipflop];
unsigned int opcode = instr >> 26;
// Register instructions have opcode 0, and use the 6 LSB instead
// Translate these so we just need one switch statement
unsigned int fn = instr & 0x3f;
opcode += (opcode == 0) * (64 + fn);
// Coprocessor instructions have opcode 16 (cop0) or 18 (cop2) and use
// the five bits after the opcode for their true opcode
unsigned int copcode = (instr >> 21) & 0x1f;
opcode += (opcode == 16) * (128 - 16 + copcode);
opcode += (opcode == 18) * (160 - 18 + copcode);
// Translated opcode ranges are:
// 0 - 63: Standard (= opcode)
// 64 - 127: Register instructions (= 64 + fn)
// 128 - 159: cop0 instructions (= 128 + copcode)
// 160 - 191: cop2 instructions (= 160 + copcode)
uint32_t pc = cpu->pc;
uint32_t *rs;
uint32_t *rt;
uint32_t *rd;
uint8_t sh;
#define INIT_REG(fmt) do { \
rs = &cpu->gr[(instr >> 21) & 0x1f]; \
rt = &cpu->gr[(instr >> 16) & 0x1f]; \
rd = &cpu->gr[(instr >> 11) & 0x1f]; \
sh = (instr >> 6) & 0x1f ; \
debug(fmt, (int)(rs - cpu->gr), (int)(rt - cpu->gr), (int)(rd - cpu->gr), sh); \
} while (0)
uint16_t im;
#define INIT_IMM(fmt) do {\
rs = &cpu->gr[(instr >> 21) & 0x1f]; \
rt = &cpu->gr[(instr >> 16) & 0x1f]; \
im = (instr >> 0) & 0xffff ; \
debug(fmt, (int)(rs - cpu->gr), (int)(rt - cpu->gr), im, (int16_t)im); \
} while (0)
uint32_t ad;
#define INIT_JMP(fmt) do { \
ad = (instr >> 0) & 0x3ffffff; \
debug(fmt, ad); \
} while (0)
uint32_t *cd;
#define INIT_CP0(fmt) do { \
rt = &cpu->gr[(instr >> 16) & 0x1f]; \
cd = &cpu->c0[(instr >> 11) & 0x1f]; \
debug(fmt, copcode, (int)(rt - cpu->gr), (int)(cd - cpu->c0)); \
} while (0)
switch (opcode) {
#define X(opnum, type, opname, opdesc, opfmt, ...) \
case opnum: \
debug("%08x: %08x(%02x) | ", instr_addr, instr, opcode); \
debug("%-22s | %-5s", opdesc, opname); \
INIT_##type(opfmt); \
__VA_ARGS__ \
break;
INSTRUCTIONS_X
#undef X
default:
error("Bad instruction ");
if (opcode < 64)
error("%u (%08x)\n", opcode, instr);
else if (opcode < 128)
error("(reg) 0(%u) (%08x)\n", fn, instr);
else if (opcode < 160)
error("(cop0) 16(%u) (%08x)\n", copcode, instr);
else if (opcode < 192)
error("(cop2) 18(%u) (%08x)\n", copcode, instr);
else
error("(?) (%u) (%08x)\n", opcode, instr);
// Still developing so don't complain too much
exit(0);
}
cpu->pc = next;
// Load delay
// Idea: we can reset the bus by setting slot->reg to 0
// In which case the bus overwrites $00, which is about to get zeroed
// if (slot->reg) debug(" | $%02x <- %08x\n", slot->reg, slot->val);
cpu->gr[slot->reg] = slot->val;
slot->reg = 0;
cpu->gr[0] = 0;
debug("\n");
}
#pragma GCC diagnostic pop
uint64_t
mem_fill(uint64_t seed, uint8_t *p, size_t n)
{
assert(!(n & 3));
if (!seed)
seed = 0xb113476d4b33afd7;
for (size_t i = 0; i < n; i += 4) {
/* https://nullprogram.com/blog/2017/09/21/ */
seed *= 0x9b60933458e17d7d;
seed += 0xd737232eeccdf7ed;
uint32_t x = (uint32_t)(seed >> (29 - (seed >> 61)));
p[i + 0] = (uint8_t)(x >> 0) & 0xff;
p[i + 1] = (uint8_t)(x >> 8) & 0xff;
p[i + 2] = (uint8_t)(x >> 16) & 0xff;
p[i + 3] = (uint8_t)(x >> 24) & 0xff;
}
return seed;
}
uint64_t
mem_assert(uint64_t seed, uint8_t *p, size_t n)
{
assert(!(n & 3));
if (!seed)
seed = 0xb113476d4b33afd7;
for (size_t i = 0; i < n; i += 4) {
/* https://nullprogram.com/blog/2017/09/21/ */
seed *= 0x9b60933458e17d7d;
seed += 0xd737232eeccdf7ed;
uint32_t x = (uint32_t)(seed >> (29 - (seed >> 61)));
uint32_t y = p[i + 0] << 0 |
p[i + 1] << 8 |
p[i + 2] << 16 |
p[i + 3] << 24;
assert(x == y);
}
return seed;
}
int
main(void)
{
struct cpu *cpu = calloc(1, sizeof(*cpu));
if (!cpu)
die("Failed to allocate mem_premap");
//fprintf(stderr, "CPU size: %luKB\n", sizeof(*cpu) / 1024);
//fprintf(stderr, "Memmap size: %luKB\n", sizeof(cpu->memmap) / 1024);
setup_memmap(cpu);
/* Test the memory map */
struct {
uint8_t *p;
size_t n;
uint32_t offset;
const char *name;
} memtest[] = {
{cpu->ram, sizeof(cpu->ram), 0x00000000, "RAM"},
{cpu->exp, sizeof(cpu->exp), 0x1f000000, "Expansion"},
{cpu->scr, sizeof(cpu->scr), 0x1f800000, "Scratchpad"},
{cpu->reg, sizeof(cpu->reg), 0x1f801000, "Hardware registers"},
{cpu->bio, sizeof(cpu->bio), 0x1fc00000, "BIOS"},
{cpu->pio, sizeof(cpu->pio), 0xfffe0000, "IO Ports"},
};
uint64_t seed = 0;
for (size_t i = 0; i < sizeof(memtest) / sizeof(*memtest); ++i) {
seed = mem_fill(seed, memtest[i].p, memtest[i].n);
}
seed = 0;
for (size_t i = 0; i < sizeof(memtest) / sizeof(*memtest); ++i) {
uint8_t *p = addr(cpu, memtest[i].offset);
seed = mem_assert(seed, p, memtest[i].n);
fprintf(stderr, "%s OK\n", memtest[i].name);
}
reset(cpu);
const int n_breakpoints = sizeof(breakpoints) / sizeof(breakpoints[0]);
while (1) {
int i = -1;
for (i = 0; i < n_breakpoints; ++i) {
uint32_t a = cpu->next_instr_addr & 0x1fffffff;
if (a == (breakpoints[i] & 0x1fffffff)) {
do_cycle(cpu);
dump_reg(cpu);
(void)getchar();
break;
}
}
if (i == n_breakpoints)
do_cycle(cpu);
}
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment