Developer’s Guide

This guide describes the structure and inner workings of the platform. After reading this guide, you will be able to create your own components and systems.

Tip

Class names are also links to their respective documentation; just click 🔘👈 them to be redirected. Or you can check out the API docs yourself. The code documentation is always auto-generated from the latest commit to the main branch.

Note

If you understand Czech, you can read a bachelor’s thesis for which the project was developed and describes not only the project, but also a matter of emulation and the NES console in a great depth.

USE as a platform consists of multiple classes.

Internal classes

The main class represeting whole application is Emulator. It takes care of configuring the rendering backend and other app configurations, renders global window, loads a chosen System, configures sound output (class Sound), runs the emulation and passes sound output from System to Sound. Class Sound uses miniaudio library and handles all of the sound processing, including buffering and low-pass filtering – you do not have to worry about anything. There are also some tools included in the Tools.cpp file. These were the platform’s “internal” classes/files.

Component

The most important class, where implementation of all the emulated components takes place, is the Component. This abstract class is a common interface for all of the components – everything is a component: bus, memory, CPU… Before anything else, you usually want to implement a component, see Creating your own component.

Every component usually needs to communicate with other components, so you will need to define an interface. Luckily for you, universal communication interface for every component is already present in the platform. The interface consists of two parts: Port and Connector. See Port and Connector to understand how they work.

Creating your own component

To create your own component, you need to inherit from the Component class. Do not forget to separate class declaration (belongs to header file) and definitions (belongs to cpp file). Then you need to implement these three methods:

  • Component::init(): this method is usually called on startup or reset of a system. Define a proper default power-on state of your component here.

  • Component::getGUIs(): in this method you implement a GUI for your component using Dear ImGui library. Do not worry, it is very easy.

  • A constructor of your class. There you define ports and connectors, see Port and Connector and set a name your component: m_deviceName = "myName"

Optionally, you may override other methods, see Component to find out which are available. You may be also interested in Keyboard input handling and Playing sounds.

You can use this template for your component’s class declaration. Do not hesitate to check other components for inspiration.

class myComponent : public Component{

private:
    // There you put your ports and internal data.

public:
    myComponent(/*here you can put some parameters*/);

    // Power-on state of your component is implemented here (resetting registers, memories...).
    void init() override;
    // GUI rendering is defined here.
    std::vector<EmulatorWindow> getGUIs() override;
};

And here’s a template for Component::getGUIs() method:

std::vector<EmulatorWindow> myComponent::getGUIs() {

    std::function<void(void)> myWindow1 = [this](){

        // Window contents
        // ===================================================================
        ImGui::SeparatorText("Registers");
        ImGui::Text("Register 1: 0x%x", m_reg1);
        ImGui::Text("Register 2: 0x%x", m_reg2);
        ImGui::Separator();
        ImGui::Checkbox("Allow writing", &m_writingAllowed);
    };

    return {
            EmulatorWindow{
                    // This will be shown as a prefix in square brackets: [myComponentName].
                    // Usually it is a name of your component.
                    .category = m_deviceName,
                    // Title which follows after the prefix.
                    .title = "myWindow1",
                    // Unique ID, you can leave this default.
                    .id    = getDeviceID(),
                    // Where do you want your window to be docked. See Types.h for available docks.
                    .dock  = DockSpace::LEFT,
                    // Pass the rendering lambda you defined above.
                    .guiFunction = myWindow1
            },
            /*
            optionally another EmulatorWindow {...} and another and another...
            */
    };
}

Tip

You can define as many lambdas as you wish, do not forget to add them to the return statement. More sophisticated examples are in the components, check for example Memory.cpp to see how to show modals and file pickers, or just check out Dear ImGui to see which widgets are available.

Keyboard input handling

If your component wants to interact with any keys or gamepad buttons, you need to also override Component::getInputs() method. In this function, you return a vector of “actions”. For example, if your components represents a gamepad that contains two buttons (left and right), you define two actions:

std::vector<ImInputBinder::action_t> myComponent::getInputs() {
    return {
        ImInputBinder::action_t {
            .name_id = "Left Button",
            .key     = ImGuiKey_LeftArrow,
            .pressCallback = [&](){ m_controller.setLeft(true); },
            .releaseCallback = [&](){ m_controller.setLeft(false); },
        },
        ImInputBinder::action_t {
            .name_id = "Right Button",
            .key     = ImGuiKey_RightArrow,
            .pressCallback = [&](){ m_controller.setRight(true); },
            .releaseCallback = [&](){ m_controller.setRight(false); },
        },
    }
}

Note

Assigned buttons can be changed later by the user, which is saved automatically to a configuration file. The platform uses ImInputBinder library to handle user inputs, check it out to find more. Also, keys and buttons are identified by the Dear ImGui’s names, check the ImGui’s documentation to find a list of the available keys.

Playing sounds

If your component wants to play some sounds, you need to override Component::getSoundSampleSources(). This functions returns a vector of functions which return normalized float amplitude value [-1,1] (miniaudio’s ma_format_f32) for two channels (left and right). If your component has only a mono output, return the same value for both channels. Here is an example of the method of a component which has only a single sound source:

SoundSampleSources myComponent::getSoundSampleSources() {
    return {
      [&](){
          SoundStereoFrame frame {leftOutput(), rightOutput()};
          return frame;
      }
    };
}

Tip

There can be as many sound sources as you wish, they will be mixed automatically. Note that you only need to return two floats, that’s all. All synchronization, buffering and so on will be done automatically as well. Just don’t forget to define correct main clock value.

Port and Connector

