From 8423bd9a308c268a66df160335739b39531d4a45 Mon Sep 17 00:00:00 2001 From: Daniel Prilik Date: Tue, 3 Jul 2018 21:23:34 -0700 Subject: [PATCH] cleanup I'd like to think this commit marks the end of the first phase of ANESE's development. It's been a long road, but ANESE is finally at a point where I'm mostly happy with it. ANESE sure as hell isn't perfect, but hey, it's pretty good! The core code is pretty clean, the UI code is... acceptable, but most importantly, ANESE actually plays my favorite NES games! There is still some work to be done before I'd be comfortable giving ANESE a v1.0.0 release, but what I have here is still pretty great. Let's call this ANESE v0.9.0 :) ------- So, what next? Well, contrary to what I said in some earlier commits, I think i'll continue to work on ANESE a little bit more! Specifically, i'd like to rewrite the 6502 emulation. My current implementation is... okay. It's instruction-level cycle accurate, but I don't think that's good enough. It really should be sub-instruction level accurate. Odds are the added accuracy will fix _a lot_ of bugs. Aside from accuracy though, I have another reason to do a rewrite... As a Waterloo student, I have to do a Work Term Report on some technical project i've worked on recently. ANESE is one such project. Since the report isn't designed to be very long, I'd limit my scope to just a small aspect of ANESE: the 6502 emulator. Yes, I could just write the report on how I arrived at my current implementation, but I think it would be cool to attempt a cleaner rewrite, and compare and contrast the two versions. So yeah, stay tuned! I might also post the writeup (once I get around to it) --- README.md | 72 ++++++++++-------- roms/README.md | 5 +- scripts/README.md | 3 + cppcheck.sh => scripts/cppcheck.sh | 0 mk_msvc.ps1 => scripts/mk_msvc.ps1 | 0 nestest.sh => scripts/nestest.sh | 0 src/common/serializable.cc | 94 ++++------------------- src/common/serializable.h | 105 +++++++++++++------------- src/nes/apu/filters.h | 2 + src/nes/cartridge/cartridge.h | 18 ++--- src/nes/cartridge/mapper.cc | 10 +-- src/nes/cartridge/mapper.h | 63 +++++++++------- src/nes/cartridge/parse_rom.cc | 95 ++++++++++++----------- src/nes/cartridge/parse_rom.h | 8 +- src/nes/cpu/cpu.cc | 67 +++++++++++------ src/nes/cpu/cpu.h | 24 +++--- src/nes/cpu/instructions.h | 7 -- src/nes/cpu/nestest.cc | 112 ++++++++++++++-------------- src/nes/interfaces/memory.h | 30 +------- src/nes/joy/controllers/standard.cc | 4 +- src/nes/joy/controllers/standard.h | 3 +- src/nes/joy/controllers/zapper.cc | 4 +- src/nes/joy/joy.cc | 19 ++--- src/nes/joy/joy.h | 8 +- src/nes/nes.cc | 29 ++++--- src/nes/nes.h | 10 +-- src/ui/SDL2/fs/load.cc | 2 +- src/ui/SDL2/gui_modules/emu.cc | 14 ++-- 28 files changed, 382 insertions(+), 426 deletions(-) create mode 100644 scripts/README.md rename cppcheck.sh => scripts/cppcheck.sh (100%) rename mk_msvc.ps1 => scripts/mk_msvc.ps1 (100%) rename nestest.sh => scripts/nestest.sh (100%) diff --git a/README.md b/README.md index fb6082e..029df14 100644 --- a/README.md +++ b/README.md @@ -13,20 +13,20 @@

