diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7a6719..79ffb6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,8 +6,13 @@ jobs: steps: - uses: actions/checkout@v4 - run: sudo apt-get update - - run: sudo apt-get install -y cc65 libsdl2-dev libsdl2-ttf-dev fonts-dejavu-core + - run: sudo apt-get install -y cc65 libsdl2-dev libsdl2-ttf-dev fonts-dejavu-core libc6-i386 unzip - run: make debug - run: make sanitize - run: make test-functional - - run: make games/snake.bin bin/mos6502-sdl + - run: make test-decimal + - run: make test-interrupt + - run: make test-apple1 + - run: make test-nestest + - run: make test-vectors + - run: make games/snake.bin bin/mos6502-sdl bin/nes diff --git a/.gitignore b/.gitignore index 10f1bcf..75ade1d 100644 --- a/.gitignore +++ b/.gitignore @@ -80,5 +80,8 @@ games/snake.bin snake.highscore snake.state tests/roms/*.bin +tests/roms/*.nes +tests/roms/*.log +tests/vectors/ # End of https://www.toptal.com/developers/gitignore/api/c,visualstudiocode diff --git a/CLAUDE.md b/CLAUDE.md index f0dadae..f9787ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,6 +16,8 @@ make snake # assemble and run the SDL game (requires SDL2 + cc65) - `Bus6502` owns RAM and Easy6502 MMIO; CPU code only uses bus accessors. - `cpu_step` executes one instruction or interrupt and returns status/opcode/cycles. - `src/opcode.c` has a 256-entry descriptor table for all 151 official encodings. +- `src/state.c` owns the portable, versioned state file format. +- `src/apple1.c` maps Apple I keyboard/display PIA registers through bus hooks. - `src/frontend.c` is an optional SDL2 frontend with a dependency-free stub. - `tests/run_tests.c` is the in-tree test harness; `tests/run_suite.c` runs external ROMs. diff --git a/Makefile b/Makefile index b719c03..9ef8d48 100644 --- a/Makefile +++ b/Makefile @@ -8,15 +8,48 @@ CPPFLAGS += -DHAVE_SDL_UI $(SDL_CFLAGS) LDLIBS += $(SDL_LIBS) endif -CORE_SRC = src/addressing.c src/bus.c src/cpu.c src/loader.c src/memory.c src/opcode.c +CORE_SRC = src/addressing.c src/apple1.c src/bus.c src/cpu.c src/loader.c src/memory.c src/nes.c src/opcode.c src/state.c APP_SRC = $(CORE_SRC) src/frontend.c src/main.c APP_OBJ = $(APP_SRC:src/%.c=build/%.o) TARGET = bin/mos6502 SDL_TARGET = bin/mos6502-sdl -.PHONY: all clean test test-rom test-functional fetch-roms debug sanitize snake check-deps FORCE +.PHONY: all clean test test-rom test-functional test-decimal test-interrupt test-apple1 test-nestest test-vectors fetch-roms debug sanitize snake check-deps FORCE all: $(TARGET) +bin/apple1: $(CORE_SRC) src/apple1_main.c + @mkdir -p bin + $(CC) $(CPPFLAGS) $(CFLAGS) $(CORE_SRC) src/apple1_main.c -o $@ + +bin/debugger: $(CORE_SRC) src/debugger_main.c + @mkdir -p bin + $(CC) $(CPPFLAGS) $(CFLAGS) $(CORE_SRC) src/debugger_main.c -o $@ + +bin/nes: $(CORE_SRC) src/nes_main.c + @mkdir -p bin + $(CC) -Iinclude $(CFLAGS) $(SDL_CFLAGS) $(CORE_SRC) src/nes_main.c $(SDL_LIBS) -o $@ + +bin/run_apple1: $(CORE_SRC) tests/run_apple1.c + @mkdir -p bin + $(CC) $(CPPFLAGS) $(CFLAGS) $(CORE_SRC) tests/run_apple1.c -o $@ + +test-apple1: tests/roms/wozmon.bin bin/run_apple1 + ./bin/run_apple1 $< + +bin/run_nestest: $(CORE_SRC) tests/run_nestest.c + @mkdir -p bin + $(CC) $(CPPFLAGS) $(CFLAGS) $(CORE_SRC) tests/run_nestest.c -o $@ + +test-nestest: tests/roms/nestest.nes tests/roms/nestest.log bin/run_nestest + ./bin/run_nestest tests/roms/nestest.nes tests/roms/nestest.log + +bin/run_vectors: $(CORE_SRC) tests/run_vectors.c + @mkdir -p bin + $(CC) $(CPPFLAGS) $(CFLAGS) $(CORE_SRC) tests/run_vectors.c -o $@ + +test-vectors: bin/run_vectors + python3 tools/test_vectors.py --download + $(TARGET): $(APP_OBJ) @mkdir -p bin $(CC) $(CFLAGS) $(APP_OBJ) $(LDLIBS) -o $@ @@ -51,13 +84,59 @@ tests/roms/6502_functional_test.bin: curl -L --fail -o $@ https://raw.githubusercontent.com/Klaus2m5/6502_65C02_functional_tests/master/bin_files/6502_functional_test.bin @echo "fa12bfc761e6f9057e4cc01a665a7b800ff01ae91f598af1e39a1201d01953fd $@" | sha256sum -c - -fetch-roms: tests/roms/6502_functional_test.bin +tests/roms/6502_decimal_test.bin: + @mkdir -p tests/roms + curl -L --fail -o $@ https://raw.githubusercontent.com/JetSetIlly/Gopher2600/master/hardware/cpu/tests/klaus2m5/decimal_mode/6502_decimal_test.bin + @echo "03798ab778456cc350044fdbe28b4078278648892712b994cdbdda09018674e7 $@" | sha256sum -c - + +tests/roms/6502_interrupt_test.bin: + @mkdir -p tests/roms build/as65 + curl -L --fail -o build/as65/as65.zip https://raw.githubusercontent.com/Klaus2m5/6502_65C02_functional_tests/master/as65_142.zip + unzip -oq build/as65/as65.zip -d build/as65 + curl -L --fail -o build/as65/6502_interrupt_test.a65 https://raw.githubusercontent.com/Klaus2m5/6502_65C02_functional_tests/master/6502_interrupt_test.a65 + chmod +x build/as65/as65 + cd build/as65 && ./as65 -pmnu 6502_interrupt_test.a65 + cp build/as65/6502_interrupt_test.bin $@ + @echo "2657b5ce08bb59e2de69363c9174c4d0f7f475bcf2ce844eb20cccfbe4374c72 $@" | sha256sum -c - + +tests/roms/wozmon.bin: + @mkdir -p tests/roms build/wozmon + curl -L --fail -o build/wozmon/wozmon.s https://raw.githubusercontent.com/jefftranter/6502/master/asm/wozmon/wozmon.s + ca65 build/wozmon/wozmon.s -o build/wozmon/wozmon.o + ld65 -C /usr/share/cc65/cfg/none.cfg -o $@ build/wozmon/wozmon.o + @echo "e5af0d1c4057bd8e0ef5cb069c208ff7cc0984a7dff53b12c5cf119de8cb5c25 $@" | sha256sum -c - + +tests/roms/nestest.nes: + @mkdir -p tests/roms + curl -L --fail -o $@ http://www.qmtpro.com/~nes/misc/nestest.nes + @echo "f67d55fd6b3cf0bad1cc85f1df0d739c65b53e79cecb7fea8f77ec0eadab0004 $@" | sha256sum -c - + +tests/roms/nestest.log: + @mkdir -p tests/roms + curl -L --fail -o $@ http://www.qmtpro.com/~nes/misc/nestest.log + @echo "627c8e180b1a924dfa705c5dc6958fad7ab75a62de556173caf880ccc1337540 $@" | sha256sum -c - + +fetch-roms: tests/roms/6502_functional_test.bin tests/roms/6502_decimal_test.bin tests/roms/6502_interrupt_test.bin tests/roms/wozmon.bin tests/roms/nestest.nes tests/roms/nestest.log test-functional: tests/roms/6502_functional_test.bin bin/run_suite ./bin/run_suite $< 0 0x400 0x3469 100000000 +bin/run_decimal: $(CORE_SRC) tests/run_decimal.c + @mkdir -p bin + $(CC) $(CPPFLAGS) $(CFLAGS) $(CORE_SRC) tests/run_decimal.c -o $@ + +test-decimal: tests/roms/6502_decimal_test.bin bin/run_decimal + ./bin/run_decimal $< + +bin/run_interrupt: $(CORE_SRC) tests/run_interrupt.c + @mkdir -p bin + $(CC) $(CPPFLAGS) $(CFLAGS) $(CORE_SRC) tests/run_interrupt.c -o $@ + +test-interrupt: tests/roms/6502_interrupt_test.bin bin/run_interrupt + ./bin/run_interrupt $< + debug: CFLAGS = -O0 -g3 -std=c11 -Wall -Wextra -Wpedantic -Werror -debug: clean all test +debug: clean all bin/debugger bin/apple1 bin/nes test sanitize: CFLAGS = -O1 -g3 -std=c11 -Wall -Wextra -Wpedantic -fsanitize=address,undefined sanitize: export ASAN_OPTIONS = detect_leaks=0 diff --git a/README.md b/README.md index b877f15..ca8739f 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ an explicit error. make # core and command-line emulator make test # unit/integration checks make test-functional # official Klaus Dormann functional suite +make test-decimal # exhaustive NMOS BCD accumulator/flag suite +make test-interrupt # IRQ/NMI/BRK integration suite +make test-vectors # 2.36M independent single-instruction vectors (large download) make debug # strict -Werror debug build and tests make sanitize # AddressSanitizer/UBSan build and tests ``` @@ -49,6 +52,14 @@ bin/mos6502 program.bin --load 0x0600 --start 0x0600 Useful options are `--hz`, `--scale`, `--seed`, `--headless`, and `--trace`. +An interactive debugger provides stepping, continue, breakpoints, registers, +memory examination/modification, disassembly, and save/load commands: + +```sh +make bin/debugger +bin/debugger program.bin 0x0600 0x0600 +``` + External functional ROMs can be run without modifying the application: ```sh @@ -64,11 +75,39 @@ This is deliberately a small game-oriented computer, not an NES emulator. Original arcade Pac-Man used a Z80; running that ROM would require a different CPU and hardware platform. +## Apple I + +The Apple I terminal target implements the keyboard/display PIA registers at +`$D010-$D013`. Supply a legally obtained 256-byte Woz Monitor image and press +Ctrl-C to exit: + +```sh +make bin/apple1 +bin/apple1 WOZMON.bin +``` + +## Nintendo Entertainment System + +The NES target implements the Ricoh 2A03 CPU behavior, iNES parsing, mappers 0 +(NROM), 2 (UxROM), and 3 (CNROM), mirrored CPU RAM and PPU registers, controller shift register, OAM DMA, +vblank/NMI timing, background scrolling/attributes, sprites, palette rendering, +the pulse/triangle/noise APU channels, and an SDL video/audio frontend: + +```sh +make bin/nes +bin/nes game.nes +``` + +Controls are arrows, Z (B), X (A), Right Shift (Select), Enter (Start), and +Escape. Use legally obtained ROMs. `make test-nestest` verifies all 8,991 +canonical CPU trace lines and cumulative cycle counts. + See [the design](docs/design.md) and [project status](docs/plan.md). ## Validation status -The in-tree tests exercise the CPU's major correctness boundaries. External -Klaus Dormann functional and decimal ROM validation is the next hardening gate; -the binaries are not vendored because their redistribution terms must be -respected. +The automated suite passes Klaus Dormann's functional and interrupt ROMs, +Bruce Clark's exhaustive decimal test, all 8,991 canonical `nestest` trace +lines, an authentic Woz Monitor interaction, and 2,360,000 SingleStepTests +vectors. Downloaded fixtures are checksum-verified or cached locally rather +than vendored. diff --git a/docs/plan.md b/docs/plan.md index 32569f9..c05c19d 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -7,23 +7,33 @@ - Binary and NMOS decimal ADC/SBC, status flags, stack, BRK/RTI, IRQ and NMI. - All official addressing modes, zero-page wrap, page penalties and indirect-JMP bug. - Instruction-level cycle accounting and deterministic illegal-opcode errors. +- Stable undocumented NMOS opcode families and multi-byte NOP encodings. - Safe flat-binary loader, headless runner, optional SDL2 frontend. +- Versioned, endian-defined, CRC32-protected portable save states. +- Extensible bus device hooks and an Apple I terminal/PIA target. +- NES Ricoh 2A03 mode, iNES mappers 0/2/3, PPU register/timing model, controller, + scrolling, masks, 8x8/8x16 sprites, sprite status, pulse/triangle/noise audio, + SDL frontend, and full 8,991-line nestest match. - Original ca65 Snake game with pause, persistent high score, and a reproducible `make snake` target. - Strict debug, sanitizer and dependency-free test targets. -## Remaining hardening +## Validation -1. Run the Klaus Dormann decimal suite and fix any discovered edge cases. -2. Expand table-driven tests to cover every official opcode/address-mode pairing. -3. Add a trace/disassembler runner for diagnosing external ROM failures. +- 2,360,000 independent SingleStepTests cases cover every implemented official + and stable undocumented opcode, final CPU/RAM state, and instruction cycles. +- The interactive debugger supports stepping, continue, breakpoints, register and + memory inspection/editing, disassembly, and portable save/load. The official Klaus Dormann functional suite passes at `$3469` after 30,646,176 -instructions. Linux CI covers strict, sanitizer, functional-ROM, Snake, and SDL -UI builds. +instructions. Linux CI covers strict, sanitizer, all external CPU/platform suites, +the complete single-step corpus, Snake, and SDL UI builds. -## Future platforms +Bruce Clark's exhaustive NMOS decimal suite passes all 131,072 operand/carry +combinations, including invalid BCD digits. Klaus Dormann's interrupt suite +passes IRQ, NMI, BRK, vector, flag, and concurrent-interrupt checks. -- Undocumented NMOS opcodes. -- Audio, save states and an interactive debugger. -- A real historical machine such as Apple I or NES. NES support is a separate major - project requiring the PPU, APU, controllers, cartridge parsing/mappers and tighter timing. +## Explicit compatibility boundary + +The NES frontend is a useful first-generation NES subsystem, not a claim of full +commercial-library compatibility. Mappers other than 0, 2, and 3, DMC audio, +cycle-by-cycle PPU bus effects, and mapper IRQs remain outside its stated scope. diff --git a/include/apple1.h b/include/apple1.h new file mode 100644 index 0000000..5ac8444 --- /dev/null +++ b/include/apple1.h @@ -0,0 +1,14 @@ +#ifndef APPLE1_H +#define APPLE1_H +#include +#include +#include +#include "bus.h" +typedef struct { + uint8_t key;bool key_ready;bool stdout_enabled; + char output[8192];size_t output_length; +} Apple1IO; +void apple1_init(Apple1IO *io,Bus6502 *bus); +void apple1_key(Apple1IO *io,uint8_t ascii); +void apple1_enable_stdout(Apple1IO *io,bool enabled); +#endif diff --git a/include/bus.h b/include/bus.h index 557e168..a2e1204 100644 --- a/include/bus.h +++ b/include/bus.h @@ -5,13 +5,20 @@ #include #include "memory.h" -typedef struct Bus6502 { +typedef struct Bus6502 Bus6502; +typedef bool (*bus_read_hook_t)(void *userdata,uint16_t addr,uint8_t *value); +typedef bool (*bus_write_hook_t)(void *userdata,uint16_t addr,uint8_t value); + +struct Bus6502 { Memory memory; uint8_t keyboard; uint32_t rng_state; bool easy6502_io; bool framebuffer_dirty; -} Bus6502; + bus_read_hook_t read_hook; + bus_write_hook_t write_hook; + void *hook_userdata; +}; void bus_init(Bus6502 *bus); uint8_t bus_read(Bus6502 *bus, uint16_t addr); @@ -20,5 +27,7 @@ uint16_t bus_read16(Bus6502 *bus, uint16_t addr); void bus_write16(Bus6502 *bus, uint16_t addr, uint16_t value); void bus_set_keyboard(Bus6502 *bus, uint8_t key); void bus_seed_rng(Bus6502 *bus, uint32_t seed); +void bus_set_hooks(Bus6502 *bus,bus_read_hook_t read_hook, + bus_write_hook_t write_hook,void *userdata); #endif diff --git a/include/cpu.h b/include/cpu.h index 2770be8..6847049 100644 --- a/include/cpu.h +++ b/include/cpu.h @@ -13,6 +13,7 @@ typedef struct CPU6502 { uint8_t P; bool nmi_pending; bool irq_asserted; + bool decimal_enabled; } CPU6502; enum { diff --git a/include/nes.h b/include/nes.h new file mode 100644 index 0000000..2c8807d --- /dev/null +++ b/include/nes.h @@ -0,0 +1,28 @@ +#ifndef NES_H +#define NES_H +#include +#include +#include +#include "bus.h" +#include "cpu.h" + +typedef struct { + uint8_t prg[512*1024],chr[256*1024],ram[2048],prg_ram[8192]; + uint8_t nametable[4096],palette[32],oam[256]; + size_t prg_size,chr_size;uint8_t mapper,prg_bank,chr_bank,mirroring;bool chr_ram; + uint8_t ppu_ctrl,ppu_mask,ppu_status,oam_addr,ppu_latch,read_buffer; + uint8_t scroll_x,scroll_y; + uint16_t ppu_addr;unsigned ppu_dot,ppu_scanline;bool frame_ready; + uint8_t controller,controller_shift;bool controller_strobe; + uint8_t apu_reg[0x18],apu_status;uint64_t apu_cycles; + double pulse_phase[2],triangle_phase,noise_phase;uint16_t noise_lfsr; + Bus6502 bus;CPU6502 cpu; +} NES; + +int nes_load(NES *nes,const char *path); +void nes_reset(NES *nes); +cpu_step_result_t nes_step(NES *nes); +void nes_set_controller(NES *nes,uint8_t buttons); +void nes_render_frame(NES *nes,uint32_t pixels[256*240]); +float nes_audio_sample(NES *nes,double sample_rate); +#endif diff --git a/include/state.h b/include/state.h new file mode 100644 index 0000000..ed7413e --- /dev/null +++ b/include/state.h @@ -0,0 +1,7 @@ +#ifndef STATE_H +#define STATE_H +#include "bus.h" +#include "cpu.h" +int state_save(const char *path,const CPU6502 *cpu,const Bus6502 *bus); +int state_load(const char *path,CPU6502 *cpu,Bus6502 *bus); +#endif diff --git a/src/apple1.c b/src/apple1.c new file mode 100644 index 0000000..f8d0b6e --- /dev/null +++ b/src/apple1.c @@ -0,0 +1,31 @@ +#include +#include +#include +#include "apple1.h" + +static bool apple1_read(void *userdata,uint16_t addr,uint8_t *value) +{ + Apple1IO *io=userdata; + if(addr==0xD010){*value=io->key|0x80;io->key_ready=false;return true;} + if(addr==0xD011){*value=io->key_ready?0x80:0;return true;} + if(addr==0xD012||addr==0xD013){*value=0;return true;} + return false; +} + +static bool apple1_write(void *userdata,uint16_t addr,uint8_t value) +{ + Apple1IO *io=userdata; + if(addr==0xD012){ + uint8_t c=value&0x7F;if(c=='\r')c='\n'; + if(io->output_length+1output){io->output[io->output_length++]=(char)c;io->output[io->output_length]=0;} + if(io->stdout_enabled){putchar(c);fflush(stdout);}return true; + } + if(addr==0xD010||addr==0xD011||addr==0xD013)return true; + return false; +} + +void apple1_init(Apple1IO *io,Bus6502 *bus) +{ memset(io,0,sizeof(*io));bus_set_hooks(bus,apple1_read,apple1_write,io); } +void apple1_key(Apple1IO *io,uint8_t ascii) +{ io->key=(uint8_t)toupper(ascii);io->key_ready=true; } +void apple1_enable_stdout(Apple1IO *io,bool enabled){io->stdout_enabled=enabled;} diff --git a/src/apple1_main.c b/src/apple1_main.c new file mode 100644 index 0000000..6c8512d --- /dev/null +++ b/src/apple1_main.c @@ -0,0 +1,24 @@ +#define _DEFAULT_SOURCE +#include +#include +#include +#include +#include "apple1.h" +#include "cpu.h" +#include "loader.h" + +int main(int argc,char **argv) +{ + Bus6502 bus;CPU6502 cpu;Apple1IO io;size_t loaded;struct termios old,raw;int tty=0; + if(argc!=2){fprintf(stderr,"usage: %s WOZMON.bin\n",argv[0]);return 2;} + bus_init(&bus);apple1_init(&io,&bus);apple1_enable_stdout(&io,true); + if(load_binary(&bus,argv[1],0xFF00,&loaded)||loaded!=256){fprintf(stderr,"Apple I monitor ROM must be exactly 256 bytes\n");return 1;} + cpu_reset(&cpu,&bus); + if(tcgetattr(STDIN_FILENO,&old)==0){tty=1;raw=old;cfmakeraw(&raw);tcsetattr(STDIN_FILENO,TCSANOW,&raw);} + fcntl(STDIN_FILENO,F_SETFL,fcntl(STDIN_FILENO,F_GETFL)|O_NONBLOCK); + for(;;){ + unsigned char key;if(read(STDIN_FILENO,&key,1)==1){if(key==3)break;apple1_key(&io,key);} + for(unsigned n=0;n<1000;n++)if(cpu_step(&cpu,&bus).status!=CPU_STEP_OK)goto done; + } +done:if(tty)tcsetattr(STDIN_FILENO,TCSANOW,&old);putchar('\n');return 0; +} diff --git a/src/bus.c b/src/bus.c index 47f9bbc..80fe314 100644 --- a/src/bus.c +++ b/src/bus.c @@ -17,6 +17,8 @@ static uint8_t random_byte(Bus6502 *bus) uint8_t bus_read(Bus6502 *bus, uint16_t addr) { + uint8_t value; + if(bus->read_hook&&bus->read_hook(bus->hook_userdata,addr,&value))return value; if (bus->easy6502_io) { if (addr == 0x00FE) return random_byte(bus); if (addr == 0x00FF) return bus->keyboard; @@ -26,6 +28,7 @@ uint8_t bus_read(Bus6502 *bus, uint16_t addr) void bus_write(Bus6502 *bus, uint16_t addr, uint8_t value) { + if(bus->write_hook&&bus->write_hook(bus->hook_userdata,addr,value))return; mem_write(&bus->memory, addr, value); if (addr >= 0x0200 && addr <= 0x05FF) bus->framebuffer_dirty = true; } @@ -45,3 +48,5 @@ void bus_write16(Bus6502 *bus, uint16_t addr, uint16_t value) void bus_set_keyboard(Bus6502 *bus, uint8_t key) { bus->keyboard = key; } void bus_seed_rng(Bus6502 *bus, uint32_t seed) { bus->rng_state = seed ? seed : 1; } +void bus_set_hooks(Bus6502 *bus,bus_read_hook_t read_hook,bus_write_hook_t write_hook,void *userdata) +{ bus->read_hook=read_hook;bus->write_hook=write_hook;bus->hook_userdata=userdata; } diff --git a/src/cpu.c b/src/cpu.c index bb0c053..a3eb868 100644 --- a/src/cpu.c +++ b/src/cpu.c @@ -8,6 +8,7 @@ void cpu_reset(CPU6502 *cpu, Bus6502 *bus) memset(cpu, 0, sizeof(*cpu)); cpu->SP = 0xFD; cpu->P = FLAG_U | FLAG_I; + cpu->decimal_enabled = true; cpu->PC = bus_read16(bus, 0xFFFC); } diff --git a/src/debugger_main.c b/src/debugger_main.c new file mode 100644 index 0000000..4082571 --- /dev/null +++ b/src/debugger_main.c @@ -0,0 +1,42 @@ +#include +#include +#include +#include +#include "bus.h" +#include "cpu.h" +#include "loader.h" +#include "opcode.h" +#include "state.h" + +static unsigned long number(const char *s,int *ok) +{ char *end;unsigned long n=strtoul(s,&end,0);while(isspace((unsigned char)*end))end++;*ok=*s&&!*end&&n<=0xFFFF;return n; } +static void show(CPU6502 *c,Bus6502 *b) +{ char text[64];cpu_disassemble(b,c->PC,text,sizeof text);printf("PC:%04X A:%02X X:%02X Y:%02X P:%02X SP:%02X %s\n",c->PC,c->A,c->X,c->Y,c->P,c->SP,text); } +static int one(CPU6502 *c,Bus6502 *b) +{ uint16_t pc=c->PC;cpu_step_result_t r=cpu_step(c,b);if(r.status!=CPU_STEP_OK){fprintf(stderr,"illegal opcode $%02X at $%04X\n",r.opcode,pc);return 0;}return 1; } + +int main(int argc,char **argv) +{ + Bus6502 bus;CPU6502 cpu;size_t loaded;uint8_t breaks[65536]={0};char line[256];uint16_t load=0,start=0; + if(argc<2||argc>4){fprintf(stderr,"usage: %s ROM [LOAD [START]]\n",argv[0]);return 2;} + if(argc>2)load=(uint16_t)strtoul(argv[2],NULL,0); + if(argc>3)start=(uint16_t)strtoul(argv[3],NULL,0);else start=load; + bus_init(&bus);if(load_binary(&bus,argv[1],load,&loaded)){fprintf(stderr,"could not load ROM\n");return 1;} + bus_write16(&bus,0xFFFC,start);cpu_reset(&cpu,&bus);printf("Loaded %zu bytes. Commands: s, c, b, r, m, w, save, load, q, h\n",loaded);show(&cpu,&bus); + while(fputs("dbg> ",stdout),fflush(stdout),fgets(line,sizeof line,stdin)){ + char *cmd=strtok(line," \t\r\n"),*arg=strtok(NULL," \t\r\n"),*arg2=strtok(NULL," \t\r\n");int ok=0; + if(!cmd)continue; + if(!strcmp(cmd,"q")||!strcmp(cmd,"quit"))break; + else if(!strcmp(cmd,"s")||!strcmp(cmd,"step")){unsigned long n=arg?strtoul(arg,NULL,0):1;while(n--&&one(&cpu,&bus));show(&cpu,&bus);} + else if(!strcmp(cmd,"c")||!strcmp(cmd,"continue")){for(unsigned long n=0;n<100000000&&one(&cpu,&bus);n++)if(breaks[cpu.PC])break;show(&cpu,&bus);} + else if(!strcmp(cmd,"b")||!strcmp(cmd,"break")){if(!arg){puts("break requires an address");continue;}unsigned long a=number(arg,&ok);if(ok){breaks[a]^=1;printf("breakpoint $%04lX %s\n",a,breaks[a]?"set":"cleared");}} + else if(!strcmp(cmd,"r")||!strcmp(cmd,"registers"))show(&cpu,&bus); + else if(!strcmp(cmd,"m")||!strcmp(cmd,"memory")){unsigned long a=arg?number(arg,&ok):cpu.PC,n=arg2?strtoul(arg2,NULL,0):64;if(!arg)ok=1;if(ok)for(unsigned long i=0;i #include #include +#include "state.h" static const uint32_t palette[16] = { 0x071018FF,0x62F294FF,0xFF5D73FF,0x8FFFAFFF,0x638CFFFF,0xD879FFFF, @@ -27,25 +28,6 @@ static void save_high_score(uint8_t score) if(file){fputc(score,file);fclose(file);} } -static bool save_state(const CPU6502 *cpu, const Bus6502 *bus) -{ - FILE *f=fopen("snake.state","wb");const char magic[8]="MOSST01"; - if(!f)return false; - bool ok=fwrite(magic,1,8,f)==8&&fwrite(cpu,1,sizeof(*cpu),f)==sizeof(*cpu)&& - fwrite(bus->memory.data,1,MEM_SIZE,f)==MEM_SIZE; - return fclose(f)==0&&ok; -} - -static bool load_state(CPU6502 *cpu, Bus6502 *bus) -{ - FILE *f=fopen("snake.state","rb");char magic[8];CPU6502 saved; - if(!f)return false; - bool ok=fread(magic,1,8,f)==8&&!memcmp(magic,"MOSST01",8)&& - fread(&saved,1,sizeof(saved),f)==sizeof(saved)&& - fread(bus->memory.data,1,MEM_SIZE,f)==MEM_SIZE; - fclose(f);if(ok){*cpu=saved;bus->framebuffer_dirty=true;}return ok; -} - static void play_tone(SDL_AudioDeviceID device, int frequency, int milliseconds) { int16_t samples[4410];int count=44100*milliseconds/1000; @@ -197,10 +179,10 @@ int frontend_run(CPU6502 *cpu, Bus6502 *bus, unsigned hz, unsigned scale) paused=!paused; budget=0; bus->framebuffer_dirty=true; update_title(window,score,high,paused); } else if(!event.key.repeat && k==SDLK_F5) { - if(save_state(cpu,bus)) SDL_SetWindowTitle(window,"MOS 6502 Snake - State saved"); + if(state_save("snake.state",cpu,bus)==0) SDL_SetWindowTitle(window,"MOS 6502 Snake - State saved"); } else if(!event.key.repeat && k==SDLK_F9) { - if(load_state(cpu,bus)){score=bus->memory.data[0x00FD];game_state=bus->memory.data[0x00FB];update_title(window,score,high,paused);} + if(state_load("snake.state",cpu,bus)==0){score=bus->memory.data[0x00FD];game_state=bus->memory.data[0x00FB];update_title(window,score,high,paused);} } else if(k==SDLK_w||k==SDLK_UP) bus_set_keyboard(bus,'w'); else if(k==SDLK_a||k==SDLK_LEFT) bus_set_keyboard(bus,'a'); diff --git a/src/nes.c b/src/nes.c new file mode 100644 index 0000000..e3c7d6c --- /dev/null +++ b/src/nes.c @@ -0,0 +1,116 @@ +#include +#include +#include +#include "nes.h" + +static uint16_t nt_index(const NES *n,uint16_t a) +{ unsigned table=((a-0x2000)&0x0FFF)>>10,offset=a&0x3FF;if(n->mirroring==2)return (uint16_t)(table*0x400+offset);return (uint16_t)(((n->mirroring?table&1:table>>1)<<10)|offset); } +static uint8_t palette_index(uint16_t a) +{ uint8_t p=(uint8_t)(a&0x1F);if((p&0x13)==0x10)p-=0x10;return p; } +static size_t chr_offset(const NES *n,uint16_t a) +{ return n->mapper==3?((size_t)n->chr_bank*8192+(a&0x1FFF))%n->chr_size:(a&0x1FFF)%n->chr_size; } +static uint8_t ppu_read(NES *n,uint16_t a) +{ a&=0x3FFF;if(a<0x2000)return n->chr[chr_offset(n,a)];if(a<0x3F00)return n->nametable[nt_index(n,a)];return n->palette[palette_index(a)]; } +static void ppu_write(NES *n,uint16_t a,uint8_t v) +{ a&=0x3FFF;if(a<0x2000){if(n->chr_ram)n->chr[chr_offset(n,a)]=v;}else if(a<0x3F00)n->nametable[nt_index(n,a)]=v;else n->palette[palette_index(a)]=v; } + +static size_t prg_offset(const NES *n,uint16_t a) +{ + if(n->mapper==2){size_t banks=n->prg_size/16384,bank=a<0xC000?n->prg_bank%(banks-1):banks-1;return bank*16384+(a&0x3FFF);} + return (a-0x8000)%n->prg_size; +} + +static bool nes_read(void *ud,uint16_t a,uint8_t *v) +{ + NES *n=ud; + if(a<0x2000){*v=n->ram[a&0x7FF];return true;} + if(a<0x4000){ + switch(a&7){ + case 2:*v=n->ppu_status;n->ppu_status&=0x7F;n->ppu_latch=0;return true; + case 4:*v=n->oam[n->oam_addr];return true; + case 7:{uint8_t value=ppu_read(n,n->ppu_addr);if((n->ppu_addr&0x3FFF)<0x3F00){*v=n->read_buffer;n->read_buffer=value;}else{*v=value;n->read_buffer=ppu_read(n,n->ppu_addr-0x1000);}n->ppu_addr+=(n->ppu_ctrl&4)?32:1;return true;} + default:*v=0;return true;} + } + if(a==0x4016){*v=n->controller_shift&1;if(!n->controller_strobe)n->controller_shift=(n->controller_shift>>1)|0x80;return true;} + if(a==0x4015){*v=n->apu_status;return true;} + if(a>=0x4000&&a<0x4020){*v=0;return true;} + if(a>=0x6000&&a<0x8000){*v=n->prg_ram[a-0x6000];return true;} + if(a>=0x8000){*v=n->prg[prg_offset(n,a)];return true;} + *v=0;return true; +} + +static bool nes_write(void *ud,uint16_t a,uint8_t v) +{ + NES *n=ud; + if(a<0x2000){n->ram[a&0x7FF]=v;return true;} + if(a<0x4000){switch(a&7){case 0:n->ppu_ctrl=v;break;case 1:n->ppu_mask=v;break;case 3:n->oam_addr=v;break;case 4:n->oam[n->oam_addr++]=v;break;case 5:if(!n->ppu_latch)n->scroll_x=v;else n->scroll_y=v;n->ppu_latch^=1;break;case 6:if(!n->ppu_latch)n->ppu_addr=(uint16_t)(v&0x3F)<<8;else n->ppu_addr=(n->ppu_addr&0xFF00)|v;n->ppu_latch^=1;break;case 7:ppu_write(n,n->ppu_addr,v);n->ppu_addr+=(n->ppu_ctrl&4)?32:1;break;}return true;} + if(a==0x4014){for(unsigned i=0;i<256;i++){uint8_t x;nes_read(n,(uint16_t)(v*256+i),&x);n->oam[n->oam_addr++]=x;}return true;} + if(a==0x4016){n->controller_strobe=v&1;if(n->controller_strobe)n->controller_shift=n->controller;return true;} + if(a>=0x4000&&a<0x4018){n->apu_reg[a-0x4000]=v;if(a==0x4015)n->apu_status=v&0x1F;return true;} + if(a>=0x6000&&a<0x8000){n->prg_ram[a-0x6000]=v;return true;} + if(a>=0x8000){if(n->mapper==2)n->prg_bank=v;if(n->mapper==3)n->chr_bank=v;return true;}return false; +} + +int nes_load(NES *n,const char *path) +{ + uint8_t h[16];FILE *f=fopen(path,"rb");size_t prg,chr;unsigned mapper; + if(!f)return errno; + memset(n,0,sizeof(*n)); + if(fread(h,1,16,f)!=16||memcmp(h,"NES\x1A",4)){fclose(f);return EINVAL;} + mapper=(h[6]>>4)|(h[7]&0xF0);if((mapper!=0&&mapper!=2&&mapper!=3)||h[4]<1){fclose(f);return ENOTSUP;} + if(h[6]&4)fseek(f,512,SEEK_CUR); + prg=(size_t)h[4]*16384;chr=(size_t)h[5]*8192;if(prg>sizeof n->prg||chr>sizeof n->chr){fclose(f);return EFBIG;} + if(fread(n->prg,1,prg,f)!=prg||(chr&&fread(n->chr,1,chr,f)!=chr)){fclose(f);return EIO;} + if(fgetc(f)!=EOF){fclose(f);return EFBIG;}fclose(f); + n->prg_size=prg;n->chr_size=chr?chr:8192;n->chr_ram=chr==0;n->mapper=(uint8_t)mapper;n->mirroring=(h[6]&8)?2:(h[6]&1); + bus_init(&n->bus);bus_set_hooks(&n->bus,nes_read,nes_write,n);return 0; +} + +void nes_reset(NES *n){cpu_reset(&n->cpu,&n->bus);n->cpu.decimal_enabled=false;n->ppu_scanline=n->ppu_dot=0;n->frame_ready=false;n->noise_lfsr=1;} +cpu_step_result_t nes_step(NES *n) +{ + cpu_step_result_t r=cpu_step(&n->cpu,&n->bus);unsigned ticks=r.cycles*3;n->apu_cycles+=r.cycles; + while(ticks--){if(++n->ppu_dot>=341){n->ppu_dot=0;if(++n->ppu_scanline>=262){n->ppu_scanline=0;n->frame_ready=true;}} + if(n->ppu_scanline==241&&n->ppu_dot==1){n->ppu_status|=0x80;if(n->ppu_ctrl&0x80)cpu_request_nmi(&n->cpu);} + if(n->ppu_scanline==261&&n->ppu_dot==1)n->ppu_status&=0x1F;} + return r; +} +void nes_set_controller(NES *n,uint8_t buttons){n->controller=buttons;if(n->controller_strobe)n->controller_shift=buttons;} + +float nes_audio_sample(NES *n,double rate) +{ + static const double cpu_hz=1789773.0;static const uint16_t noise_period[16]={4,8,16,32,64,96,128,160,202,254,380,508,762,1016,2034,4068}; + static const uint8_t duty_bits[4]={1,3,15,63};double mix=0.0; + for(unsigned ch=0;ch<2;ch++)if(n->apu_status&(1u<apu_reg[ch*4];unsigned timer=r[2]|((r[3]&7)<<8);double freq=cpu_hz/(16.0*(timer+1));n->pulse_phase[ch]+=freq/rate;n->pulse_phase[ch]-=(unsigned)n->pulse_phase[ch];unsigned phase=(unsigned)(n->pulse_phase[ch]*8)&7;mix+=((duty_bits[r[0]>>6]>>phase)&1)?(r[0]&15)/30.0:0.0;} + if(n->apu_status&4){uint8_t *r=&n->apu_reg[8];unsigned timer=n->apu_reg[10]|((n->apu_reg[11]&7)<<8);double freq=cpu_hz/(32.0*(timer+1));n->triangle_phase+=freq/rate;n->triangle_phase-=(unsigned)n->triangle_phase;double tri=n->triangle_phase<0.5?n->triangle_phase*4-1:3-n->triangle_phase*4;mix+=tri*(r[0]&0x7F?0.22:0.0);} + if(n->apu_status&8){uint8_t *r=&n->apu_reg[12];double freq=cpu_hz/noise_period[n->apu_reg[14]&15];n->noise_phase+=freq/rate;if(n->noise_phase>=1){unsigned tap=(n->apu_reg[14]&0x80)?6:1;n->noise_lfsr=(uint16_t)((n->noise_lfsr>>1)|(((n->noise_lfsr^(n->noise_lfsr>>tap))&1)<<14));n->noise_phase-=1;}if(!(n->noise_lfsr&1))mix+=(r[0]&15)/45.0;} + if(mix>1)mix=1; + if(mix< -1)mix=-1; + return (float)mix; +} + +void nes_render_frame(NES *n,uint32_t px[256*240]) +{ + static const uint32_t colors[64]={ + 0x666666FF,0x002A88FF,0x1412A7FF,0x3B00A4FF,0x5C007EFF,0x6E0040FF,0x6C0600FF,0x561D00FF,0x333500FF,0x0B4800FF,0x005200FF,0x004F08FF,0x00404DFF,0,0,0, + 0xADADADFF,0x155FD9FF,0x4240FFFF,0x7527FEFF,0xA01ACCFF,0xB71E7BFF,0xB53120FF,0x994E00FF,0x6B6D00FF,0x388700FF,0x0C9300FF,0x008F32FF,0x007C8DFF,0,0,0, + 0xFFFFFFFF,0x64B0FFFF,0x9290FFFF,0xC676FFFF,0xF36AFFFF,0xFE6ECCFF,0xFE8170FF,0xEA9E22FF,0xBCBE00FF,0x88D800FF,0x5CE430FF,0x45E082FF,0x48CDDEFF,0x4F4F4FFF,0,0, + 0xFFFFFFFF,0xC0DFFFFF,0xD3D2FFFF,0xE8C8FFFF,0xFBC2FFFF,0xFEC4EAFF,0xFECCC5FF,0xF7D8A5FF,0xE4E594FF,0xCFEF96FF,0xBDF4ABFF,0xB3F3CCFF,0xB5EBF2FF,0xB8B8B8FF,0,0}; + uint8_t opaque[256*240]={0};uint16_t bg_pattern=(n->ppu_ctrl&0x10)?0x1000:0;n->ppu_status&=0x1F; + for(unsigned y=0;y<240;y++)for(unsigned x=0;x<256;x++){ + unsigned wx=x+n->scroll_x+((n->ppu_ctrl&1)?256:0),wy=y+n->scroll_y+((n->ppu_ctrl&2)?240:0); + unsigned tx=(wx/8)%64,ty=(wy/8)%60,nt=(tx/32)+(ty/30)*2,lx=tx%32,ly=ty%30; + uint16_t base=(uint16_t)(0x2000+nt*0x400);uint8_t tile=ppu_read(n,(uint16_t)(base+ly*32+lx)); + uint8_t attr=ppu_read(n,(uint16_t)(base+0x3C0+(ly/4)*8+lx/4));unsigned shift=((ly&2)<<1)|(lx&2); + uint16_t p=bg_pattern+tile*16+(wy&7);uint8_t bit=(uint8_t)(7-(wx&7));uint8_t index=(uint8_t)(((ppu_read(n,p)>>bit)&1)|(((ppu_read(n,p+8)>>bit)&1)<<1)); + if(!(n->ppu_mask&8)||(x<8&&!(n->ppu_mask&2)))index=0; + unsigned pal=((attr>>shift)&3)*4+index;opaque[y*256+x]=index;px[y*256+x]=colors[n->palette[index?pal:0]&0x3F]; + } + if(n->ppu_mask&0x10){unsigned height=(n->ppu_ctrl&0x20)?16:8; + for(unsigned y=0;y<240;y++){unsigned count=0;for(unsigned s=0;s<64;s++){int sy=n->oam[s*4]+1;if((int)y>=sy&&(int)y8)n->ppu_status|=0x20;}} + for(int s=63;s>=0;s--){int sy=n->oam[s*4]+1,sx=n->oam[s*4+3];uint8_t tile=n->oam[s*4+1],attr=n->oam[s*4+2]; + for(unsigned py=0;py=256||yy<0||yy>=240||(xx<8&&!(n->ppu_mask&4)))continue;unsigned ry=(attr&0x80)?height-1-py:py,rx=(attr&0x40)?(unsigned)pxl:7-(unsigned)pxl;uint16_t base; + if(height==16){base=(uint16_t)((tile&1)*0x1000+(tile&0xFE)*16+(ry>=8?16:0)+(ry&7));}else base=(uint16_t)(((n->ppu_ctrl&8)?0x1000:0)+tile*16+ry); + uint8_t index=(uint8_t)(((ppu_read(n,base)>>rx)&1)|(((ppu_read(n,base+8)>>rx)&1)<<1));if(index&&s==0&&opaque[yy*256+xx]&&xx<255)n->ppu_status|=0x40;if(index&&(!(attr&0x20)||!opaque[yy*256+xx]))px[yy*256+xx]=colors[n->palette[0x10+(attr&3)*4+index]&0x3F];}} + } +} diff --git a/src/nes_main.c b/src/nes_main.c new file mode 100644 index 0000000..aad9f0a --- /dev/null +++ b/src/nes_main.c @@ -0,0 +1,21 @@ +#include +#include +#include "nes.h" + +int main(int argc,char **argv) +{ + NES nes;SDL_Window *w;SDL_Renderer *r;SDL_Texture *t;SDL_AudioDeviceID audio=0;uint32_t pixels[256*240];bool run=true;uint8_t pad=0; + if(argc!=2){fprintf(stderr,"usage: %s game.nes\n",argv[0]);return 2;} + if(nes_load(&nes,argv[1])){fprintf(stderr,"unsupported or invalid iNES ROM (supported mappers: 0, 2, 3)\n");return 1;}nes_reset(&nes); + if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO|SDL_INIT_GAMECONTROLLER)!=0)return 1; + w=SDL_CreateWindow("MOS 6502 - NES",SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,768,720,SDL_WINDOW_RESIZABLE); + r=w?SDL_CreateRenderer(w,-1,SDL_RENDERER_ACCELERATED|SDL_RENDERER_PRESENTVSYNC):NULL;if(w&&!r)r=SDL_CreateRenderer(w,-1,SDL_RENDERER_SOFTWARE); + t=r?SDL_CreateTexture(r,SDL_PIXELFORMAT_RGBA8888,SDL_TEXTUREACCESS_STREAMING,256,240):NULL;if(!t){SDL_Quit();return 1;}SDL_RenderSetLogicalSize(r,256,240); + {SDL_AudioSpec want={0};want.freq=48000;want.format=AUDIO_F32SYS;want.channels=1;want.samples=1024;audio=SDL_OpenAudioDevice(NULL,0,&want,NULL,0);if(audio)SDL_PauseAudioDevice(audio,0);} + while(run){SDL_Event e;while(SDL_PollEvent(&e)){if(e.type==SDL_QUIT)run=false;if(e.type==SDL_KEYDOWN||e.type==SDL_KEYUP){bool on=e.type==SDL_KEYDOWN;uint8_t bit=0;switch(e.key.keysym.sym){case SDLK_x:bit=1;break;case SDLK_z:bit=2;break;case SDLK_RSHIFT:bit=4;break;case SDLK_RETURN:bit=8;break;case SDLK_UP:bit=16;break;case SDLK_DOWN:bit=32;break;case SDLK_LEFT:bit=64;break;case SDLK_RIGHT:bit=128;break;case SDLK_ESCAPE:run=false;break;default:break;}if(on)pad|=bit;else pad&=(uint8_t)~bit;nes_set_controller(&nes,pad);}} + nes.frame_ready=false;while(run&&!nes.frame_ready){cpu_step_result_t step=nes_step(&nes);if(step.status!=CPU_STEP_OK){fprintf(stderr,"illegal opcode $%02X at $%04X\n",step.opcode,(uint16_t)(nes.cpu.PC-1));run=false;}} + if(run){float samples[801];for(unsigned i=0;i<801;i++)samples[i]=nes_audio_sample(&nes,48000);if(audio&&SDL_GetQueuedAudioSize(audio)A, carry = !!(c->P & FLAG_C); uint16_t binary = (uint16_t)a + m + carry; - set_flag(c, FLAG_V, (~(a ^ m) & (a ^ (uint8_t)binary) & 0x80) != 0); - if (c->P & FLAG_D) { - uint16_t decimal = binary; - if ((a & 0x0F) + (m & 0x0F) + carry > 9) decimal += 6; - set_flag(c, FLAG_C, decimal > 0x99); - if (decimal > 0x99) decimal += 0x60; - c->A = (uint8_t)decimal; - set_flag(c, FLAG_Z, (uint8_t)binary == 0); set_flag(c, FLAG_N, binary & 0x80); - } else { set_flag(c, FLAG_C, binary > 0xFF); c->A = (uint8_t)binary; set_nz(c, c->A); } + if ((c->P & FLAG_D) && c->decimal_enabled) { + uint16_t low=(a&0x0F)+(m&0x0F)+carry; + uint16_t intermediate; + if(low>=0x0A)low=((low+6)&0x0F)+0x10; + intermediate=(a&0xF0)+(m&0xF0)+low; + set_flag(c,FLAG_Z,(uint8_t)binary==0); + set_flag(c,FLAG_N,intermediate&0x80); + set_flag(c,FLAG_V,(~(a^m)&(a^(uint8_t)intermediate)&0x80)!=0); + set_flag(c,FLAG_C,intermediate>=0xA0); + if(intermediate>=0xA0)intermediate+=0x60; + c->A=(uint8_t)intermediate; + } else { + set_flag(c,FLAG_V,(~(a^m)&(a^(uint8_t)binary)&0x80)!=0); + set_flag(c, FLAG_C, binary > 0xFF); c->A = (uint8_t)binary; set_nz(c, c->A); + } } static void sbc(CPU6502 *c, uint8_t m) @@ -129,7 +148,7 @@ static void sbc(CPU6502 *c, uint8_t m) uint8_t result = (uint8_t)binary; set_flag(c, FLAG_V, ((a ^ result) & (a ^ m) & 0x80) != 0); set_flag(c, FLAG_C, binary > 0xFF); set_nz(c, result); - if (c->P & FLAG_D) { + if ((c->P & FLAG_D) && c->decimal_enabled) { int16_t lo = (a & 15) - (m & 15) - (carry ? 0 : 1); int16_t hi = (a >> 4) - (m >> 4); if (lo < 0) { lo -= 6; hi--; } @@ -148,6 +167,7 @@ cpu_step_result_t cpu_step(CPU6502 *c, Bus6502 *b) if (c->irq_asserted && !(c->P & FLAG_I)) { interrupt(c,b,0xFFFE,false); out.cycles=7; return out; } out.opcode = bus_read(b, c->PC++); d = ops[out.opcode]; if (d.op == O_BAD) { out.status = CPU_STEP_ILLEGAL_OPCODE; return out; } + if(d.op==O_JSR){uint8_t low=bus_read(b,c->PC++);uint16_t ret=c->PC;push(c,b,(uint8_t)(ret>>8));push(c,b,(uint8_t)ret);c->PC=(uint16_t)(low|((uint16_t)bus_read(b,c->PC)<<8));out.cycles=6;c->P|=FLAG_U;return out;} ar = address(c,b,d.mode); out.cycles = d.cycles + (d.page && ar.page_crossed); v = (d.mode == AM_IMP || d.mode == AM_ACC || d.mode == AM_REL) ? 0 : bus_read(b,ar.address); if (is_branch(d.op)) { @@ -163,7 +183,7 @@ cpu_step_result_t cpu_step(CPU6502 *c, Bus6502 *b) case O_CMP:compare(c,c->A,v);break; case O_CPX:compare(c,c->X,v);break; case O_CPY:compare(c,c->Y,v);break; case O_DEC:v--;bus_write(b,ar.address,v);set_nz(c,v);break; case O_DEX:c->X--;set_nz(c,c->X);break; case O_DEY:c->Y--;set_nz(c,c->Y);break; case O_EOR:c->A^=v;set_nz(c,c->A);break; case O_INC:v++;bus_write(b,ar.address,v);set_nz(c,v);break; case O_INX:c->X++;set_nz(c,c->X);break; case O_INY:c->Y++;set_nz(c,c->Y);break; - case O_JMP:c->PC=ar.address;break; case O_JSR:{uint16_t ret=c->PC-1;push(c,b,(uint8_t)(ret>>8));push(c,b,(uint8_t)ret);c->PC=ar.address;}break; + case O_JMP:c->PC=ar.address;break; case O_JSR:break; case O_LDA:c->A=v;set_nz(c,v);break; case O_LDX:c->X=v;set_nz(c,v);break; case O_LDY:c->Y=v;set_nz(c,v);break; case O_LSR:old=d.mode==AM_ACC?c->A:v;set_flag(c,FLAG_C,old&1);old>>=1;set_nz(c,old);if(d.mode==AM_ACC)c->A=old;else bus_write(b,ar.address,old);break; case O_NOP:break; case O_ORA:c->A|=v;set_nz(c,c->A);break; case O_PHA:push(c,b,c->A);break; case O_PHP:push(c,b,c->P|FLAG_B|FLAG_U);break; @@ -176,6 +196,18 @@ cpu_step_result_t cpu_step(CPU6502 *c, Bus6502 *b) case O_STA:bus_write(b,ar.address,c->A);break; case O_STX:bus_write(b,ar.address,c->X);break; case O_STY:bus_write(b,ar.address,c->Y);break; case O_TAX:c->X=c->A;set_nz(c,c->X);break; case O_TAY:c->Y=c->A;set_nz(c,c->Y);break; case O_TSX:c->X=c->SP;set_nz(c,c->X);break; case O_TXA:c->A=c->X;set_nz(c,c->A);break; case O_TXS:c->SP=c->X;break; case O_TYA:c->A=c->Y;set_nz(c,c->A);break; + case O_LAX:c->A=c->X=v;set_nz(c,v);break; + case O_SAX:bus_write(b,ar.address,c->A&c->X);break; + case O_DCP:v--;bus_write(b,ar.address,v);compare(c,c->A,v);break; + case O_ISC:v++;bus_write(b,ar.address,v);sbc(c,v);break; + case O_SLO:set_flag(c,FLAG_C,v&0x80);v<<=1;bus_write(b,ar.address,v);c->A|=v;set_nz(c,c->A);break; + case O_RLA:{bool nc=v&0x80;v=(uint8_t)((v<<1)|!!(c->P&FLAG_C));set_flag(c,FLAG_C,nc);bus_write(b,ar.address,v);c->A&=v;set_nz(c,c->A);}break; + case O_SRE:set_flag(c,FLAG_C,v&1);v>>=1;bus_write(b,ar.address,v);c->A^=v;set_nz(c,c->A);break; + case O_RRA:{bool nc=v&1;v=(uint8_t)((v>>1)|((c->P&FLAG_C)?0x80:0));set_flag(c,FLAG_C,nc);bus_write(b,ar.address,v);adc(c,v);}break; + case O_ANC:c->A&=v;set_nz(c,c->A);set_flag(c,FLAG_C,c->A&0x80);break; + case O_ALR:c->A&=v;set_flag(c,FLAG_C,c->A&1);c->A>>=1;set_nz(c,c->A);break; + case O_ARR:{uint8_t anded=c->A&v,result=(uint8_t)((anded>>1)|((c->P&FLAG_C)?0x80:0));set_nz(c,result);set_flag(c,FLAG_V,((result>>6)^(result>>5))&1);if((c->P&FLAG_D)&&c->decimal_enabled){if((anded&0x0F)+(anded&1)>5)result=(uint8_t)((result&0xF0)|((result+6)&0x0F));if((anded&0xF0)+(anded&0x10)>0x50){result+=0x60;set_flag(c,FLAG_C,true);}else set_flag(c,FLAG_C,false);c->A=result;}else{c->A=result;set_flag(c,FLAG_C,result&0x40);}}break; + case O_AXS:{uint8_t anded=c->A&c->X;set_flag(c,FLAG_C,anded>=v);c->X=(uint8_t)(anded-v);set_nz(c,c->X);}break; default:break; } c->P |= FLAG_U; diff --git a/src/state.c b/src/state.c new file mode 100644 index 0000000..c78cabd --- /dev/null +++ b/src/state.c @@ -0,0 +1,45 @@ +#include +#include +#include +#include +#include "state.h" + +enum { HEADER_SIZE=25, STATE_SIZE=HEADER_SIZE+MEM_SIZE+4 }; + +static uint32_t crc32(const uint8_t *data,size_t size) +{ + uint32_t crc=0xFFFFFFFFu; + for(size_t i=0;i>1)^((uint32_t)-(int32_t)(crc&1)&0xEDB88320u);} + return ~crc; +} + +static void put32(uint8_t *p,uint32_t v){p[0]=(uint8_t)v;p[1]=(uint8_t)(v>>8);p[2]=(uint8_t)(v>>16);p[3]=(uint8_t)(v>>24);} +static uint32_t get32(const uint8_t *p){return (uint32_t)p[0]|(uint32_t)p[1]<<8|(uint32_t)p[2]<<16|(uint32_t)p[3]<<24;} + +int state_save(const char *path,const CPU6502 *c,const Bus6502 *b) +{ + uint8_t *data=calloc(1,STATE_SIZE);FILE *f;size_t wrote; + if(!data)return ENOMEM; + memcpy(data,"MOS6502",7);data[8]=2;data[9]=c->A;data[10]=c->X;data[11]=c->Y; + data[12]=c->SP;data[13]=c->P;data[14]=(uint8_t)c->PC;data[15]=(uint8_t)(c->PC>>8); + data[16]=c->nmi_pending;data[17]=c->irq_asserted;data[18]=c->decimal_enabled; + data[19]=b->keyboard;data[20]=b->easy6502_io;put32(data+21,b->rng_state); + memcpy(data+HEADER_SIZE,b->memory.data,MEM_SIZE);put32(data+HEADER_SIZE+MEM_SIZE,crc32(data,HEADER_SIZE+MEM_SIZE)); + f=fopen(path,"wb");if(!f){int e=errno;free(data);return e;} + wrote=fwrite(data,1,STATE_SIZE,f);free(data);if(fclose(f)!=0||wrote!=STATE_SIZE)return EIO;return 0; +} + +int state_load(const char *path,CPU6502 *c,Bus6502 *b) +{ + uint8_t *data=malloc(STATE_SIZE);FILE *f;size_t got;int extra; + if(!data)return ENOMEM; + f=fopen(path,"rb"); + if(!f){int e=errno;free(data);return e;} + got=fread(data,1,STATE_SIZE,f);extra=fgetc(f);fclose(f); + if(got!=STATE_SIZE||extra!=EOF||memcmp(data,"MOS6502",7)||data[8]!=2|| + get32(data+HEADER_SIZE+MEM_SIZE)!=crc32(data,HEADER_SIZE+MEM_SIZE)){free(data);return EINVAL;} + c->A=data[9];c->X=data[10];c->Y=data[11];c->SP=data[12];c->P=data[13]; + c->PC=(uint16_t)data[14]|(uint16_t)data[15]<<8;c->nmi_pending=data[16]!=0;c->irq_asserted=data[17]!=0;c->decimal_enabled=data[18]!=0; + b->keyboard=data[19];b->easy6502_io=data[20]!=0;b->rng_state=get32(data+21); + memcpy(b->memory.data,data+HEADER_SIZE,MEM_SIZE);b->framebuffer_dirty=true;free(data);return 0; +} diff --git a/tests/roms/README.md b/tests/roms/README.md index 199f3c3..30752cf 100644 --- a/tests/roms/README.md +++ b/tests/roms/README.md @@ -6,3 +6,18 @@ The download is pinned by SHA-256 and is not committed here. `make test-functional` fetches and runs it. Success is the documented loop at `$3469`. + +The decimal image is Bruce Clark's public-domain test configured by Gopher2600 +for NMOS accumulator and N/V/Z/C flag validation. `make test-decimal` runs all +valid and invalid BCD operand combinations and requires `ERROR == 0`. + +`make test-interrupt` assembles Klaus Dormann's GPL-3.0 interrupt test with the +upstream bundled AS65 assembler and verifies IRQ, NMI, BRK, vectors, flags, and +concurrent interrupt priority through its success trap at `$06F5`. + +`make test-apple1` assembles the original Woz Monitor source maintained in +Jeff Tranter's 6502 repository, boots its `$FF00` reset vector, injects a memory +examine command through the emulated PIA, and verifies the monitor's output. + +`make test-nestest` downloads Kevtris's canonical NROM test and Nintendulator +golden log, then compares all 8,991 CPU states and cumulative cycle counts. diff --git a/tests/run_apple1.c b/tests/run_apple1.c new file mode 100644 index 0000000..0e02e2e --- /dev/null +++ b/tests/run_apple1.c @@ -0,0 +1,20 @@ +#include +#include +#include "apple1.h" +#include "cpu.h" +#include "loader.h" + +int main(int argc,char **argv) +{ + static const char command[]="FF00.FF07\r";size_t sent=0,loaded;Bus6502 bus;CPU6502 cpu;Apple1IO io; + if(argc!=2){fprintf(stderr,"usage: %s wozmon.bin\n",argv[0]);return 2;} + bus_init(&bus);apple1_init(&io,&bus); + if(load_binary(&bus,argv[1],0xFF00,&loaded)||loaded!=256)return 1; + cpu_reset(&cpu,&bus); + for(unsigned long i=0;i<2000000;i++){ + if(!io.key_ready&&sent +#include "bus.h" +#include "cpu.h" +#include "loader.h" + +int main(int argc,char **argv) +{ + Bus6502 bus;CPU6502 cpu;size_t loaded;const unsigned long limit=200000000; + if(argc!=2){fprintf(stderr,"usage: %s 6502_decimal_test.bin\n",argv[0]);return 2;} + bus_init(&bus); + if(load_binary(&bus,argv[1],0x0200,&loaded)){fprintf(stderr,"failed to load %s\n",argv[1]);return 1;} + bus_write16(&bus,0xFFFC,0x0200);cpu_reset(&cpu,&bus); + for(unsigned long i=0;i +#include "bus.h" +#include "cpu.h" +#include "loader.h" +#include "opcode.h" + +int main(int argc,char **argv) +{ + Bus6502 bus;CPU6502 cpu;size_t loaded;uint8_t previous=0;uint16_t history[32]={0}; + if(argc!=2){fprintf(stderr,"usage: %s 6502_interrupt_test.bin\n",argv[0]);return 2;} + bus_init(&bus); + if(load_binary(&bus,argv[1],0x000A,&loaded)){fprintf(stderr,"failed to load %s\n",argv[1]);return 1;} + bus.memory.data[0xBFFC]=0; + cpu.PC=0x0400;cpu.A=cpu.X=cpu.Y=0;cpu.SP=0xFD;cpu.P=FLAG_U|FLAG_I; + cpu.nmi_pending=false;cpu.irq_asserted=false; + for(unsigned long i=0;i<10000000;i++){ + uint16_t before=cpu.PC;history[i%32]=before;cpu_step_result_t r=cpu_step(&cpu,&bus); + uint8_t feedback=bus.memory.data[0xBFFC]&3; + cpu_set_irq(&cpu,(feedback&1)!=0); + if((feedback&2)&&!(previous&2))cpu_request_nmi(&cpu); + previous=feedback; + if(r.status!=CPU_STEP_OK){fprintf(stderr,"illegal $%02X at $%04X\n",r.opcode,before);return 1;} + if(cpu.PC==0x06F5){printf("interrupt success after %lu instructions (%zu-byte ROM)\n",i+1,loaded);return 0;} + if(cpu.PC==before){ + fprintf(stderr,"interrupt trap at $%04X after %lu instructions\n",before,i+1); + unsigned long first=i>31?i-31:0; + for(unsigned long n=first;n<=i;n++){char text[48];uint16_t pc=history[n%32];cpu_disassemble(&bus,pc,text,sizeof text);fprintf(stderr," %04X %s\n",pc,text);}return 1; + } + } + fprintf(stderr,"interrupt test limit reached at $%04X\n",cpu.PC);return 1; +} diff --git a/tests/run_nestest.c b/tests/run_nestest.c new file mode 100644 index 0000000..3c48943 --- /dev/null +++ b/tests/run_nestest.c @@ -0,0 +1,26 @@ +#include +#include +#include +#include "nes.h" + +static unsigned field(const char *line,const char *name) +{ const char *p=strstr(line,name);return p?(unsigned)strtoul(p+strlen(name),NULL,16):0xFFFFFFFFu; } + +int main(int argc,char **argv) +{ + NES nes;FILE *log;char line[256];unsigned long lines=0,cycles=7; + if(argc!=3){fprintf(stderr,"usage: %s nestest.nes nestest.log\n",argv[0]);return 2;} + if(nes_load(&nes,argv[1])){fprintf(stderr,"failed to load nestest ROM\n");return 1;} + log=fopen(argv[2],"r");if(!log){perror(argv[2]);return 1;} + nes_reset(&nes);nes.cpu.PC=0xC000;nes.cpu.A=nes.cpu.X=nes.cpu.Y=0;nes.cpu.P=FLAG_U|FLAG_I;nes.cpu.SP=0xFD; + while(fgets(line,sizeof line,log)){ + unsigned pc=(unsigned)strtoul(line,NULL,16),a=field(line,"A:"),x=field(line,"X:"),y=field(line,"Y:"),p=field(line,"P:"),sp=field(line,"SP:"); + const char *cy=strstr(line,"CYC:");unsigned long expected_cycles=cy?strtoul(cy+4,NULL,10):cycles; + if(nes.cpu.PC!=pc||nes.cpu.A!=a||nes.cpu.X!=x||nes.cpu.Y!=y||nes.cpu.P!=p||nes.cpu.SP!=sp||cycles!=expected_cycles){ + fprintf(stderr,"nestest mismatch line %lu\nexpected: %sactual: %04X A:%02X X:%02X Y:%02X P:%02X SP:%02X CYC:%lu\n",lines+1,line,nes.cpu.PC,nes.cpu.A,nes.cpu.X,nes.cpu.Y,nes.cpu.P,nes.cpu.SP,cycles);fclose(log);return 1; + } + cpu_step_result_t r=nes_step(&nes);if(r.status!=CPU_STEP_OK){fprintf(stderr,"illegal opcode $%02X on line %lu\n",r.opcode,lines+1);fclose(log);return 1;} + cycles+=r.cycles;lines++; + } + fclose(log);printf("nestest matched %lu instructions and cycle counts\n",lines);return lines==8991?0:1; +} diff --git a/tests/run_tests.c b/tests/run_tests.c index 6acead6..223f64c 100644 --- a/tests/run_tests.c +++ b/tests/run_tests.c @@ -1,7 +1,10 @@ #include #include #include "bus.h" +#include "apple1.h" #include "cpu.h" +#include "state.h" +#include "nes.h" static int tests, failures; #define CHECK(expr) do { tests++; if (!(expr)) { failures++; fprintf(stderr,"%s:%d: check failed: %s\n",__FILE__,__LINE__,#expr); } } while(0) @@ -68,9 +71,95 @@ static void test_branches_and_illegal(void) b.memory.data[0x8100]=0x02;cpu_step_result_t r=step(&c,&b);CHECK(r.status==CPU_STEP_ILLEGAL_OPCODE);EQ8(r.opcode,0x02); } +static void test_stable_undocumented(void) +{ + CPU6502 c;Bus6502 b;setup(&c,&b,0x8000); + const uint8_t p[]={0xA9,0xF0,0xA2,0xCC,0x87,0x10,0xA7,0x10,0xC7,0x10, + 0xE7,0x10,0x0B,0x80,0x4B,0x0F,0xCB,0x01,0x1A}; + memcpy(&b.memory.data[0x8000],p,sizeof p); + step(&c,&b);step(&c,&b);step(&c,&b);EQ8(b.memory.data[0x10],0xC0); + step(&c,&b);EQ8(c.A,0xC0);EQ8(c.X,0xC0); + step(&c,&b);EQ8(b.memory.data[0x10],0xBF);CHECK(c.P&FLAG_C); + c.P|=FLAG_C;step(&c,&b);EQ8(b.memory.data[0x10],0xC0);EQ8(c.A,0x00);CHECK(c.P&FLAG_C); + step(&c,&b);EQ8(c.A,0);CHECK(!(c.P&FLAG_C)); + c.A=0xFF;step(&c,&b);EQ8(c.A,7);CHECK(c.P&FLAG_C); + c.A=0xFF;c.X=0x0F;step(&c,&b);EQ8(c.X,0x0E);CHECK(c.P&FLAG_C); + EQ8(step(&c,&b).cycles,2); +} + +static void test_portable_state(void) +{ + const char *path="/tmp/mos6502-test.state";CPU6502 c,restored;Bus6502 b,out; + setup(&c,&b,0x8123);c.A=0x42;c.X=0x77;c.P=FLAG_U|FLAG_C;b.memory.data[0x2345]=0xAB; + b.keyboard='d';b.easy6502_io=true;bus_seed_rng(&b,12345); + CHECK(state_save(path,&c,&b)==0);bus_init(&out);memset(&restored,0,sizeof restored); + CHECK(state_load(path,&restored,&out)==0);EQ16(restored.PC,0x8123);EQ8(restored.A,0x42); + EQ8(restored.X,0x77);EQ8(restored.P,FLAG_U|FLAG_C);EQ8(out.memory.data[0x2345],0xAB); + EQ8(out.keyboard,'d');CHECK(out.easy6502_io);CHECK(out.rng_state==12345);CHECK(out.framebuffer_dirty); + CHECK(restored.decimal_enabled); + remove(path); +} + +static void test_2a03_ignores_decimal(void) +{ + CPU6502 c;Bus6502 b;setup(&c,&b,0x8000);c.decimal_enabled=false; + const uint8_t p[]={0xF8,0x18,0xA9,0x49,0x69,0x51};memcpy(&b.memory.data[0x8000],p,sizeof p); + step(&c,&b);step(&c,&b);step(&c,&b);step(&c,&b);EQ8(c.A,0x9A);CHECK(!(c.P&FLAG_C)); +} + +static void test_nes_nrom_bus(void) +{ + const char *path="/tmp/mos6502-test.nes";uint8_t header[16]={ 'N','E','S',0x1A,1,1 }; + uint8_t prg[16384]={0},chr[8192]={0};FILE *f=fopen(path,"wb");NES n; + prg[0]=0xF8;prg[1]=0x18;prg[2]=0xA9;prg[3]=0x49;prg[4]=0x69;prg[5]=0x51; + prg[0x3FFC]=0x00;prg[0x3FFD]=0x80;fwrite(header,1,sizeof header,f);fwrite(prg,1,sizeof prg,f);fwrite(chr,1,sizeof chr,f);fclose(f); + CHECK(nes_load(&n,path)==0);nes_reset(&n);EQ16(n.cpu.PC,0x8000);CHECK(!n.cpu.decimal_enabled); + bus_write(&n.bus,0x0001,0xAA);EQ8(bus_read(&n.bus,0x0801),0xAA);EQ8(bus_read(&n.bus,0xC000),0xF8); + nes_step(&n);nes_step(&n);nes_step(&n);nes_step(&n);EQ8(n.cpu.A,0x9A); + nes_set_controller(&n,0x05);bus_write(&n.bus,0x4016,1);bus_write(&n.bus,0x4016,0); + EQ8(bus_read(&n.bus,0x4016)&1,1);EQ8(bus_read(&n.bus,0x4016)&1,0);EQ8(bus_read(&n.bus,0x4016)&1,1); + remove(path); +} + +static void write_test_rom(const char *path,uint8_t mapper,uint8_t prg_banks,uint8_t chr_banks) +{ + uint8_t h[16]={'N','E','S',0x1A,prg_banks,chr_banks,(uint8_t)(mapper<<4),0};FILE *f=fopen(path,"wb"); + fwrite(h,1,sizeof h,f); + for(unsigned b=0;bpeak)peak=s;}CHECK(peak>0.1f); + remove(u);remove(c); +} + +static void test_nes_ppu_rendering(void) +{ + NES n;uint32_t pixels[256*240];memset(&n,0,sizeof n);n.chr_size=8192;n.chr_ram=true;n.ppu_mask=0x18; + n.palette[0]=0;n.palette[1]=1;n.palette[0x11]=2;n.chr[0]=0x80;n.chr[16]=0x80; + memset(n.oam,0xFF,sizeof n.oam);n.oam[0]=7;n.oam[1]=1;n.oam[2]=0;n.oam[3]=8; + nes_render_frame(&n,pixels);CHECK(n.ppu_status&0x40);CHECK(!(n.ppu_status&0x20));CHECK(pixels[8*256+8]!=pixels[8*256+9]); + for(unsigned s=0;s<9;s++){n.oam[s*4]=7;n.oam[s*4+3]=(uint8_t)(s*8+8);}nes_render_frame(&n,pixels);CHECK(n.ppu_status&0x20); +} + +static void test_bus_hooks_and_apple1(void) +{ + Bus6502 b;Apple1IO io;bus_init(&b);apple1_init(&io,&b);apple1_key(&io,'a'); + EQ8(bus_read(&b,0xD011),0x80);EQ8(bus_read(&b,0xD010),'A'|0x80);EQ8(bus_read(&b,0xD011),0); + bus_write(&b,0x1234,0x56);EQ8(bus_read(&b,0x1234),0x56); +} + int main(void) { test_reset_and_bus();test_load_store_and_modes();test_indirect_and_control(); - test_alu_and_flags();test_stack_interrupts();test_branches_and_illegal(); + test_alu_and_flags();test_stack_interrupts();test_branches_and_illegal();test_stable_undocumented();test_portable_state();test_bus_hooks_and_apple1();test_2a03_ignores_decimal();test_nes_nrom_bus();test_nes_mappers_and_apu();test_nes_ppu_rendering(); printf("%d checks, %d failures\n",tests,failures);return failures?1:0; } diff --git a/tests/run_vectors.c b/tests/run_vectors.c new file mode 100644 index 0000000..2a8d4e6 --- /dev/null +++ b/tests/run_vectors.c @@ -0,0 +1,30 @@ +#include +#include +#include +#include "bus.h" +#include "cpu.h" + +static int byte(void){return fgetc(stdin);} +static int word(uint16_t *v){int a=byte(),b=byte();if(a==EOF||b==EOF)return 0;*v=(uint16_t)(a|(b<<8));return 1;} +static int regs(CPU6502 *c) +{ + int s,a,x,y,p;if(!word(&c->PC))return 0;s=byte();a=byte();x=byte();y=byte();p=byte();if(p==EOF)return 0; + c->SP=(uint8_t)s;c->A=(uint8_t)a;c->X=(uint8_t)x;c->Y=(uint8_t)y;c->P=(uint8_t)p;c->decimal_enabled=true;return 1; +} +static int pairs(Bus6502 *b,uint16_t *addresses,uint8_t *values) +{ + uint16_t count;if(!word(&count))return -1;for(uint16_t i=0;imemory.data[a]=(uint8_t)v;if(addresses){addresses[i]=a;values[i]=(uint8_t)v;}}return count; +} +int main(void) +{ + unsigned long tests=0;int failures=0; + for(;;){CPU6502 c,want;Bus6502 b;uint16_t addresses[64];uint8_t values[64];int count,cycles; + bus_init(&b);memset(&c,0,sizeof c);memset(&want,0,sizeof want);if(!regs(&c))break; + if(pairs(&b,NULL,NULL)<0||!regs(&want)||(count=pairs(NULL,addresses,values))<0||(cycles=byte())==EOF)return 2; + cpu_step_result_t r=cpu_step(&c,&b);tests++; + int bad=r.status!=CPU_STEP_OK||r.cycles!=(uint8_t)cycles||c.PC!=want.PC||c.SP!=want.SP||c.A!=want.A||c.X!=want.X||c.Y!=want.Y||c.P!=want.P; + for(int i=0;i64:raise ValueError("vector has more than 64 final RAM entries") + runner.stdin.write(emit(initial,initial["ram"]));runner.stdin.write(emit(final,final["ram"]));runner.stdin.write(bytes([len(case["cycles"])])) + runner.stdin.close();return runner.wait() +if __name__=="__main__":raise SystemExit(main())