In Part I, I began work on an emulator for the Vectrex video game system - if you have not read Part I you should do that first. In this post I will start out by creating a boilerplate layout for the project and fill in some user interface parts - so there will be a basis for the emulator.
Now it's time to start writing some code... (Follow along with me on github beardypig/vectrexia-emulator )
I will be writing this emulator using C++, as it is object oriented, fast and somewhat portable - I plan to use some C++14 features which might not be available on all systems. I will be using CMake to build the project and I will be compiling with gcc on Linux and clang on Mac. I will primarily be doing the testing with an Ubuntu 14.04 VM - once it's working in Ubuntu I will make it work in MacOS, Windows, Android and on the RaspberryPi (not necessarily in that order).
I think writing portable software is important and I don't like reinventing the wheel (or writing UIs), so I have chosen to write this emulator as a libretro
core. One of the reasons I choose to emulate the Vectrex is that there aren't many Vectrex emulators that run on linux, MacOS, etc. (the SDL and existing libretro
ports of VecX, M.E.S.S., and ParaJVE
being the exceptions). Using libretro
means that I won't have to worry too much about writing UI code and just concentrate on the actual emulation!
Boring stuff first, the boilerplate for the project, it will be laid out like so:
.
├── LICENSE
├── CMakeLists.txt
├── README.md
├── link.T
├── tests
│ ├── CMakeLists.txt
│ └── test_runner.cpp
└── src
├── libretro
│ ├── libretro.h
│ └── libretro.cpp
├── CMakeLists.txt
└── [classes].cpp
The libretro.h
file is from the libretro
project and provides the header for the libretro
API. The CMake files build the retroarch core, currently this will only build on MacOS and Linux with a compiler that supports C++14 - I plan to add support for more platforms later.
The first thing to do is think about how the Vectrex will be represented. As C++ is an object oriented programming language, most of the components will be modelled as objects.
There will need to be a few classes, one for the Vectrex itself, called Vectrex
. The Vectrex
class will be the conductor of the other components. As discussed in the previous post the Vectrex has a M6809 CPU, some built in System RAM and ROM, address decoding, a 6522 VIA, an AY-3-8910 PSG, a multiplexed DAC, custom vector drawing circuitry, and user input circuitry.
Most of these components will be represented by an object; the CPU, the game Cartridge, the 6522, the AY-3-8910, the vector drawing hardware, and user input. The other components will be emulated inside the Vectrex
class as they glue the other components together or they are so trivial that it doesn't warrant a separate class. The system rom and ram, for example, are best represented by a simply array of bytes.
The CPU class will need to be able to access the memory of the rest of the system and will be able to do so via read/write callback methods defined in the Vectrex
class. These methods will effectively emulate the address decoding and the memory bus. The other components are also connected to the memory bus and will need to be accessed by the CPU.
There will need to be able to reset the machine, execute a number of CPU cycles, load a cartridge rom and unload a cartridge. Aside from the Cartridge
class, which will be use for loading and unloading ROM, we will not define any extra classes yet and will create them as they are required.
The best thing to do now would be to clone the vectrexia repo and take a look at the files to get an idea of where we are heading. You should also try to compile and run the libretro core in retroarch.
$ retroarch -L vectrexia_libretro.so
In vectrexia.h
you will see a class that represents the whole Vectrex machine, the Vectrex
class. We will add to this class as we go along. You will also see the Cartridge
class that will manage the loaded ROM and provide access to it. From the documentation we know that the cart ROM has a maximum addressable size of 32K, so for the base case we will simply use an array of 32K bytes.
libretro
is an API that is designed for creating emulators and games. libretro
projects are compiled in to a library file called a libretro
core. That core can be loaded by a front-end that supports the libretro
API, the front-end is responsible for video, audio, user input, etc. This means when you write a libretro
core you don't have to worry about the specifics of rendering a framebuffer on the Wii or on an Android phone, etc. and this makes life a lot simpler
I will get the libretro
part working first so that retroarch
can start the vectrexia
core without giving any errors, but it won't actually do anything. First we need to stub out some functions for libretro
, there are a fair number of API functions that have to be implemented - I won't list them all here as you can see them in the source code with some explanation. All these functions are implemented in libretro.cpp
.
There are two functions that return information about the system, retro_get_system_info
and retro_get_system_av_info
. These need to set information about the system that is being emulated.
/*
* Tell libretro about this core, it's name, version and which rom file types it supports.
*/
void retro_get_system_info(struct retro_system_info *info)
{
memset(info, 0, sizeof(*info));
info->library_name = "Vectrexia";
info->library_version = "0.1.0";
info->need_fullpath = false;
info->valid_extensions = "bin|vec"; // file types supported
}
/*
* Tell libretro about the AV system; the fps, sound sample rate and the
* resolution of the display.
*/
void retro_get_system_av_info(struct retro_system_av_info *info) {
memset(info, 0, sizeof(*info));
info->timing.fps = 50.0f;
info->timing.sample_rate = 44100; // 44.1kHz
info->geometry.base_width = 330;
info->geometry.base_height = 410;
info->geometry.max_width = 330;
info->geometry.max_height = 410;
info->geometry.aspect_ratio = 330.0f / 410.0f;
}
retro_get_system_info
populates a retro_system_info
struct that has information about the core; the name, the version and the rom files types it can load. retro_get_system_av_info
populates a retro_system_av_info
struct with specific information about the Vectrex AV systems; the sample rate for the audio we want (44.1kHz) and the frame rate (50 fps, same as PAL refresh rate) and the resolution of the display. With a vector display the resolution is a bit strange, according to gamasutra
the resolution is 330x410, so we'll go with that for now.
Next we need to be able to load a ROM, and then unload it. These functions are performed by retro_load_game
and retro_unload_game
.
The Vectrex came with a built-in game, "Mine Storm", that means that the emulator can we started without loading a game. libretro
needs to be configured to allow the emulator core to start without loading a ROM, this can be done by setting a RETRO_ENVIRONMENT
setting.
/* libretro global setters */
void retro_set_environment(retro_environment_t cb)
{
environ_cb = cb;
bool no_rom = true;
cb(RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME, &no_rom);
That setting is RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME
, this means that when the game is loaded the retro_game_info
struct may be set to NULL
. The built-in game is part of the system ROM which also provide some functions for controling the vector drawing electronics, the system ROM will be loaded in the retro_reset
function.
If a ROM is specified with the emulator core then it needs to be loaded in to the cartridge memory space, the ROMs (.vec/.bin) are simply dumps of ROM chips in the cartridge with no header or packing so they do not need to be preprocessed at all.
To do this we will need to implement a couple of methods in the Cartridge
class, one to load and one to unload the cartridge - these will be called from the Vectrex class via Vectrex::LoadCartridge
and Vectrex::UnloadCartridge
.
void Cartridge::Load(const uint8_t *data, size_t size)
{
if (size <= rom_.size()) {
memcpy(rom_.data(), data, size);
is_loaded_flag_ = true;
}
else
{
Unload();
}
}
void Cartridge::Unload()
{
rom_.fill(0);
is_loaded_flag_ = false;
}
bool Cartridge::is_loaded()
{
return is_loaded_flag_;
}
In the Vectrex
class, there needs to be an instance of Cartridge, for that we defined a unique_ptr<Cartridge>
to store the reference to the Cartridge and then assign it in the Vectrex::CartLoadCartridge
method.
bool Vectrex::LoadCartridge(const uint8_t *data, size_t size)
{
cartridge_ = std::make_unique<Cartridge>();
cartridge_->Load(data, size);
return cartridge_->is_loaded();
}
void Vectrex::UnloadCartridge()
{
// Can only unload a cartridge, if one has been loaded
if (cartridge_ && cartridge_->is_loaded())
cartridge_->Unload();
}
// Load a cartridge
bool retro_load_game(const struct retro_game_info *info)
{
// Reset the Vectrex, reset all the component
vectrex.Reset();
if (info && info->data) { // ensure there is ROM data
return vectrex.LoadCartridge((const uint8_t*)info->data, info->size);
}
return true;
}
Unloading is simple, we just clear the cart memory array and then reset the Vectrex.
// Unload the cartridge
void retro_unload_game(void)
{
vectrex.UnloadCartridge();
}
Next retro_run
and retro_reset
control stepping the Vectrex and resetting it. retro_run
will run a single frame of emulation; during which the CPU, vector circuitry, sound chip, etc. will need to be updated, then the frame is rendered to the screen and the sound generated is output. For that will need the functions for stepping and reseting the Vectrex. We will stub out the implementation of these functions as they don't have anything to do yet.
Vectrex::Run
will run the machine for the number of clock cycles provided, and Vectrex::Reset
will reset the Vectrex.
unsigned short framebuffer[330*410];
// Run a single frames with out Vectrex emulation.
void retro_run(void)
{
// Vectrex CPU is 1.5MHz (1500000) and at 50 fps, a frame lasts 20ms, therefore in every frame 30,000 cycles happen.
vectrex->Run(30000);
// 882 audio samples per frame (44.1kHz @ 50 fps)
for (int i = 0; i < 882; i++) {
audio_cb(1, 1);
}
video_cb(framebuffer, 330, 410, sizeof(unsigned short) * 330);
}
// Reset the Vectrex
void retro_reset(void) { vectrex.Reset(); }
retro_run
is called once per frame at the frame rate we requested earlier (50.0 fps), so the Vectrex needs to run for the number of cycles in a frame. This is calculated by taking the clock speed (1.5MHz), the time for one frame (20ms - at 50 fps it is 1/50 of a second) and multiplying them. The number of cycles per frame should, therefore, be 30,000.
The retro_run
function should generate the video and audio for one frame, the video data is sent back to the libretro
front-end using video_cb
, and the audio is sent via audio_cb
. The video frame is a frame buffer with the dimensions the same as those defined in retro_get_system_av_info
, one element for each pixel. The pixel format is 0RGB1555
by default, however this is deprecated and RGB565
is recommended - more details can be found in libretro.h
under retro_video_refresh_t
and retro_pixel_format
. A placeholder framebuffer
is defined so that we can generate a blank screen in the libretro
core.
One thing to note is that some front-ends (retroarch, at least) will run at the rate of the audio samples if VSYNC is not enabled, this means without the dummy call to audio_cb
the core would run at the maximum FPS that it could.
retro_reset
just resets the Vectrex.
The pixel format needs to be changed in retro_load_game
or retro_get_system_av_info
, in this case it will be set in retro_get_system_av_info
as all the games will have the same pixel format.
retro_pixel_format pixel_format = RETRO_PIXEL_FORMAT_RGB565;
environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &pixel_format);
This will now give us a libretro
core that will compile and run in retroarch
! It won't do anything useful, but the basis for a working core is there.
To compile and run the core you will need an Ubuntu 14.04 (or similar) machine with binutils
, cmake
and new version of clang
(>=3.5) or g++
(>=4.9).
$ cmake -E make_directory build && cmake -E chdir build cmake ..
$ cmake --build build -- all test # compile and run the tests
$ retroarch -L build/src/vectrexia_libretro.so
When you run the core with retroarch
you will just see a black screen running at ~50fps
Next time I will tackle emulation of the CPU.