**ANESE** (**A**nother **NES** **E**mulator) is a Nintendo Entertainment System -Emulator being written for fun and learning. +Emulator written for fun and learning. -While accuracy and performance are long-term goals, ANESE's primary focus is -getting some of the more popular titles up and running. Most basic Mappers have +While accuracy and performance are long-term goals, ANESE's primary focus is to +get some of the more popular titles up and running. Most basic Mappers have been implemented, so many popular titles should be working! :smile: -ANESE is built with _cross-platform_ in mind, and is regularly built on all +ANESE is built with _cross-platform_ in mind, and is regularly tested on all major platforms (macOS, Windows, and Linux). ANESE doesn't use any vendor-specific language extensions, and is compiled with strict compiler flags. It is also linted (fairly) regularly. Lastly, ANESE strives to keep a clean and _interesting_ C++11 codebase, emphasizing _readability_, _maintainability_, and _approachability_. The code -is heavily commented, providing sources and insights for much of the logic. +is well commented, providing sources and insights for much of the logic. ## Downloads @@ -87,12 +87,13 @@ If you're interested in looking under the hood of the PPU, you can pass the ## Running -ANESE can run from the shell using `anese [rom.nes]` syntax. +Running ANESE with no arguments throws you into a directory-browser, from which +you can navigate to your ROM and launch it. -If no ROM is provided, a simple dialog window pops-up prompting the user to -select a valid NES rom. - -For a full list of switches, run `anese -h` +Alternatively, ANESE can run from the shell using `anese [rom.nes]` syntax. +Certain features are only accessible from the command-line at the moment (e.g: +movie recording / playback, PPU timing hacks). For a full list of switches, +run `anese -h` **Windows Users:** make sure the executable can find `SDL2.dll`! Download the runtime DLLs from the SDL website, and plop them in the same directory as @@ -163,17 +164,28 @@ that rely on sub-instruction level timings (eg: Solomon's Key). ## TODO -This is a list of things I would like to try to accomplish, with those closer to -the top higher on my priority list: +These are features that will add major value to ANESE: + +- [ ] _Implement_: Cycle accurate CPU (will probably fix _many_ bugs) +- [ ] _Implement_: Better menu (not just fs, also config) +- [ ] _CMake_: more robust macOS bundles (good way to get SDL2.0 packaged?) +- [ ] _Implement_: LibRetro Core +- [ ] _Implement_: Get the Light-gun working +- [ ] _Debugging_: Add debug GUI + - All objects implementing the Memory interface _must also_ implement `peek`, + i.e: a `const` read. As such, a debugger could easily inspect any/all memory + locations with no side effects! + +Here's a couple that have been crossed off already: - [x] _Implement_: My own APU (don't use Blarrg's) -- [ ] _Implement_: More robust menu system - [x] _Refactor_: Modularize `main.cc` - push everything into `src/ui/` - [x] _Refactor_: Split `gui.cc` into more files! -- [ ] _CMake_: Make building macOS bundles less brittle - [x] _Refactor_: Push common mapper behavior to Base Mapper (eg: bank chunking) -- [ ] _Implement_: LibRetro Core -- [ ] _Implement_: Sub-instruction cycle accurate CPU + +And here are some ongoing low-priority goals: + +- [ ] _Refactor_: Roll-my-own Sound_Queue (SDL_QueueAudio?) - [ ] _Cleanup_: Unify naming conventions (either camelCase or snake_case) - [ ] _Cleanup_: Comment the codebase _even more_ - [ ] _Security_: Actually bounds-check files lol @@ -221,7 +233,7 @@ the top higher on my priority list: - [x] DMC DMA - [ ] Joypads - [x] Basic Controller - - [ ] Zapper - _needs work_ + - [ ] Zapper - _still needs work_ - [ ] NES Four Score ### Secondary Milestones @@ -234,13 +246,13 @@ the top higher on my priority list: - [x] Battery Backed RAM - Saves to `.sav` - [x] Save-states - [ ] Dump to file -- [ ] Config File +- [x] Config File - [x] Preserve ROM path - [x] Window size - [ ] Controls - [x] Running NESTEST (behind a flag) - [x] Controller support - _currently very basic_ -- [ ] A SDL GUI +- [x] A SDL GUI - [x] SDL-based ROM picker - [ ] Options menu @@ -257,13 +269,9 @@ the top higher on my priority list: - [x] SDL Standalone - [ ] LibRetro - [ ] Debugger! - - There is a lot of great infrastructure in place that could make ANESE a - top-tier NES debugger, primarily the fact that all memory-interfaced - objects _must_ implement `peek`, which enables non-destructive looks at - arbitrary objects! - [ ] CPU - [ ] Step through instructions - - [ ] PPU Views + - [x] PPU Views - [x] Static Palette - [x] Palette Memory - [x] Pattern Tables @@ -273,25 +281,27 @@ the top higher on my priority list: ### Accuracy & Compatibility - More Mappers! Always more mappers! +- [ ] Add automatic testing + - [ ] Screenshots: compare power-on with 30 seconds of button mashing + - [ ] Test ROMs: Parse debug outputs - CPU - [ ] Implement Unofficial Opcodes - - [ ] Pass More Tests yo + - [ ] Pass More Tests - [ ] _\(Stretch\)_ Switch to sub-instruction level cycle-based emulation (vs instruction level) - PPU - [x] Make the sprite rendering pipeline more accurate (fetch-timings) - - This _should_ fix _Punch Out!!_ **UPDATE:** it totally did. - - [ ] Pass More Tests yo - - [ ] Make value in PPU <-> CPU bus decay + - [ ] Pass More Tests + - [ ] Make value in PPU <-> CPU bus decay? ## Attributions - A big shout-out to [LaiNES](https://github.com/AndreaOrru/LaiNES) and [fogleman/nes](https://github.com/fogleman/nes), two solid NES emulators that I referenced while implementing some particularly tricky parts of the PPU). While -I actively avoided looking at the source codes of other NES emulators as I wrote -my initial implementations of the CPU and PPU, I did occationally sneak a peek -at how others did things when I got very stuck. +I actively avoided looking at the source codes of other NES emulators while +writing my initial implementations of the CPU and PPU, I did sneak a peek at how +others solved some problems once I got stuck. - These awesome libraries make ANESE a lot nicer to use: - [sdl2](https://www.libsdl.org/) - A/V and Input - [SDL_inprint](https://github.com/driedfruit/SDL_inprint/) - SDL fonts, without SDL_ttf diff --git a/roms/README.md b/roms/README.md index 71f2240..55072b9 100644 --- a/roms/README.md +++ b/roms/README.md @@ -1,4 +1,5 @@ -All these test roms are sourced from http://wiki.nesdev.com/w/index.php/Emulator_tests -and are subject to the terms of usage laid out by their respective authors. +The roms under `tests` are primarily sourced from +http://wiki.nesdev.com/w/index.php/Emulator_tests +and are subject to the licenses they were initially distributed under. They are mirrored in this repository for personal convenience. diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..e623dfa --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,3 @@ +These scripts should be run from the root directory. + +eg: `sh scripts/cppcheck.sh` diff --git a/cppcheck.sh b/scripts/cppcheck.sh similarity index 100% rename from cppcheck.sh rename to scripts/cppcheck.sh diff --git a/mk_msvc.ps1 b/scripts/mk_msvc.ps1 similarity index 100% rename from mk_msvc.ps1 rename to scripts/mk_msvc.ps1 diff --git a/nestest.sh b/scripts/nestest.sh similarity index 100% rename from nestest.sh rename to scripts/nestest.sh diff --git a/src/common/serializable.cc b/src/common/serializable.cc index 87dd4cd..0f2ce1c 100644 --- a/src/common/serializable.cc +++ b/src/common/serializable.cc @@ -117,25 +117,25 @@ Serializable::Chunk* Serializable::serialize() const { : (field.type == 2 ? *field.len_variable : field.len_fixed) ); switch (field.type) { - case _field_type::INVALID: assert(false); break; - case _field_type::POD: + case _field_type::SERIAL_INVALID: assert(false); break; + case _field_type::SERIAL_POD: fprintf(stderr, "0x%08X\n", *((uint*)field.thing)); next = new Chunk(field.thing, field.len_fixed); break; - case _field_type::ARRAY_VARIABLE: + case _field_type::SERIAL_ARRAY_VARIABLE: fprintf(stderr, "0x%08X\n", **((uint**)field.thing)); next = new Chunk(*((void**)field.thing), *field.len_variable); break; - case _field_type::SERIALIZABLE: + case _field_type::SERIAL_IZABLE: fprintf(stderr, "serializable: \n"); next = ((Serializable*)field.thing)->serialize(); assert(next != nullptr); break; - case _field_type::SERIALIZABLE_PTR: { + case _field_type::SERIAL_IZABLE_PTR: { fprintf(stderr, "serializable_ptr: "); if (!field.thing) { fprintf(stderr, "null\n"); - next = new Chunk(); // nullchunk == tried to serialize null serializable + next = new Chunk(); // nullchunk. } else { fprintf(stderr, "recursive\n"); next = ((Serializable*)field.thing)->serialize(); @@ -151,8 +151,8 @@ Serializable::Chunk* Serializable::serialize() const { tail = next; } - if (field.type == _field_type::SERIALIZABLE || - field.type == _field_type::SERIALIZABLE_PTR) { + if (field.type == _field_type::SERIAL_IZABLE || + field.type == _field_type::SERIAL_IZABLE_PTR) { while (tail->next) tail = tail->next; } } @@ -180,38 +180,32 @@ const Serializable::Chunk* Serializable::deserialize(const Chunk* c) { : (field.type == 2 ? *field.len_variable : field.len_fixed) ); - if (c->len != field.len_fixed && (field.type == _field_type::ARRAY_VARIABLE && c->len != *field.len_variable)) { - fprintf(stderr, "[Deserialization] Field length mismatch! " - "This is probably caused by a change in serialization order.\n"); - assert(false); - } - switch (field.type) { - case _field_type::INVALID: assert(false); break; - case _field_type::POD: + case _field_type::SERIAL_INVALID: assert(false); break; + case _field_type::SERIAL_POD: fprintf(stderr, "0x%08X\n", *((uint*)c->data)); memcpy(field.thing, c->data, c->len); c = c->next; break; - case _field_type::ARRAY_VARIABLE: + case _field_type::SERIAL_ARRAY_VARIABLE: fprintf(stderr, "0x%08X\n", *((uint*)c->data)); memcpy(*((void**)field.thing), c->data, c->len); c = c->next; break; - case _field_type::SERIALIZABLE: + case _field_type::SERIAL_IZABLE: fprintf(stderr, "serializable: \n"); // recursively deserialize the data c = ((Serializable*)field.thing)->deserialize(c); break; - case _field_type::SERIALIZABLE_PTR: { + case _field_type::SERIAL_IZABLE_PTR: { fprintf(stderr, "serializable_ptr: "); if (c->len == 0 && field.thing == nullptr) { fprintf(stderr, "null\n"); - // nullchunk == this serializable was null, so the thing must be null + // nullchunk. Ignore this and carry on. c = c->next; } else { - // recursively deserialize the data fprintf(stderr, "recursive\n"); + // recursively deserialize the data c = ((Serializable*)field.thing)->deserialize(c); } } break; @@ -222,61 +216,3 @@ const Serializable::Chunk* Serializable::deserialize(const Chunk* c) { delete field_data; return c; } - -// // "testing" -// struct test_struct_base : public Serializable { -// uint balls; -// }; - -// struct test_struct : test_struct_base { -// uint val; -// test_struct* next; -// uint val2; - -// u8* varlen_array; - -// SERIALIZE_START(4, "test") -// SERIALIZE_SERIALIZABLE_PTR(this->next) -// SERIALIZE_POD(this->val) -// SERIALIZE_POD(this->val2) -// SERIALIZE_ARRAY_VARIABLE(this->varlen_array, this->val) -// SERIALIZE_END(4) - -// test_struct(uint val) { -// this->val = val; -// this->val2 = val << 16; -// this->next = nullptr; -// this->varlen_array = new u8 [val]; -// for (uint i = 0; i < val; i++) { -// this->varlen_array[i] = (i + 1) * 2; -// this->varlen_array[i] |= this->varlen_array[i] << 4; -// } -// } - -// void add(test_struct* next) { this->next = next; } -// }; - -// static auto test = [](){ -// test_struct* basicboi = new test_struct(1); -// basicboi->add(new test_struct(2)); -// basicboi->next->add(new test_struct(3)); - -// const u8* data; -// uint len; -// Serializable::Chunk* base_state = ((test_struct_base*)basicboi)->serialize(); -// base_state->debugprint(); -// base_state->collate(data, len); - -// basicboi->val = 100; -// basicboi->next->val = 200; -// basicboi->next->next->val = 300; - -// const Serializable::Chunk* too = Serializable::Chunk::parse(data, len); -// too->debugprint(); - -// ((test_struct_base*)basicboi)->deserialize(Serializable::Chunk::parse(data, len)); - -// assert(basicboi->val == 1 && basicboi->next->val == 2 && basicboi->next->next->val == 3); - -// return 1; -// }(); diff --git a/src/common/serializable.h b/src/common/serializable.h index 527e317..2c4591d 100644 --- a/src/common/serializable.h +++ b/src/common/serializable.h @@ -7,8 +7,8 @@ #include // A DIY data-serialization system. -// Incredibly hacky, super brittle, and a true abuse of C++'s typesystem. -// Beautiful. +// Hacky. Brittle. Beautiful. +// A true abuse of C/C++'s typesystem (or lack thereof) // // Caveats: // -------- @@ -19,16 +19,16 @@ // or else deserialization will fail catastrophically // 3) Circular dependencies break everything (i.e: bad things will happen when // serialize() gets called on A, thus calling serialize() on member B, who -// holds a referance to A... You'll blow the stack, and that's bad) +// holds a reference to A... You'll blow the stack, and that's bad) // 4) `Serializable` inheritence is not handled _nicely_, with one having to // manually override de/serialize functions with calls to the parent funcs, // performing the chunk-manipulation manually. // // Dangerous Shenanigans that should be AVOIDED // -------------------------------------------- -// - Calling de/serialize in the constructor/destructor can be dangeous, as some -// fields (notably SERIALIZE_ARRAY_VARIABLE and SERIALIZE_SERIALIZABLE(_PTR)) -// may not be instatiated yet. +// - Calling de/serialize in the constructor/destructor can be dangerous, as +// some fields (notably SERIALIZE_ARRAY_VARIABLE and +// SERIALIZE_SERIALIZABLE_PTR) may not have been instantiated yet. // Tread carefully. // // Basic Usage: @@ -59,8 +59,8 @@ // // 2) define the serialization structure in the structure's definition // ``` -// my_thing() { -// // a POD type of fixed ssize +// struct my_thing : public Serializable { +// // a POD type of fixed size // uint some_flags; // ... // ... @@ -91,15 +91,15 @@ // Advanced: // --------- // - If a class need to preform some actions post / pre de/serialize, you can -// override the serilization methods and add custom behavior. +// override the serialization methods and add custom behavior. // - **Be Careful:** when overriding de/serialize functions, make sure to // call the parent class's de/serialize functions, or else the class will // not properly serialize! // - If you inherit from an object that is already Serializable, make sure that // you add the SERIALIZE_PARENT(BaseClass) macro before SERIALIZE_START. // This tells Serializable to also serialize whatever state the parent had -// - _Caveat:_ There is no support for multiple inheritence at the moment -// - Since everything is just regular ol C++ under the hood, you can implement +// - _Caveat:_ There is no support for multiple inheritance at the moment +// - Since everything is just regular 'ol C++ under the hood, you can implement // custom serialization routines (outside of the predefined macros). When // doing so, express intent by wrapping the custom implementation in a // SERIALIZE_CUSTOM() {} block @@ -110,8 +110,8 @@ class Serializable { public: // Chunks are the base units of serialization. - // They are functionally singly-linked lists, where each node stores a pointer - // to an array of bytes of a certain length. + // Functionally, they are simple singly-linked lists, with each node holding + // a chunk of data, it's length, and a reference to the next link in the chain struct Chunk { uint len; u8* data; @@ -138,11 +138,11 @@ public: /*------------------------------ Macro Support -----------------------------*/ protected: enum _field_type { - INVALID = 0, - POD, - ARRAY_VARIABLE, - SERIALIZABLE, - SERIALIZABLE_PTR + SERIAL_INVALID = 0, + SERIAL_POD, + SERIAL_ARRAY_VARIABLE, + SERIAL_IZABLE, + SERIAL_IZABLE_PTR }; struct _field_data { @@ -150,7 +150,7 @@ protected: const char* label; _field_type type; void* thing; - // only one of the two is used + // only one of the following are used uint len_fixed; uint* len_variable; }; @@ -160,15 +160,18 @@ protected: // this is a bit of a hacky workaround to the case where a base-class is // marked serializable, but none of it's children, or itself, provide any // serializable state... - // this is bad since it uses a static "dump" to read/write dummy data, and - // it is hella-not guaranteed to be consistent + // this is bad since it uses a static "dump" chunk of memory to read/write + // dummy data, and that means the serialized state probably won't be + // consistent :/ fprintf(stderr, "[Serializable] WARNING: " "Calling base _get_serializable_state. " "This may result in different dumps for identical state!\n"); static uint dump = 0; static const _field_data nothing [1] = {{ - "", "", _field_type::POD, &dump, sizeof dump, nullptr + "", "", + _field_type::SERIAL_POD, &dump, + sizeof dump, nullptr }}; data = nothing; @@ -232,44 +235,44 @@ protected: /*----------------------- Serialize Datatypes Macros -----------------------*/ #ifdef _WIN32 - const char slash = '\\'; + static constexpr char slash = '\\'; #else - const char slash = '/'; + static constexpr char slash = '/'; #endif -#define SERIALIZE_POD(thing) \ - new_state[i++] = { \ - chunk_label, strrchr(__FILE__ ": " #thing, slash) + 1, \ - Serializable::_field_type::POD, \ - (void*)&(thing), \ - sizeof(thing), 0 \ +#define SERIALIZE_POD(thing) \ + new_state[i++] = { \ + chunk_label, strrchr(__FILE__ ": " #thing, slash) + 1, \ + Serializable::_field_type::SERIAL_POD, \ + (void*)&(thing), \ + sizeof(thing), 0 \ }; -#define SERIALIZE_ARRAY_VARIABLE(thing, len) \ - new_state[i++] = { \ - chunk_label, strrchr(__FILE__ ": " #thing, slash) + 1, \ - Serializable::_field_type::ARRAY_VARIABLE, \ - (void*)&(thing), \ - 0, (uint*)&len \ +#define SERIALIZE_ARRAY_VARIABLE(thing, len) \ + new_state[i++] = { \ + chunk_label, strrchr(__FILE__ ": " #thing, slash) + 1, \ + Serializable::_field_type::SERIAL_ARRAY_VARIABLE, \ + (void*)&(thing), \ + 0, (uint*)&len \ }; -#define SERIALIZE_SERIALIZABLE(item) \ - new_state[i++] = { \ - chunk_label, strrchr(__FILE__ ": " #item, slash) + 1, \ - Serializable::_field_type::SERIALIZABLE, \ - (void*)static_cast(&(item)), \ - 0, 0 \ - }; \ - if (new_state[i-1].thing == nullptr) \ - fprintf(stderr, "[Serializable][%s] Warning: could not cast `" #item \ +#define SERIALIZE_SERIALIZABLE(item) \ + new_state[i++] = { \ + chunk_label, strrchr(__FILE__ ": " #item, slash) + 1, \ + Serializable::_field_type::SERIAL_IZABLE, \ + (void*)static_cast(&(item)), \ + 0, 0 \ + }; \ + if (new_state[i-1].thing == nullptr) \ + fprintf(stderr, "[Serializable][%s] Warning: could not cast `" #item \ "` down to Serializable!\n", chunk_label); -#define SERIALIZE_SERIALIZABLE_PTR(thingptr) \ - new_state[i++] = { \ - chunk_label, strrchr(__FILE__ ": " #thingptr, slash) + 1, \ - Serializable::_field_type::SERIALIZABLE_PTR, \ - (void*)static_cast(thingptr), \ - 0, 0 \ +#define SERIALIZE_SERIALIZABLE_PTR(thingptr) \ + new_state[i++] = { \ + chunk_label, strrchr(__FILE__ ": " #thingptr, slash) + 1, \ + Serializable::_field_type::SERIAL_IZABLE_PTR, \ + (void*)static_cast(thingptr), \ + 0, 0 \ }; #define SERIALIZE_CUSTOM() // doesn't do anything except express intent diff --git a/src/nes/apu/filters.h b/src/nes/apu/filters.h index 49881fe..d76c292 100644 --- a/src/nes/apu/filters.h +++ b/src/nes/apu/filters.h @@ -30,6 +30,7 @@ public: LoPassFilter(hertz f, hertz sample_rate) : FirstOrderFilter(f, sample_rate) , a { this->dt / (this->RC + this->dt) } + , prev { 0, 0 } {} double process(double x) override { @@ -49,6 +50,7 @@ public: HiPassFilter(hertz f, hertz sample_rate) : FirstOrderFilter(f, sample_rate) , a { this->RC / (this->RC + this->dt) } + , prev { 0, 0 } {} double process(double x) override { diff --git a/src/nes/cartridge/cartridge.h b/src/nes/cartridge/cartridge.h index 4a14c58..e269f19 100644 --- a/src/nes/cartridge/cartridge.h +++ b/src/nes/cartridge/cartridge.h @@ -3,9 +3,9 @@ #include "mapper.h" #include "rom_file.h" -#undef NO_ERROR // TODO: track down nebulous Windows.h include that defines this - -// A thin class that owns a ROM_File and it's associated mapper +// Container for ROM_File* and associated mapper. +// Takes ownership of any ROM_File passed to it, and handles Mapper detection +// and construction class Cartridge { private: const ROM_File* const rom_file; @@ -23,15 +23,15 @@ public: {} enum class Status { - NO_ERROR = 0, - BAD_MAPPER, - BAD_DATA + CART_NO_ERROR = 0, + CART_BAD_MAPPER, + CART_BAD_DATA }; Status status() { - if (this->rom_file == nullptr) return Status::BAD_DATA; - if (this->mapper == nullptr) return Status::BAD_MAPPER; - return Status::NO_ERROR; + if (this->rom_file == nullptr) return Status::CART_BAD_DATA; + if (this->mapper == nullptr) return Status::CART_BAD_MAPPER; + return Status::CART_NO_ERROR; } const ROM_File* get_rom_file() { return this->rom_file; } diff --git a/src/nes/cartridge/mapper.cc b/src/nes/cartridge/mapper.cc index a9eaf31..c174f7d 100644 --- a/src/nes/cartridge/mapper.cc +++ b/src/nes/cartridge/mapper.cc @@ -2,7 +2,7 @@ /*-------------------------------- Helpers ---------------------------------*/ -void Mapper::set_prg_banks(const ROM_File& rom_file, const u16 size) { +void Mapper::init_prg_banks(const ROM_File& rom_file, const u16 size) { this->banks.prg.len = rom_file.rom.prg.len / size; this->banks.prg.bank = new ROM* [this->banks.prg.len]; @@ -14,7 +14,7 @@ void Mapper::set_prg_banks(const ROM_File& rom_file, const u16 size) { this->banks.prg.bank[i] = new ROM (size, p, "Mapper PRG"); } -void Mapper::set_chr_banks(const ROM_File& rom_file, const u16 size) { +void Mapper::init_chr_banks(const ROM_File& rom_file, const u16 size) { if (!rom_file.rom.chr.len) { fprintf(stderr, "[Mapper] No CHR ROM detected. Using 8K CHR RAM\n"); this->banks.chr.is_RAM = true; @@ -36,7 +36,7 @@ void Mapper::set_chr_banks(const ROM_File& rom_file, const u16 size) { : (Memory*) new ROM (size, p, "Mapper CHR ROM"); } -/*------------------------------- Serivices --------------------------------*/ +/*------------------ Common Mapper Functions / Services --------------------*/ void Mapper::irq_trigger() const { if (this->interrupt_line) @@ -84,8 +84,8 @@ Mapper::Mapper( : name(name) , number(number) { - this->set_prg_banks(rom_file, prg_bank_size); - this->set_chr_banks(rom_file, chr_bank_size); + this->init_prg_banks(rom_file, prg_bank_size); + this->init_chr_banks(rom_file, chr_bank_size); } /*--------------------------------- Factory --------------------------------*/ diff --git a/src/nes/cartridge/mapper.h b/src/nes/cartridge/mapper.h index 934c268..3212c43 100644 --- a/src/nes/cartridge/mapper.h +++ b/src/nes/cartridge/mapper.h @@ -11,10 +11,18 @@ #include "common/serializable.h" -/** - * @brief Base Mapper Interface - * @details Provides Mapper interface, and provides common mapper functionality - */ +// Base Mapper Interface +// Implements common mapper utilities (eg: bank-chunking, IRQ handling) +// At minimum, Mappers must implement the following methods: +// - peek/write +// - update_banks +// - mirroring +// - reset +// The following virtual methods implement default behaviors: +// - read ................. calls peek +// - get/setBatterySave ... get returns nullptr, set does nothing +// - cycle ................ does nothing +// - power_cycle .......... clears CHR RAM, and calls reset + update_banks class Mapper : public Memory, public Serializable { private: /*--------------------------------- Data ---------------------------------*/ @@ -40,6 +48,8 @@ private: } chr; } banks; + /*---------------------------- Serialization -----------------------------*/ + protected: SERIALIZE_START(this->banks.chr.len, "Mapper") SERIALIZE_CUSTOM() { @@ -52,11 +62,21 @@ protected: } SERIALIZE_END(this->banks.chr.len) - /*------------------------------- Methods --------------------------------*/ + virtual void update_banks() = 0; + + virtual const Serializable::Chunk* deserialize(const Serializable::Chunk* c) override { + c = this->Serializable::deserialize(c); + this->update_banks(); + return c; + } + + /*------------------------------- Helpers --------------------------------*/ private: - void set_prg_banks(const ROM_File& rom_file, const u16 size); - void set_chr_banks(const ROM_File& rom_file, const u16 size); + void init_prg_banks(const ROM_File& rom_file, const u16 size); + void init_chr_banks(const ROM_File& rom_file, const u16 size); + + /*----------------- Common Mapper Functions / Services -------------------*/ protected: // Services provided to all mappers @@ -67,19 +87,17 @@ protected: ROM& get_prg_bank(uint bank) const; Memory& get_chr_bank(uint bank) const; + /*-------------------------- External Interface --------------------------*/ + public: - virtual ~Mapper(); - Mapper( uint number, const char* name, const ROM_File& rom_file, u16 prg_bank_size, u16 chr_bank_size ); - /*------------------- Emulator Actions Mapper Interface ------------------*/ - - // Call when inserting / removing cartridge from NES + // Should be called when when inserting / removing cartridges from NES void set_interrupt_line(InterruptLines* interrupt_line) { this->interrupt_line = interrupt_line; } @@ -90,30 +108,24 @@ public: // ---- Battery Backed Saving ---- // virtual const Serializable::Chunk* getBatterySave() const { return nullptr; } - virtual void setBatterySave(const Serializable::Chunk* c) { (void)c; } - - // ---- Serialization ---- // - virtual const Serializable::Chunk* deserialize(const Serializable::Chunk* c) override { - c = this->Serializable::deserialize(c); - this->update_banks(); - return c; - } + virtual void setBatterySave(const Serializable::Chunk* c) { return (void)c; } /*------------------------ Core Mapper Interface -------------------------*/ - // - // reading doesn't tend to have side-effects, except in some fancy mappers + // reads tend to be side-effect free, except with some fancier mappers virtual u8 read(u16 addr) override { return this->peek(addr); } virtual u8 peek(u16 addr) const override = 0; virtual void write(u16 addr, u8 val) override = 0; - // virtual Mirroring::Type mirroring() const = 0; // Get mirroring mode - // Mappers tend to be benign, but some do have some fancy behavior. virtual void cycle() {} virtual void power_cycle() { + // NOTE: there are a couple of boards that have battery-backed CHR RAM. + // They required a complete override of the power_cycle() method + // iNES 168 + // NES 2.0 513 if (this->banks.chr.is_RAM) { for (uint j = 0; j < this->banks.chr.len; j++) { static_cast(this->banks.chr.bank[j])->clear(); @@ -124,9 +136,6 @@ public: }; virtual void reset() = 0; -protected: - virtual void update_banks() = 0; // (called post-deserialization) - public: /*------------------------------ Utilities -------------------------------*/ diff --git a/src/nes/cartridge/parse_rom.cc b/src/nes/cartridge/parse_rom.cc index 506464b..e84fd53 100644 --- a/src/nes/cartridge/parse_rom.cc +++ b/src/nes/cartridge/parse_rom.cc @@ -4,8 +4,9 @@ #include -static ROM_File* parse_iNES(const u8* data, uint data_len) { - (void)data_len; +// https://wiki.nesdev.com/w/index.php/INES +static ROM_File* parseROM_iNES(const u8* data, uint data_len) { + (void)data_len; // TODO: actually bounds-check data lol const u8 prg_rom_pages = data[4]; const u8 chr_rom_pages = data[5]; @@ -21,10 +22,10 @@ static ROM_File* parse_iNES(const u8* data, uint data_len) { } // Cool, this seems to be a valid iNES rom. - // Let's allocate the rom_file, and get to work! - ROM_File* rom_file = new ROM_File(); - rom_file->data = data; - rom_file->data_len = data_len; + // Let's allocate the rom_file, handoff data ownership, and get to work! + ROM_File* rf = new ROM_File(); + rf->data = data; + rf->data_len = data_len; // 7 0 // --------- @@ -36,26 +37,24 @@ static ROM_File* parse_iNES(const u8* data, uint data_len) { // B: has_battery // M: mirror_type (0 = horizontal, 1 = vertical) - rom_file->meta.has_trainer = nth_bit(data[6], 2); - rom_file->meta.has_battery = nth_bit(data[6], 1); + rf->meta.has_trainer = nth_bit(data[6], 2); + rf->meta.has_battery = nth_bit(data[6], 1); if (nth_bit(data[6], 3)) { - rom_file->meta.mirror_mode = Mirroring::FourScreen; + rf->meta.mirror_mode = Mirroring::FourScreen; } else { - if (nth_bit(data[6], 0)) { - rom_file->meta.mirror_mode = Mirroring::Vertical; - } else { - rom_file->meta.mirror_mode = Mirroring::Horizontal; - } + rf->meta.mirror_mode = nth_bit(data[6], 0) + ? Mirroring::Vertical + : Mirroring::Horizontal; } - if (rom_file->meta.has_trainer) + if (rf->meta.has_trainer) fprintf(stderr, "[File Parsing][iNES] ROM has a trainer\n"); - if (rom_file->meta.has_battery) - fprintf(stderr, "[File Parsing][iNES] ROM has a battery\n"); + if (rf->meta.has_battery) + fprintf(stderr, "[File Parsing][iNES] ROM has battery-backed SRAM\n"); fprintf(stderr, "[File Parsing][iNES] Initial Mirroring Mode: %s\n", - Mirroring::toString(rom_file->meta.mirror_mode)); + Mirroring::toString(rf->meta.mirror_mode)); // 7 0 // --------- @@ -72,19 +71,19 @@ static ROM_File* parse_iNES(const u8* data, uint data_len) { fprintf(stderr, "[File Parsing][iNES] ROM has NES 2.0 header.\n"); } - rom_file->meta.is_PC10 = nth_bit(data[7], 1); - rom_file->meta.is_VS = nth_bit(data[7], 0); + rf->meta.is_PC10 = nth_bit(data[7], 1); + rf->meta.is_VS = nth_bit(data[7], 0); - if (rom_file->meta.is_PC10) + if (rf->meta.is_PC10) fprintf(stderr, "[File Parsing][iNES] This is a PC10 ROM\n"); - if (rom_file->meta.is_VS) + if (rf->meta.is_VS) fprintf(stderr, "[File Parsing][iNES] This is a VS ROM\n"); - rom_file->meta.mapper = data[6] >> 4 | (data[7] & 0xFF00); + rf->meta.mapper = data[6] >> 4 | (data[7] & 0xFF00); - fprintf(stderr, "[File Parsing][iNES] Mapper: %d\n", rom_file->meta.mapper); + fprintf(stderr, "[File Parsing][iNES] Mapper: %d\n", rf->meta.mapper); - // I'm not parsing the rest of the header, since it's not that useful + // I'm not parsing the rest of the header, since it's not that useful... // Finally, use the header data to get pointers into the data for the various @@ -104,30 +103,30 @@ static ROM_File* parse_iNES(const u8* data, uint data_len) { const u8* data_p = data + 0x10; // move past header // Look for the trainer - if (rom_file->meta.has_trainer) { - rom_file->rom.misc.trn_rom = data_p; + if (rf->meta.has_trainer) { + rf->rom.misc.trn_rom = data_p; data_p += 0x200; } else { - rom_file->rom.misc.trn_rom = nullptr; + rf->rom.misc.trn_rom = nullptr; } - rom_file->rom.prg.len = prg_rom_pages * 0x4000; - rom_file->rom.prg.data = data_p; - data_p += rom_file->rom.prg.len; + rf->rom.prg.len = prg_rom_pages * 0x4000; + rf->rom.prg.data = data_p; + data_p += rf->rom.prg.len; - rom_file->rom.chr.len = chr_rom_pages * 0x2000; - rom_file->rom.chr.data = data_p; - data_p += rom_file->rom.chr.len; + rf->rom.chr.len = chr_rom_pages * 0x2000; + rf->rom.chr.data = data_p; + data_p += rf->rom.chr.len; - if (rom_file->meta.is_PC10) { - rom_file->rom.misc.pci_rom = data_p; - rom_file->rom.misc.pc_prom = data_p + 0x2000; + if (rf->meta.is_PC10) { + rf->rom.misc.pci_rom = data_p; + rf->rom.misc.pc_prom = data_p + 0x2000; } else { - rom_file->rom.misc.pci_rom = nullptr; - rom_file->rom.misc.pc_prom = nullptr; + rf->rom.misc.pci_rom = nullptr; + rf->rom.misc.pc_prom = nullptr; } - return rom_file; + return rf; } static ROMFileFormat::Type rom_type(const u8* data, uint data_len) { @@ -136,7 +135,7 @@ static ROMFileFormat::Type rom_type(const u8* data, uint data_len) { // Can't parse data if there is none ;) if (data == nullptr) { fprintf(stderr, "[File Parsing] ROM file is nullptr!\n"); - return ROMFileFormat::INVALID; + return ROMFileFormat::ROM_INVALID; } // Try to determine ROM format @@ -152,22 +151,22 @@ static ROMFileFormat::Type rom_type(const u8* data, uint data_len) { const bool is_NES2 = nth_bit(data[7], 3) && !nth_bit(data[6], 2); if (is_NES2) { fprintf(stderr, "[File Parsing] ROM has NES 2.0 header.\n"); - return ROMFileFormat::NES2; + return ROMFileFormat::ROM_NES2; } - return ROMFileFormat::iNES; + return ROMFileFormat::ROM_iNES; } fprintf(stderr, "[File Parsing] Cannot identify ROM type!\n"); - return ROMFileFormat::INVALID; + return ROMFileFormat::ROM_INVALID; } -ROM_File* parse_ROM(const u8* data, uint data_len) { +ROM_File* parseROM(const u8* data, uint data_len) { // Determine ROM type, and parse it switch(rom_type(data, data_len)) { - case ROMFileFormat::iNES: return parse_iNES(data, data_len); - case ROMFileFormat::NES2: return parse_iNES(data, data_len); - case ROMFileFormat::INVALID: return nullptr; + case ROMFileFormat::ROM_iNES: return parseROM_iNES(data, data_len); + case ROMFileFormat::ROM_NES2: return parseROM_iNES(data, data_len); + case ROMFileFormat::ROM_INVALID: return nullptr; } return nullptr; } diff --git a/src/nes/cartridge/parse_rom.h b/src/nes/cartridge/parse_rom.h index facdda5..4286353 100644 --- a/src/nes/cartridge/parse_rom.h +++ b/src/nes/cartridge/parse_rom.h @@ -4,11 +4,11 @@ namespace ROMFileFormat { enum Type { - iNES, - NES2, - INVALID + ROM_iNES, + ROM_NES2, + ROM_INVALID }; } // Parses data into usable ROM_File -ROM_File* parse_ROM(const u8* data, uint data_len); +ROM_File* parseROM(const u8* data, uint data_len); diff --git a/src/nes/cpu/cpu.cc b/src/nes/cpu/cpu.cc index e1a7ef2..e370505 100644 --- a/src/nes/cpu/cpu.cc +++ b/src/nes/cpu/cpu.cc @@ -26,25 +26,22 @@ void CPU::power_cycle() { this->reg.s = 0xFD; - this->state = CPU::State::Running; + this->is_running = true; } void CPU::reset() { - this->state = CPU::State::Running; + this->is_running = true; + // RESET interrupt should be asserted exterally } -CPU::State CPU::getState() const { return this->state; } - /*---------------------------- Private Methods -----------------------------*/ void CPU::service_interrupt(Interrupts::Type interrupt, bool brk /* = false */) { - using namespace Interrupts; - - assert(interrupt != NONE); + assert(interrupt != Interrupts::NONE); #ifdef NESTEST // custom reset for headless nestest - if (interrupt == RESET) { + if (interrupt == Interrupts::RESET) { this->reg.pc = 0xC000; this->interrupt.service(interrupt); return; @@ -52,7 +49,7 @@ void CPU::service_interrupt(Interrupts::Type interrupt, bool brk /* = false */) #endif // Push stack pointer and processor status onto stack for safekeeping - if (interrupt != RESET) { + if (interrupt != Interrupts::RESET) { this->s_push16(this->reg.pc); this->s_push(this->reg.p.raw); } else { @@ -63,10 +60,10 @@ void CPU::service_interrupt(Interrupts::Type interrupt, bool brk /* = false */) this->cycles += 7; switch (interrupt) { - case IRQ: if (brk || !this->reg.p.i) - this->reg.pc = this->mem.read16(0xFFFE); break; - case RESET: this->reg.pc = this->mem.read16(0xFFFC); break; - case NMI: this->reg.pc = this->mem.read16(0xFFFA); break; + case Interrupts::IRQ: if (brk || !this->reg.p.i) + this->reg.pc = this->read16(0xFFFE); break; + case Interrupts::RESET: this->reg.pc = this->read16(0xFFFC); break; + case Interrupts::NMI: this->reg.pc = this->read16(0xFFFA); break; default: break; } @@ -84,7 +81,7 @@ u16 CPU::get_operand_addr(const Instructions::Opcode& opcode) { // To keep switch-statement code clean, define some temporary macros that // read 8 and 1 bit arguments (respectively) #define arg8 (this->mem[this->reg.pc++]) - #define arg16 (this->mem.read16((this->reg.pc += 2) - 2)) + #define arg16 (this->read16((this->reg.pc += 2) - 2)) #define dummy_read() this->mem.read(this->reg.pc) @@ -92,9 +89,9 @@ u16 CPU::get_operand_addr(const Instructions::Opcode& opcode) { case abs_: addr = arg16; break; case absX: addr = arg16 + this->reg.x; break; case absY: addr = arg16 + this->reg.y; break; - case ind_: addr = this->mem.read16_zpg(arg16); break; - case indY: addr = this->mem.read16_zpg(arg8) + this->reg.y; break; - case Xind: addr = this->mem.read16_zpg((arg8 + this->reg.x) & 0xFF); break; + case ind_: addr = this->read16_zpg(arg16); break; + case indY: addr = this->read16_zpg(arg8) + this->reg.y; break; + case Xind: addr = this->read16_zpg((arg8 + this->reg.x) & 0xFF); break; case zpg_: addr = arg8; break; case zpgX: addr = (arg8 + this->reg.x) & 0xFF; break; case zpgY: addr = (arg8 + this->reg.y) & 0xFF; break; @@ -145,10 +142,10 @@ uint CPU::step() { Instructions::Opcode opcode = Instructions::Opcodes[op]; if (this->print_nestest) { - this->nestest(opcode); + this->nestest(*this, opcode); } #ifdef NESTEST - this->nestest(opcode); // print NESTEST debug info + this->nestest(*this, opcode); #endif u16 addr = this->get_operand_addr(opcode); @@ -171,8 +168,11 @@ uint CPU::step() { /* Check if extra cycles due to jumping across pages */ \ if ((this->reg.pc & 0xFF00) != ((this->reg.pc + offset) & 0xFF00)) \ this->cycles += 1; \ - this->reg.pc += offset; \ + this->reg.pc += offset; + // This _may_ seem bad, but in reality, this switch statement actually gets + // compiled down to a jump table! + // Don't believe me? Check godbolt! switch (opcode.instr) { case ADC: { u8 val = this->mem[addr]; u16 sum = this->reg.a + val + this->reg.p.c; @@ -219,7 +219,7 @@ uint CPU::step() { case BPL: { branch(!this->reg.p.n); } break; case BRK: { // ignores interrupt disable bit, and forces an interrupt - this->service_interrupt(Interrupts::Type::IRQ, true); + this->service_interrupt(Interrupts::IRQ, true); } break; case BVC: { branch(!this->reg.p.v); } break; @@ -394,7 +394,7 @@ uint CPU::step() { this->cycles, opcode.raw ); - this->state = CPU::State::Halted; + this->is_running = false; break; } @@ -416,3 +416,26 @@ void CPU::s_push16(u16 val) { this->s_push(val >> 8); // push hi this->s_push(val & 0xFF); // push lo } + +u16 CPU::peek16(u16 addr) const { + return this->mem.peek(addr + 0) | + (this->mem.peek(addr + 1) << 8); +} +u16 CPU::read16(u16 addr) { + return this->mem.read(addr + 0) | + (this->mem.read(addr + 1) << 8); +}; + +u16 CPU::peek16_zpg(u16 addr) const { + return this->mem.peek(addr + 0) | + (this->mem.peek(((addr + 0) & 0xFF00) | ((addr + 1) & 0x00FF)) << 8); +} +u16 CPU::read16_zpg(u16 addr) { + return this->mem.read(addr + 0) | + (this->mem.read(((addr + 0) & 0xFF00) | ((addr + 1) & 0x00FF)) << 8); +} + +void CPU::write16(u16 addr, u8 val) { + this->mem.write(addr + 0, val); + this->mem.write(addr + 1, val); +} diff --git a/src/nes/cpu/cpu.h b/src/nes/cpu/cpu.h index 341f833..deac316 100644 --- a/src/nes/cpu/cpu.h +++ b/src/nes/cpu/cpu.h @@ -15,12 +15,6 @@ // http://users.telenet.be/kim1-6502/6502 // http://obelisk.me.uk/6502 class CPU final : public Serializable { -public: - enum class State { - Running, - Halted - }; - private: /*---------- Hardware ----------*/ @@ -54,12 +48,12 @@ private: /*---------- Emulation Vars ----------*/ uint cycles; // Cycles elapsed - State state; // CPU state + bool is_running; SERIALIZE_START(3, "CPU") SERIALIZE_POD(reg) SERIALIZE_POD(cycles) - SERIALIZE_POD(state) + SERIALIZE_POD(is_running) SERIALIZE_END(3) /*-------------- Helpers -------------*/ @@ -74,11 +68,17 @@ private: void s_push (u8 val); void s_push16(u16 val); + // 16-bit memory accesses + u16 peek16(u16 addr) const; + u16 read16(u16 addr); + u16 peek16_zpg(u16 addr) const; + u16 read16_zpg(u16 addr); + void write16(u16 addr, u8 val); + /*------------- Debug --------------*/ const bool& print_nestest; - - // print nestest golden-log formatted CPU log data - void nestest(const Instructions::Opcode& opcode) const; + // nestest is implemented in nestest.cc + static void nestest(const CPU& cpu, const Instructions::Opcode& opcode); public: CPU() = delete; @@ -87,7 +87,7 @@ public: void power_cycle(); void reset(); - CPU::State getState() const; + bool isRunning() const { return this->is_running; } uint step(); // exec instruction, and return cycles taken }; diff --git a/src/nes/cpu/instructions.h b/src/nes/cpu/instructions.h index 859f79f..30a34b2 100644 --- a/src/nes/cpu/instructions.h +++ b/src/nes/cpu/instructions.h @@ -4,13 +4,6 @@ namespace Instructions { -// I am using namespaced enums instead of enum classes so that I can use -// `using namepsace Instructions::Instr and Instructions::AddrM` in functions -// that make heavy use of them. -// -// The alternative is enum switch statements where every single case has to have -// Instr:: and AddrM:: appended to the front of it, which suuuuucks - namespace Instr { enum Type { INVALID, diff --git a/src/nes/cpu/nestest.cc b/src/nes/cpu/nestest.cc index bf7822a..cd6fffc 100644 --- a/src/nes/cpu/nestest.cc +++ b/src/nes/cpu/nestest.cc @@ -3,11 +3,11 @@ #include -void CPU::nestest(const Instructions::Opcode& opcode) const { +void CPU::nestest(const CPU& cpu, const Instructions::Opcode& opcode) { using namespace Instructions; // Print PC and raw opcode byte - printf("%04X %02X ", this->reg.pc - 1, opcode.raw); + printf("%04X %02X ", cpu.reg.pc - 1, opcode.raw); // create buffer for instruction operands char instr_buf [64]; @@ -18,8 +18,8 @@ void CPU::nestest(const Instructions::Opcode& opcode) const { using namespace Instructions::AddrM; // Evaluate a few useful values - u8 arg8 = this->mem.peek(this->reg.pc + 0); - u8 arg8_2 = this->mem.peek(this->reg.pc + 1); + u8 arg8 = cpu.mem.peek(cpu.reg.pc + 0); + u8 arg8_2 = cpu.mem.peek(cpu.reg.pc + 1); u16 arg16 = arg8 | (arg8_2 << 8); // Print operand bytes @@ -50,72 +50,72 @@ void CPU::nestest(const Instructions::Opcode& opcode) const { // Decode addressing mode switch(opcode.addrm) { - case abs_: addr = arg16; break; - case absX: addr = arg16 + this->reg.x; break; - case absY: addr = arg16 + this->reg.y; break; - case ind_: addr = this->mem.peek16_zpg(arg16); break; - case indY: addr = this->mem.peek16_zpg(arg8) + this->reg.y; break; - case Xind: addr = this->mem.peek16_zpg((arg8 + this->reg.x) & 0xFF); break; - case zpg_: addr = arg8; break; - case zpgX: addr = (arg8 + this->reg.x) & 0xFF; break; - case zpgY: addr = (arg8 + this->reg.y) & 0xFF; break; - case rel : addr = this->reg.pc; break; - case imm : addr = this->reg.pc; break; - case acc : addr = this->reg.a; break; - case impl: addr = u8(0xFACA11); break; + case abs_: addr = arg16; break; + case absX: addr = arg16 + cpu.reg.x; break; + case absY: addr = arg16 + cpu.reg.y; break; + case ind_: addr = cpu.peek16_zpg(arg16); break; + case indY: addr = cpu.peek16_zpg(arg8) + cpu.reg.y; break; + case Xind: addr = cpu.peek16_zpg((arg8 + cpu.reg.x) & 0xFF); break; + case zpg_: addr = arg8; break; + case zpgX: addr = (arg8 + cpu.reg.x) & 0xFF; break; + case zpgY: addr = (arg8 + cpu.reg.y) & 0xFF; break; + case rel : addr = cpu.reg.pc; break; + case imm : addr = cpu.reg.pc; break; + case acc : addr = cpu.reg.a; break; + case impl: addr = u8(0xFACA11); break; default: break; } // Print specific instrucion operands for each addressing mode switch(opcode.addrm) { case abs_: sprintf(instr_buf, "$%04X = %02X", - arg16, - this->mem.peek(arg16) + arg16, + cpu.mem.peek(arg16) ); break; case absX: sprintf(instr_buf, "$%04X,X @ %04X = %02X", - arg16, - u16(arg16 + this->reg.x), - this->mem.peek(arg16 + this->reg.x) + arg16, + u16(arg16 + cpu.reg.x), + cpu.mem.peek(arg16 + cpu.reg.x) ); break; case absY: sprintf(instr_buf, "$%04X,Y @ %04X = %02X", - arg16, - u16(arg16 + this->reg.y), - this->mem.peek(arg16 + this->reg.y) + arg16, + u16(arg16 + cpu.reg.y), + cpu.mem.peek(arg16 + cpu.reg.y) ); break; case indY: sprintf(instr_buf, "($%02X),Y = %04X @ %04X = %02X", - arg8, - this->mem.peek16_zpg(arg8), - u16(this->mem.peek16_zpg(arg8) + this->reg.y), - this->mem.peek(this->mem.peek16_zpg(arg8) + this->reg.y) + arg8, + cpu.peek16_zpg(arg8), + u16(cpu.peek16_zpg(arg8) + cpu.reg.y), + cpu.mem.peek(cpu.peek16_zpg(arg8) + cpu.reg.y) ); break; case Xind: sprintf(instr_buf, "($%02X,X) @ %02X = %04X = %02X", - arg8, - u8(this->reg.x + arg8), - this->mem.peek16_zpg(u8(this->reg.x + arg8)), - this->mem.peek(this->mem.peek16_zpg(u8(this->reg.x + arg8))) + arg8, + u8(cpu.reg.x + arg8), + cpu.peek16_zpg(u8(cpu.reg.x + arg8)), + cpu.mem.peek(cpu.peek16_zpg(u8(cpu.reg.x + arg8))) ); break; case ind_: sprintf(instr_buf, "($%04X) = %04X", - arg16, - this->mem.peek16_zpg(arg16) + arg16, + cpu.peek16_zpg(arg16) ); break; case zpg_: sprintf(instr_buf, "$%02X = %02X", - arg8, - this->mem.peek(arg8) + arg8, + cpu.mem.peek(arg8) ); break; case zpgX: sprintf(instr_buf, "$%02X,X @ %02X = %02X", - arg8, - u8(arg8 + this->reg.x), - this->mem.peek(u8(arg8 + this->reg.x)) + arg8, + u8(arg8 + cpu.reg.x), + cpu.mem.peek(u8(arg8 + cpu.reg.x)) ); break; case zpgY: sprintf(instr_buf, "$%02X,Y @ %02X = %02X", - arg8, - u8(arg8 + this->reg.y), - this->mem.peek(u8(arg8 + this->reg.y)) + arg8, + u8(arg8 + cpu.reg.y), + cpu.mem.peek(u8(arg8 + cpu.reg.y)) ); break; - case rel : sprintf(instr_buf, "$%04X", this->reg.pc + 1 + i8(arg8)); break; - case imm : sprintf(instr_buf, "#$%02X", arg8); break; - case acc : sprintf(instr_buf, " "); break; - case impl: sprintf(instr_buf, " "); break; + case rel : sprintf(instr_buf, "$%04X", cpu.reg.pc + 1 + i8(arg8)); break; + case imm : sprintf(instr_buf, "#$%02X", arg8); break; + case acc : sprintf(instr_buf, " "); break; + case impl: sprintf(instr_buf, " "); break; default: sprintf(instr_buf, " "); break; } @@ -143,14 +143,14 @@ void CPU::nestest(const Instructions::Opcode& opcode) const { // Print processor state printf("A:%02X X:%02X Y:%02X P:%02X SP:%02X CYC:%3u\n", - this->reg.a, - this->reg.x, - this->reg.y, - this->reg.p.raw & ~0x10, // 0b11101111, match nestest "golden" log - this->reg.s, - this->cycles * 3 % 341 // CYC measures PPU X coordinates - // PPU does 1 x coordinate per cycle - // PPU runs 3x as fast as CPU - // ergo, multiply cycles by 3 should be fineee + cpu.reg.a, + cpu.reg.x, + cpu.reg.y, + cpu.reg.p.raw & ~0x10, // 0b11101111, match nestest "golden" log + cpu.reg.s, + cpu.cycles * 3 % 341 // CYC measures PPU X coordinates + // PPU does 1 x coordinate per cycle + // PPU runs 3x as fast as CPU + // ergo, multiply cycles by 3 should be fineee ); } diff --git a/src/nes/interfaces/memory.h b/src/nes/interfaces/memory.h index e83f8bb..a640404 100644 --- a/src/nes/interfaces/memory.h +++ b/src/nes/interfaces/memory.h @@ -3,39 +3,17 @@ #include "common/util.h" // 16 bit addressable memory interface +// All memory interfaces must also implement peek, a const version on read. +// This is awesome, since it means one can write tools to inspect any memory +// location without modifying the emulator state! class Memory { public: virtual ~Memory() = default; - virtual u8 peek(u16 addr) const = 0; // read, but without the side-effects + virtual u8 peek(u16 addr) const = 0; virtual u8 read(u16 addr) = 0; virtual void write(u16 addr, u8 val) = 0; - // ---- Derived memory access methods ---- // - - u16 peek16(u16 addr) const { - return this->peek(addr + 0) | - (this->peek(addr + 1) << 8); - } - u16 read16(u16 addr) { - return this->read(addr + 0) | - (this->read(addr + 1) << 8); - }; - - u16 peek16_zpg(u16 addr) const { - return this->peek(addr + 0) | - (this->peek(((addr + 0) & 0xFF00) | ((addr + 1) & 0x00FF)) << 8); - } - u16 read16_zpg(u16 addr) { - return this->read(addr + 0) | - (this->read(((addr + 0) & 0xFF00) | ((addr + 1) & 0x00FF)) << 8); - } - - void write16(u16 addr, u8 val) { - this->write(addr + 0, val); - this->write(addr + 1, val); - } - // ---- Overloaded Array Subscript Operator ---- // // If we want to define this overload at the interface level, things get hard. // See, the Array Subscript operator is supposed to return a &u8, so that diff --git a/src/nes/joy/controllers/standard.cc b/src/nes/joy/controllers/standard.cc index 6b09899..34f053c 100644 --- a/src/nes/joy/controllers/standard.cc +++ b/src/nes/joy/controllers/standard.cc @@ -4,7 +4,9 @@ #include #include -JOY_Standard::JOY_Standard(const char* label /* = "?" */) : label(label) {} +JOY_Standard::JOY_Standard(const char* label /* = "?" */) : label(label) { + (void)this->label; // silence unused warning +} u8 JOY_Standard::read(u16 addr) { (void) addr; diff --git a/src/nes/joy/controllers/standard.h b/src/nes/joy/controllers/standard.h index 82a7916..3db1a41 100644 --- a/src/nes/joy/controllers/standard.h +++ b/src/nes/joy/controllers/standard.h @@ -18,11 +18,12 @@ namespace JOY_Standard_Button { }; } +// https://wiki.nesdev.com/w/index.php/Standard_controller class JOY_Standard final : public Memory, public Serializable { private: bool strobe = false; u8 curr_btn = 0x01; - u8 buttons = 0; + u8 buttons = 0x00; const char* label; diff --git a/src/nes/joy/controllers/zapper.cc b/src/nes/joy/controllers/zapper.cc index 8a2e6dc..c7cac94 100644 --- a/src/nes/joy/controllers/zapper.cc +++ b/src/nes/joy/controllers/zapper.cc @@ -1,6 +1,8 @@ #include "zapper.h" -JOY_Zapper::JOY_Zapper(const char* label) : label(label) {} +JOY_Zapper::JOY_Zapper(const char* label) : label(label) { + (void)this->label; // silence unused warning +} // u8 JOY_Zapper::read(u16 addr) { return this->peek(addr); } diff --git a/src/nes/joy/joy.cc b/src/nes/joy/joy.cc index 850bc3f..84e56ea 100644 --- a/src/nes/joy/joy.cc +++ b/src/nes/joy/joy.cc @@ -3,11 +3,6 @@ #include #include -JOY::JOY() { - this->joy[0] = nullptr; - this->joy[1] = nullptr; -} - u8 JOY::read(u16 addr) { // Why | 0x40? // On the NES and Famicom, the top three (or five) bits are Open Bus. @@ -40,14 +35,14 @@ u8 JOY::peek(u16 addr) const { void JOY::write(u16 addr, u8 val) { if (addr == 0x4016) { - if (this->joy[0]) this->joy[0]->write(addr, val); - if (this->joy[1]) this->joy[1]->write(addr, val); - } else { - fprintf(stderr, - "[JOY] Should not be able to write to 0x%04X! Check MMU!\n", addr - ); - assert(false); + if (this->joy[0]) return this->joy[0]->write(addr, val); + if (this->joy[1]) return this->joy[1]->write(addr, val); } + + fprintf(stderr, + "[JOY] Should not be able to write to 0x%04X! Check MMU!\n", addr + ); + assert(false); } void JOY::attach_joy(uint port, Memory* joy) { diff --git a/src/nes/joy/joy.h b/src/nes/joy/joy.h index 3811267..b15f613 100644 --- a/src/nes/joy/joy.h +++ b/src/nes/joy/joy.h @@ -3,14 +3,16 @@ #include "common/util.h" #include "nes/interfaces/memory.h" -// Joypad router +// Joypad "router" +// Doesn't construct controllers, simply accepts controllers, and maps them to +// their appropriate memory address class JOY final : public Memory { private: - Memory* joy [2]; + Memory* joy [2] = { nullptr }; public: ~JOY() = default; // doesn't own joypads - JOY(); + JOY() = default; // u8 read(u16 addr) override; diff --git a/src/nes/nes.cc b/src/nes/nes.cc index 00d1811..71802a9 100644 --- a/src/nes/nes.cc +++ b/src/nes/nes.cc @@ -75,12 +75,6 @@ void NES::power_cycle() { this->is_running = true; - this->apu.power_cycle(); - this->cpu.power_cycle(); - this->ppu.power_cycle(); - - if (this->cart) this->cart->power_cycle(); - this->cpu_wram.clear(); this->ppu_pram.clear(); this->ppu_vram.clear(); @@ -88,12 +82,22 @@ void NES::power_cycle() { this->interrupts.clear(); this->interrupts.request(Interrupts::RESET); + this->apu.power_cycle(); + this->cpu.power_cycle(); + this->ppu.power_cycle(); + + if (this->cart) + this->cart->power_cycle(); + fprintf(stderr, "[NES] Power Cycled\n"); } void NES::reset() { this->is_running = true; + this->interrupts.clear(); + this->interrupts.request(Interrupts::RESET); + // cpu_wram, ppu_pram, and ppu_vram are not affected by resets // (i.e: they keep previous state) @@ -101,10 +105,8 @@ void NES::reset() { this->cpu.reset(); this->ppu.reset(); - if (this->cart) this->cart->reset(); - - this->interrupts.clear(); - this->interrupts.request(Interrupts::RESET); + if (this->cart) + this->cart->reset(); fprintf(stderr, "[NES] Reset\n"); } @@ -128,8 +130,7 @@ void NES::cycle() { this->cart->cycle(); } - // Check if the CPU halted, and stop NES if it is - if (this->cpu.getState() == CPU::State::Halted) + if (!this->cpu.isRunning()) this->is_running = false; } @@ -149,7 +150,3 @@ void NES::getFramebuff(const u8*& framebuffer) const { void NES::getAudiobuff(float*& samples, uint& len) { this->apu.getAudiobuff(samples, len); } - -bool NES::isRunning() const { - return this->is_running; -} diff --git a/src/nes/nes.h b/src/nes/nes.h index 1fe5816..2dd186b 100644 --- a/src/nes/nes.h +++ b/src/nes/nes.h @@ -19,6 +19,7 @@ // Core NES class. // - Owns all NES core resources (but NOT the cartridge or joypads) // - Runs CPU, PPU, APU +// - serialize() returns a full save state (including cart) class NES final : public Serializable { private: /*================================ @@ -86,15 +87,14 @@ public: void attach_joy(uint port, Memory* joy); void detach_joy(uint port); - void power_cycle(); // Set all volatile components to default power_on state - void reset(); // Set all volatile components to default reset state + void power_cycle(); + void reset(); void cycle(); // Run a single clock cycle - void step_frame(); // Run the NES until there is a new frame to display - // (calls cycle() internally) + void step_frame(); // Cycle the NES until there is a new frame to display void getFramebuff(const u8*& framebuffer) const; void getAudiobuff(float*& samples, uint& len); - bool isRunning() const; + bool isRunning() const { return this->is_running; } }; diff --git a/src/ui/SDL2/fs/load.cc b/src/ui/SDL2/fs/load.cc index 851c069..676806d 100644 --- a/src/ui/SDL2/fs/load.cc +++ b/src/ui/SDL2/fs/load.cc @@ -137,5 +137,5 @@ ROM_File* ANESE_fs::load::load_rom_file(const char* filepath) { return nullptr; } - return parse_ROM(data, data_len); + return parseROM(data, data_len); } diff --git a/src/ui/SDL2/gui_modules/emu.cc b/src/ui/SDL2/gui_modules/emu.cc index 611949b..d603b0c 100644 --- a/src/ui/SDL2/gui_modules/emu.cc +++ b/src/ui/SDL2/gui_modules/emu.cc @@ -292,16 +292,16 @@ int EmuModule::load_rom(const char* rompath) { Cartridge* cart = new Cartridge (ANESE_fs::load::load_rom_file(rompath)); switch (cart->status()) { - case Cartridge::Status::BAD_DATA: + case Cartridge::Status::CART_BAD_DATA: fprintf(stderr, "[Cart] ROM file could not be parsed!\n"); delete cart; return 1; - case Cartridge::Status::BAD_MAPPER: + case Cartridge::Status::CART_BAD_MAPPER: fprintf(stderr, "[Cart] Mapper %u has not been implemented yet!\n", cart->get_rom_file()->meta.mapper); delete cart; return 1; - case Cartridge::Status::NO_ERROR: + case Cartridge::Status::CART_NO_ERROR: fprintf(stderr, "[Cart] ROM file loaded successfully!\n"); strcpy(this->current_rom_file, rompath); this->cart = cart; @@ -346,9 +346,10 @@ int EmuModule::unload_rom(Cartridge* cart) { uint len; Serializable::Chunk::collate(data, len, sav); - const char* sav_file_name = (std::string(this->current_rom_file) + ".sav").c_str(); + char buf [256]; + sprintf(buf, "%s.sav", this->current_rom_file); - FILE* sav_file = fopen(sav_file_name, "wb"); + FILE* sav_file = fopen(buf, "wb"); if (!sav_file) { fprintf(stderr, "[Savegame][Save] Failed to open save file!\n"); return 1; @@ -356,8 +357,7 @@ int EmuModule::unload_rom(Cartridge* cart) { fwrite(data, 1, len, sav_file); fclose(sav_file); - fprintf(stderr, "[Savegame][Save] Game successfully saved to '%s'!\n", - sav_file_name); + fprintf(stderr, "[Savegame][Save] Game successfully saved to '%s'!\n", buf); delete sav; }