When two components communicate, usually there is a component which controls the communication (active) and the other component which is communicated with (passive). For example, when a CPU wants to write something to the memory, the CPU is the active one and the memory passive. However, if there is for example a button connected directly to the CPU (for example to trigger an interrupt), the button is now the active and the CPU is the passive component. To sum up, in a communication, there is always one component (active) which sends something to the other one (passive).

Port

If your component wants to control any other component, you have to provide a Port. In the code you then interface with the port, which represents whatever component is connected to the port. To provide a port, you usually place a selected type of the port as a data member to your class:

class myComponent : public Component {
// ...
private:
    DataPort m_mainBus;
    SignalPort m_INT;
// ...
}

Then to make magic happen you have to place your ports to the internal port map in the constructor:

myComponent::myComponent() {
    m_ports["mainBus"] = &m_mainBus;
    m_ports["INT"]     = &m_INT;
}

That it is. Now your component has a platform-compatible interface defined. To use the interface in your code, you can use functions which offers your selected port type.

DataPort offers methods for reading and writing the data, SignalPort offers two methods: SignalPort::set() to set a logical voltage value (high, low) of a pin of a connected device, SignalPort::send() is used to send a simple pulse (where a specific value is not important, e.g. clock pulses). Check classes DataPort and SignalPort for signatures of available methods.

Connector

If you want to be your component controlled by any other, you have to specify a Connector. Connectors are usually declared and defined directly in the constructor of your component. There are two types of interfaces for your connector: DataInterface and SignalInterface. Depending on chosen interface the connector will be compatible with different port types:

Connector interface and Port compabitility

Connector interface

Port

DataInterface

DataPort

SignalInterface

SignalPort

Here are some examples. First a demonstration of SignalInterface.

myComponent::myComponent() {

     m_connectors["myConnector1"] = std::make_shared<Connector>(SignalInterface{
         .send = [this]() {
             // Do something when someone sends a pulse.
         },
         .set = [this](bool active) {
             if (active) {
                 // Do something when someone sets a pin value to 1 (high).
             } else {
                 // Do something when someone sets a pin value to 0 (low).
             }
         }
     });
}

As you can see, SignalInterface offers two methods. Method SignalInterface::set is meant to be used to set a logic level of a certain pin. Method SignalInterface::send is meant to send only a pulse of a “truthy” logic value where you only need to trigger an action and specific value is not important (e.g. clock pins). You can choose to implement only one method and leave the second unimplemented.

Now an example of DataInterface:

// This example component has a single data interface represented by "myConnector".
// It contains register at 0x0020 available for reading and writing.
// Register at 0x0021 is write-only.
m_connectors["myConnector"] = std::make_shared<Connector>( DataInterface {

    .read = [&](uint32_t address, uint32_t & buffer) -> bool {
        if (address == 0x0020) {
            buffer = m_register20;
            return true;
        } else {
            return false;
        }
    },
    .write = [&](uint32_t address, uint32_t data) {

        if (address == 0x0020) {
            m_register20 = data;
        } else if (address == 0x0021) {
            m_register21 = data;
        }
    }
});

DataInterface offers also two methods. First method DataInterface::read() is used by other component to read something at specified address from your component. You either write value you want to respond with to the buffer parameter and return true or do not write anything and return false (when your component do not respond to that particular address). Second method DataInterface::write() is used to write something to your component. If your component is mapped to the address specified, you can take the value in data and do whatever with it, if your component is not mapped, do not do anything.

Warning

Please be aware that DataInterface’s method can be called even when the communication is not meant for your component. For example if your component is connected to a Bus, everything is written to every component just like in a real bus and it is up to the component whether to care about the written data. It is the same with reading. In a case of a simple bus like Bus class, every component is tried to read from and the first component which responds to the provided address “wins” and its value is taken for the next processing.

System

After you create required components, you may combine them into a system. To create a component, you need to inherit from System class. Then you implement five methods:

  • System::doClocks(): here you define what to do when a main clock signal is sent to your system.

  • System::doSteps(): here you proceed as many clock as needed to process a single CPU instruction (if CPU available, otherwise leave blank).

  • System::doFrames(): here you proceed as many clock as needed to process a single video frame (if video output available, otherwise leave blank).

  • System::doRun(): this is called when the user wants to keep the emulation running in the real time.

  • A constructor of your class.

In the constructor you need to define several things:

  • System::m_systemName: system’s name,

  • System::m_systemClockRate: system’s main clock rate,

  • interconnect your components,

  • push all the components to the base class container: m_components.push_back(&m_myComponent);,

  • if there is any audio output, push it to the sample sources: m_sampleSources.push_back(myComponent.getSoundSampleSources());

To implement Component::doRun() you can use this template to properly calculate how many clock to proceed:

void mySystem::doRun(unsigned int updateFrequency) {
    // Calculate how many clocks to run based on function call interval.
    unsigned int remainingClocks = m_systemClockRate / updateFrequency;
    doClocks(remainingClocks);
}

Interconnecting the components

You can check out the NES systems constructor in NES.cpp to see an example.

Adding the system to the plaform

When the system is finished, you can integrate it to the rest of the platform.

At first, in the Emulator.h, add a new value to the Emulator::SYSTEMS enum. Then, in Emulator.cpp, include your new system at the top of the file: #include "systems/mySystem.h" and add a new row to the menu in Emulator::guiMenuItems() like this:

// ... other systems ...
else if(ImGui::MenuItem("mySystem", nullptr, m_systemID == SYSTEMS::MYSYSTEM)) {

            loadSystem(std::make_unique<MYSYSTEM>());
            m_systemID = SYSTEMS::MYSYSTEM;
}

🎉 THAT’S IT 🎉

Congratulations, you now have a working emulation of your system. Have fun!

If everything is good, do not forget to leave a star ⭐ at GitHub. If something isn’t working 🪲, please create an issue.