Vectrex Emulator: libretro

  • emulationemulation
  • retroretro
  • codecode

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.

  1. Writing an Emulator: Introduction
  2. Vectrex Emulator: libretro

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.

Design

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.

classDiagram Vectrex -- Catridge Vectrex -- M6809 Vectrex -- VIA6522 Vectrex -- AY38912 Vectrex -- VectorDrawer class Vectrex{ Catridge cartridge M6809 cpu VIA6522 via AY38912 psg VectorDrawer vector_drawer } class Catridge{ } class M6809{ } class VIA6522 { } class AY38912{ } class VectorDrawer{ }

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

libretro
libretro

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 emoji-smile

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.

ROM Loading

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

 

Run the ROM

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 emoji-smile

Next time I will tackle emulation of the CPU.

beardypig
Copyright © beardypig 2015 - 2023
A blog mostly about stuff that I enjoy making or doing, expect posts about retro gaming, emulation, programming and electronics!