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)
This commit is contained in:
Daniel Prilik 2018-07-03 21:23:34 -07:00
parent 52c8ea08c2
commit 8423bd9a30
28 changed files with 382 additions and 426 deletions

View file

@ -13,20 +13,20 @@
</p>
**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

View file

@ -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.

3
scripts/README.md Normal file
View file

@ -0,0 +1,3 @@
These scripts should be run from the root directory.
eg: `sh scripts/cppcheck.sh`

View file

@ -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;
// }();

View file

@ -7,8 +7,8 @@
#include <cstdio>
// 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] = {{
"<nothing>", "<nothing>", _field_type::POD, &dump, sizeof dump, nullptr
"<nothing>", "<nothing>",
_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<const Serializable*>(&(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<const Serializable*>(&(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<const Serializable*>(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<const Serializable*>(thingptr), \
0, 0 \
};
#define SERIALIZE_CUSTOM() // doesn't do anything except express intent

View file

@ -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 {

View file

@ -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; }

View file

@ -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 --------------------------------*/

View file

@ -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 -------------------------*/
// <Memory>
// 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;
// <Memory/>
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<RAM*>(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 -------------------------------*/

View file

@ -4,8 +4,9 @@
#include <cstdio>
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;
}

View file

@ -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);

View file

@ -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);
}

View file

@ -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
};

View file

@ -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,

View file

@ -3,11 +3,11 @@
#include <cstdio>
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
);
}

View file

@ -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

View file

@ -4,7 +4,9 @@
#include <cassert>
#include <cstring>
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;

View file

@ -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;

View file

@ -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
}
// <Memory>
u8 JOY_Zapper::read(u16 addr) { return this->peek(addr); }

View file

@ -3,11 +3,6 @@
#include <cassert>
#include <cstdio>
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) {

View file

@ -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;
// <Memory>
u8 read(u16 addr) override;

View file

@ -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;
}

View file

@ -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; }
};

View file

@ -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);
}

View file

@ -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;
}