Created
January 25, 2024 17:19
-
-
Save binji/3dba4717c6e6a1c4ff77ed2bf1d109db to your computer and use it in GitHub Desktop.
Parsing/executing tom harte tests for NES in C
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "common.h" | |
#include "options.h" | |
#include "vec.h" | |
#include "third_party/json.h" | |
#define HOOK(name, ...) | |
#define DEBUG(...) (void)0 | |
// See s_opcode_bits below | |
#define STEP_CPU_DECODE 781 | |
#define STEP_CALLVEC (STEP_CPU_DECODE + 1) | |
#define STEP_RESET (STEP_CALLVEC + 7) | |
#define STEP_OAMDMA (STEP_RESET + 7) | |
#define STEP_DMC (STEP_OAMDMA + 514) | |
#define CPU_ONLY 1 | |
static const char* s_json_filename; | |
typedef struct { u16 PC; u8 S, A, X, Y, P; } cpu_state; | |
typedef struct { u16 addr; u8 val; } ram_state; | |
typedef struct { u16 addr; u8 val; bool write; } cycle_state; | |
typedef union { | |
struct { u8 lo, hi; }; // TODO endian | |
u16 val; | |
} u16pair; | |
typedef struct { | |
u64 bits; | |
u16 step, next_step, dmc_step; | |
u16pair PC, T, bus, oam; | |
u8 fixhi, veclo; | |
u8 A, X, Y, S; | |
u8 ram[0x10000]; | |
u8 opcode, open_bus, irq; | |
bool C, Z, I, D, V, N; // Flags. | |
bool req_nmi, req_reset, has_nmi, has_irq, has_reset, reset_active; | |
u64 set_vec_cy; | |
// | |
cycle_state log[8]; | |
} C; | |
typedef struct { | |
struct { | |
C c; | |
u64 cy; | |
} s; | |
} E; | |
static const u8 s_opcode_bits[781+1+7+7+514+4]; | |
static const u16 s_opcode_loc[256]; | |
static u8 cpu_read(E *e, u16 addr) { | |
u8 val = e->s.c.ram[addr]; | |
e->s.c.log[e->s.cy] = (cycle_state){.addr = addr, .val = val, 0}; | |
return val; | |
} | |
static void cpu_write(E *e, u16 addr, u8 val) { | |
e->s.c.log[e->s.cy] = (cycle_state){.addr = addr, .val = val, 1}; | |
e->s.c.ram[addr] = val; | |
} | |
void check_irq(E* e) {} | |
static inline u16 get_u16(u8 hi, u8 lo) { return (hi << 8) | lo; } | |
#include "cpu.c" | |
static void usage(int argc, char** argv) { | |
PRINT_ERROR( | |
"usage: %s [options] <file.json>\n" | |
" -h,--help help\n", | |
argv[0]); | |
} | |
static void parse_arguments(int argc, char** argv) { | |
static const Option options[] = { | |
{'h', "help", 0}, | |
}; | |
struct OptionParser* parser = option_parser_new( | |
options, sizeof(options) / sizeof(options[0]), argc, argv); | |
int errors = 0; | |
int done = 0; | |
while (!done) { | |
OptionResult result = option_parser_next(parser); | |
switch (result.kind) { | |
case OPTION_RESULT_KIND_UNKNOWN: | |
PRINT_ERROR("ERROR: Unknown option: %s.\n\n", result.arg); | |
goto error; | |
case OPTION_RESULT_KIND_EXPECTED_VALUE: | |
PRINT_ERROR("ERROR: Option --%s requires a value.\n\n", | |
result.option->long_name); | |
goto error; | |
case OPTION_RESULT_KIND_BAD_SHORT_OPTION: | |
PRINT_ERROR("ERROR: Short option -%c is too long: %s.\n\n", | |
result.option->short_name, result.arg); | |
goto error; | |
case OPTION_RESULT_KIND_OPTION: | |
switch (result.option->short_name) { | |
case 'h': | |
goto error; | |
default: | |
abort(); | |
break; | |
} | |
break; | |
case OPTION_RESULT_KIND_ARG: | |
s_json_filename = result.value; | |
break; | |
case OPTION_RESULT_KIND_DONE: | |
done = 1; | |
break; | |
} | |
} | |
if (!s_json_filename) { | |
PRINT_ERROR("ERROR: expected input .json\n\n"); | |
goto error; | |
} | |
option_parser_delete(parser); | |
return; | |
error: | |
usage(argc, argv); | |
option_parser_delete(parser); | |
exit(1); | |
} | |
bool json_string_matches(json_string_t* js, const char* s) { | |
size_t i; | |
for (i = 0; i < js->string_size && s[i]; ++i) { | |
if (js->string[i] != s[i]) { | |
return false; | |
} | |
} | |
return s[i] == 0; | |
} | |
Result parse_cpu_state(json_object_t* jo, cpu_state *result) { | |
for (json_object_element_t* joe = jo->start; joe; joe = joe->next) { | |
if (json_string_matches(joe->name, "pc")) { | |
json_number_t* n = json_value_as_number(joe->value); | |
CHECK(n); | |
result->PC = strtoul(n->number, NULL, 10); | |
} else if (json_string_matches(joe->name, "s")) { | |
json_number_t* n = json_value_as_number(joe->value); | |
CHECK(n); | |
result->S = strtoul(n->number, NULL, 10); | |
} else if (json_string_matches(joe->name, "a")) { | |
json_number_t* n = json_value_as_number(joe->value); | |
CHECK(n); | |
result->A = strtoul(n->number, NULL, 10); | |
} else if (json_string_matches(joe->name, "x")) { | |
json_number_t* n = json_value_as_number(joe->value); | |
CHECK(n); | |
result->X = strtoul(n->number, NULL, 10); | |
} else if (json_string_matches(joe->name, "y")) { | |
json_number_t* n = json_value_as_number(joe->value); | |
CHECK(n); | |
result->Y = strtoul(n->number, NULL, 10); | |
} else if (json_string_matches(joe->name, "p")) { | |
json_number_t* n = json_value_as_number(joe->value); | |
CHECK(n); | |
result->P = strtoul(n->number, NULL, 10); | |
} | |
} | |
return OK; | |
ON_ERROR_RETURN; | |
} | |
Result parse_ram_state(json_object_t* jo, ram_state* result, | |
int* ram_state_count) { | |
int index = 0; | |
for (json_object_element_t* joe = jo->start; joe; joe = joe->next) { | |
if (json_string_matches(joe->name, "ram")) { | |
json_array_t* ja = json_value_as_array(joe->value); | |
CHECK(ja); | |
for (json_array_element_t* jae = ja->start; jae; | |
jae = jae->next, ++index) { | |
CHECK_MSG(index < *ram_state_count, "too many ram states"); | |
json_array_t* pair = json_value_as_array(jae->value); | |
CHECK_MSG(pair, "ram element is not array"); | |
CHECK_MSG(pair->length == 2, "ram element length != 2"); | |
json_number_t* naddr = json_value_as_number(pair->start->value); | |
CHECK_MSG(naddr, "pair first is not number"); | |
json_number_t* nval = json_value_as_number(pair->start->next->value); | |
CHECK_MSG(nval, "pair second is not number"); | |
result[index].addr = strtoul(naddr->number, NULL, 10); | |
result[index].val = strtoul(nval->number, NULL, 10); | |
} | |
} | |
} | |
*ram_state_count = index; | |
return OK; | |
ON_ERROR_RETURN; | |
} | |
Result parse_cycle_state(json_array_t* ja, cycle_state* result, | |
int* cycle_state_count) { | |
int index = 0; | |
for (json_array_element_t* jae = ja->start; jae; jae = jae->next, ++index) { | |
CHECK_MSG(index < *cycle_state_count, "too many cycle states"); | |
json_array_t* triple = json_value_as_array(jae->value); | |
CHECK_MSG(triple, "ram element is not array"); | |
CHECK_MSG(triple->length == 3, "ram element length != 3"); | |
json_number_t* naddr = json_value_as_number(triple->start->value); | |
CHECK_MSG(naddr, "triple first is not number"); | |
json_number_t* nval = json_value_as_number(triple->start->next->value); | |
CHECK_MSG(nval, "triple second is not number"); | |
json_string_t* swrite = | |
json_value_as_string(triple->start->next->next->value); | |
CHECK_MSG(swrite, "triple third is not string"); | |
result[index].addr = strtoul(naddr->number, NULL, 10); | |
result[index].val = strtoul(nval->number, NULL, 10); | |
result[index].write = json_string_matches(swrite, "write"); | |
} | |
*cycle_state_count = index; | |
return OK; | |
ON_ERROR_RETURN; | |
} | |
int main(int argc, char** argv) { | |
parse_arguments(argc, argv); | |
E e; | |
C* c = &e.s.c; | |
FileData file_data; | |
CHECK(SUCCESS(file_read(s_json_filename, &file_data))); | |
json_value_t* jv = json_parse(file_data.data, file_data.size); | |
CHECK(jv); | |
json_array_t* ja = json_value_as_array(jv); | |
CHECK(ja); | |
size_t test_count = ja->length; | |
size_t failed = 0; | |
for (json_array_element_t* jae = ja->start; jae; jae = jae->next) { | |
char name[16]; | |
cpu_state init_cpu; | |
ram_state init_ram[20]; | |
int init_ram_count = ARRAY_SIZE(init_ram); | |
cpu_state final_cpu; | |
ram_state final_ram[20]; | |
int final_ram_count = ARRAY_SIZE(final_ram); | |
cycle_state cycles[8]; | |
int cycle_count = ARRAY_SIZE(cycles); | |
json_object_t* jo = json_value_as_object(jae->value); | |
CHECK(jo); | |
for (json_object_element_t* joe = jo->start; joe; joe = joe->next) { | |
if (json_string_matches(joe->name, "name")) { | |
json_string_t* js = json_value_as_string(joe->value); | |
CHECK(js); | |
strncpy(name, js->string, js->string_size); | |
name[MIN(js->string_size, sizeof(name) - 1)] = 0; | |
} else if (json_string_matches(joe->name, "initial")) { | |
json_object_t* jo2 = json_value_as_object(joe->value); | |
CHECK(jo2); | |
CHECK(SUCCESS(parse_cpu_state(jo2, &init_cpu))); | |
CHECK(SUCCESS(parse_ram_state(jo2, init_ram, &init_ram_count))); | |
} else if (json_string_matches(joe->name, "final")) { | |
json_object_t* jo2 = json_value_as_object(joe->value); | |
CHECK(jo2); | |
CHECK(SUCCESS(parse_cpu_state(jo2, &final_cpu))); | |
CHECK(SUCCESS(parse_ram_state(jo2, final_ram, &final_ram_count))); | |
} else if (json_string_matches(joe->name, "cycles")) { | |
json_array_t* ja2 = json_value_as_array(joe->value); | |
CHECK(ja2); | |
CHECK(SUCCESS(parse_cycle_state(ja2, cycles, &cycle_count))); | |
} | |
} | |
// Init CPU | |
ZERO_MEMORY(e); | |
c->PC.val = init_cpu.PC; | |
c->A = init_cpu.A; | |
c->X = init_cpu.X; | |
c->Y = init_cpu.Y; | |
c->S = init_cpu.S; | |
set_P(&e, init_cpu.P); | |
// Init RAM | |
for (int i = 0; i < init_ram_count; ++i) { | |
c->ram[init_ram[i].addr] = init_ram[i].val; | |
} | |
// Step | |
for (int i = 0; i < cycle_count; ++i) { | |
cpu_step(&e); | |
e.s.cy++; | |
} | |
// Check CPU state | |
bool ok = true; | |
ok &= c->PC.val == final_cpu.PC; | |
ok &= c->A == final_cpu.A; | |
ok &= c->X == final_cpu.X; | |
ok &= c->Y == final_cpu.Y; | |
ok &= c->S == final_cpu.S; | |
u8 P = get_P(&e, false); | |
ok &= P == final_cpu.P; | |
// Check RAM state | |
for (int i = 0; i < final_ram_count; ++i) { | |
ok &= c->ram[final_ram[i].addr] == final_ram[i].val; | |
} | |
// Check cycle state | |
for (int i = 0; i < cycle_count; ++i) { | |
ok &= c->log[i].addr == cycles[i].addr; | |
ok &= c->log[i].val == cycles[i].val; | |
ok &= c->log[i].write == cycles[i].write; | |
} | |
// Print any failures | |
if (!ok) { | |
printf("X %s:", name); | |
if (c->PC.val != final_cpu.PC) { | |
printf(" PC:$%04x != $%04x", c->PC.val, final_cpu.PC); | |
} | |
if (c->A != final_cpu.A) { | |
printf(" A:$%02x != $%02x", c->A, final_cpu.A); | |
} | |
if (c->X != final_cpu.X) { | |
printf(" X:$%02x != $%02x", c->X, final_cpu.X); | |
} | |
if (c->Y != final_cpu.Y) { | |
printf(" Y:$%02x != $%02x", c->Y, final_cpu.Y); | |
} | |
if (P != final_cpu.P) { | |
printf(" P:$%02x != $%02x", P, final_cpu.P); | |
} | |
if (c->S != final_cpu.S) { | |
printf(" S:$%02x != $%02x", c->S, final_cpu.S); | |
} | |
for (int i = 0; i < final_ram_count; ++i) { | |
u16 addr = final_ram[i].addr; | |
u8 actual = c->ram[addr]; | |
u8 expected = final_ram[i].val; | |
if (actual != expected) { | |
printf(" R[$%04x]:$%02x != $%02x", addr, actual, expected); | |
} | |
} | |
for (int i = 0; i < cycle_count; ++i) { | |
if (c->log[i].addr != cycles[i].addr || | |
c->log[i].val != cycles[i].val || | |
c->log[i].write != cycles[i].write) { | |
printf(" C[%u]:{$%04x,$%02x,%u} != {$%04x,$%02x,%u}", i, | |
c->log[i].addr, c->log[i].val, c->log[i].write, cycles[i].addr, | |
cycles[i].val, cycles[i].write); | |
} | |
} | |
printf("\n"); | |
failed++; | |
} | |
} | |
printf("Passed %zu/%zu tests.\n", test_count - failed, test_count); | |
return 0; | |
error: | |
return 1; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment