Studying openMSX 0.14 emulator

Introduction

OpenMSX is a super cool MSX 8-bit microcomputer emulator that runs on many operating systems. It is now June 17th, 2018, and I have just finished a rather lengthy studying session of openMSX 0.14 on Fedora Linux 28. OpenMSX source code is freely available from GitHub. It is fantastic, GNU GPL licensed free software and we should be grateful for that.

The first problem I encountered was that the source code is C++ and not C. About ten years ago, out of curiosity I bought Bjarne Stroustrup's C++ Programming Language book. I guess it was the 3rd edition. It was hardcover and I remember it was also some kind of a special version. I am not sure, but I think I read most of it back then. I lent the book to a former bandmate, but unfortunately she never returned it. What is more, ever since reading that book, I have read very little C++ code and written practically none. Yes, I know C language does count as C++, but I am talking about proper C++ including classes and language features that are different and in addition to C.

So to begin with I had to refresh my memory of even many very basic things in C++. Since reading the 3rd edition, C++ has also evolved pretty much. C++ 2011 includes support for e.g. smart pointers, and openMSX makes use of them, so I had to figure out what they are about. But I have written only a few short test programs so far. Mostly I have been reading openMSX source code to see the C++ concepts actually being used in a real-world project of considerable complexity. It seems to me that openMSX is exceptionally clean code and the object-oriented C++ division using classes appears very beautiful. It closely models the division and structure of physical computers this emulator is trying to emulate: ROMs, devices, etc. I see elegance and striving for clarity.

When I started toying with openMSX, the first thing that struck me was its elegant solution to emulate the numerous MSX models there are. You know, there were many manufacturers who built MSX machines and openMSX tries to emulate all of them with minimum effort. OpenMSX developers have come up with a nice solution: For each MSX computer and peripheral, there exists a corresponding file containing a machine description written in XML. For instance, when you start Spectravideo SVI-728 using command:

openmsx -machine Spectravideo_SVI-728

on Fedora Linux 28, openMSX will search for an XML-file called /usr/share/openmsx/machines/Spectravideo_SVI-728.xml. It contains a high level description of Spectravideo SVI-728 MSX computer and it looks like this:

<?xml version="1.0" ?>
<!DOCTYPE msxconfig SYSTEM 'msxconfig2.dtd'>
<msxconfig>

  <info>
    <manufacturer>Spectravideo</manufacturer>
    <code>SVI-728</code>
    <release_year>1983</release_year>
    <description/>
    <type>MSX</type>
  </info>

<!-- 
Info from serial BI 728053932
see http://bilgisayarlarim.com/Spectravideo/SVI-728/
- says "SVI SPECTRAVIDEO 728 HOME COMPUTER" on the left side of the top of the
  casing
- has a small blue MSX logo on the right side of the casing
- has a black 'expansion' connector
- PSG: GI AY-3-8910A
- CPU: Zilog
- PPI: Mitsubishi M5L8255AP-5
- 1 OKI mask ROM

Info from serial BI 728019657
- says "SVI SPECTRAVIDEO 728 HOME COMPUTER" on the left side of the top of the
  casing
- has a small blue MSX logo on the right side of the casing
- has a black 'expansion' connector
- PSG: GI AY-3-8910A
- CPU: SGS Z8400AB1 Z80ACPU 88442-ITALY
- PPI: Toshiba TMP8255AP-5
- VDP: TMS91xx, because it's using TI TMS4416-15NL VRAMs
- mainboard version: 1.4
- SVI-728 POWER & VIDEO: version 1.2
- 1 OKI M38256-71 mask ROM
- RAM chips: 8x National Semiconductor 8505-350 NMC3764N-20
Ident says:
- TMS91xx (no 4/16k effect, no address latch, clones, screen 2 mirrored mode,
  mode 3 mixed mode)
- Z80 scf: 01ED29ED
- Z80 out (c),0: 0 (MSX)
- PSG mask: AY8910

Info from serial BI 728002545
- says "SVI SPECTRAVIDEO 728 PERSONAL COMPUTER" on the left side of the top of
  the casing
- has a large green MSX logo on the top left side of the top of the casing
- has a blue 'expansion' connector
- PSG: GI AY-3-8910A
- CPU: NEC D780C-1 8337XD
- PPI: Mitsubishi M5L8255AP-5
- VDP: TMS91xx, because it's using TI TMS4416-20NL VRAMs
- mainboard version: 1.2
- SVI-728 POWER & VIDEO: version 1.2
- 4 ROM chips, 1 Mitsibishi M5L2764K (A), 1 Hitachi HN482364G-3 (B), 1 Hitachi
  unknown (C) and 1 ..82764G (D)
- RAM chips: 8x Matsushita MN4164P
Ident says:
- TMS91xx (no 4/16k effect, no address latch, clones, screen 2 mirrored mode,
  mode 3 mixed mode)
- Z80 scf: 01C529ED (dunno)
- Z80 out (c),0: 0 (MSX)
- PSG mask: AY8910

At least the latter 2 of these machines have the same BIOS (the one with the
sha1sum below. Strange is that this BIOS says it's a 60Hz interrupt frequency
system, but that is not true. It's definitely 50Hz, so the VDP is TMS9129
indeed.

-->

  <CassettePort/>

  <devices>

    <PPI id="ppi">
      <sound>
        <volume>16000</volume>
      </sound>
      <io base="0xA8" num="4"/>
      <keyboard_type>int</keyboard_type>
      <has_keypad>true</has_keypad>
      <key_ghosting_sgc_protected>true</key_ghosting_sgc_protected>
      <code_kana_locks>false</code_kana_locks>
      <graph_locks>false</graph_locks>
    </PPI>

    <VDP id="VDP">
      <version>TMS9129</version>
      <io base="0x98" num="2"/>
    </VDP>

    <PSG id="PSG">
      <type>AY8910</type>
      <sound>
        <volume>21000</volume>
      </sound>
      <io base="0xA0" num="2" type="O"/>
      <io base="0xA2" num="1" type="I"/>
    </PSG>

    <PrinterPort id="Printer Port">
      <io base="0x90" num="2"/>
    </PrinterPort>

    <primary slot="0">
      <ROM id="MSX BIOS with BASIC ROM">
        <rom>
          <filename>svi-728_basic-bios1.rom</filename>
          <sha1>ea6a82cf8c6e65eb30b98755c8577cde8d9186c0</sha1>
        </rom>
        <mem base="0x0000" size="0x10000"/> <!-- it's mirrored -->
      </ROM>
    </primary>

    <primary slot="1">
      <RAM id="Main RAM">
        <mem base="0x0000" size="0x10000"/>
      </RAM>
    </primary>

    <primary external="true" slot="2"/>

    <primary external="true" slot="3"/>

  </devices>

</msxconfig>

I was extremely interested in finding out how does openMSX construct the emulated machines based on those XML-files. So I have spent many hours with openMSX lately. In this article I will show you some things that I have learned while reading and playing with openMSX source code.

Some important C++ classes

Since we are interested in how openMSX constructs emulated machines, let's get started with some C++ classes that are relevant and important with respect to our investigations. According to my understanding, class MSXMotherBoard in MSXMotherBoard.hh is quite central, because this forms the foundation to add devices to - just like when building physical computers.

File MSXDevice.hh contains class MSXDevice. It serves as a base class for several derived classes such as:

As far as I know, C++ standard libraries do not contain support for parsing XML-files. That is why openMSX derives its XML handling classes from util/rapidsax.{cc,hh}. I do not remember where rapidsax code comes from, but it was not written by the openMSX team.

Code analysis

File main.cc calls code for doing command line parsing:

CommandLineParser parser(reactor);
parser.parse(argc, argv);

CommandLineParser.cc registers -machine command line option:

registerOption("-machine",    machineOption, PHASE_LOAD_MACHINE);

The -machine option is handled by void CommandLineParser::MachineOption::parseOption(const string& option, array_ref<string>& cmdLine):

void CommandLineParser::MachineOption::parseOption(
    const string& option, array_ref<string>& cmdLine)
{
    auto& parser = OUTER(CommandLineParser, machineOption);
    if (parser.haveConfig) {
        throw FatalError("Only one machine option allowed");
    }   
    try {
        parser.reactor.switchMachine(getArgument(option, cmdLine));
    } catch (MSXException& e) {
        throw FatalError(e.getMessage());
    }   
    parser.haveConfig = true;
}

As you can see, C++ 2011 language includes auto keyword. It can be used instead of specifying type. Now C++ compiler deduces the correct type for a variable based on the return type of the following expression.

In my opinion, parser.haveConfig could be called parser.haveMachine to mirror the command line option name -machine, but parser.haveConfig is all right, too.

In the code above, parser.reactor.switchMachine(getArgument(option, cmdLine)); is what we are interested in.

Reactor.cc contains code:

void Reactor::switchMachine(const string& machine)
{
    if (!display) {
#if UNIQUE_PTR_BUG
        display2 = make_unique<Display>(*this);
        display = display2.get();
#else
        display = make_unique<Display>(*this);
#endif
        // TODO: Currently it is not possible to move this call into the
        //       constructor of Display because the call to createVideoSystem()
        //       indirectly calls Reactor.getDisplay().
        display->createVideoSystem();
    }    

    // create+load new machine
    // switch to new machine
    // delete old active machine

    assert(Thread::isMainThread());
    // Note: loadMachine can throw an exception and in that case the
    //       motherboard must be considered as not created at all.
    auto newBoard_ = createEmptyMotherBoard();
    auto* newBoard = newBoard_.get();
    newBoard->loadMachine(machine);
    boards.push_back(move(newBoard_));

    auto* oldBoard = activeBoard;
    switchBoard(newBoard);
    deleteBoard(oldBoard);
}

newBoard->loadMachine(machine); now interests us. Following our earlier example, we know machine variable's value is "Spectravideo_SVI-728".

MSXMotherBoard.cc contains:

string MSXMotherBoard::loadMachine(const string& machine)
{
    assert(machineName.empty());
    assert(extensions.empty());
    assert(!machineConfig2);
    assert(!getMachineConfig());

    try {
        machineConfig2 = HardwareConfig::createMachineConfig(*this, machine);
        setMachineConfig(machineConfig2.get());
    } catch (FileException& e) { 
        throw MSXException("Machine \"" + machine + "\" not found: " +
                           e.getMessage());
    } catch (MSXException& e) { 
        throw MSXException("Error in \"" + machine + "\" machine: " +
                           e.getMessage());
    }    
    try {
        machineConfig->parseSlots();
        machineConfig->createDevices();
    } catch (MSXException& e) { 
        throw MSXException("Error in \"" + machine + "\" machine: " +
                           e.getMessage());
    }    
    if (powerSetting.getBoolean()) {
        powerUp();
    }    
    machineName = machine;
    return machineName;
}

Let's next focus on machineConfig2 = HardwareConfig::createMachineConfig(*this, machine);

It is in config/HardwareConfig.cc:

unique_ptr<HardwareConfig> HardwareConfig::createMachineConfig(
    MSXMotherBoard& motherBoard, const string& machineName)
{
    auto result = make_unique<HardwareConfig>(motherBoard, machineName);
    result->load("machines");
    return result;
}

Then we will look at:

void HardwareConfig::load(string_ref type)
{
    string filename = getFilename(type, hwName);
    setConfig(loadConfig(filename));

    assert(!userName.empty());
    const auto& dirname = FileOperations::getDirName(filename);
    setFileContext(configFileContext(dirname, hwName, userName));
}

Where does hwName variable in string filename = getFilename(type, hwName); come from? From the HardwareConfig object. When the object is created, it is passed to the constructor:

 
HardwareConfig(MSXMotherBoard& motherBoard, std::string hwName);

So in the string filename = getFilename(type, hwName);, type value is "machines" and name value is "Spectravideo_SVI-728"

string HardwareConfig::getFilename(string_ref type, string_ref name)
{
    auto context = systemFileContext();
    try {
        // try <name>.xml
        return context.resolve(FileOperations::join(
            type, name + ".xml"));
    } catch (MSXException& e) {
        // backwards-compatibility:
        //  also try <name>/hardwareconfig.xml
        try {
            return context.resolve(FileOperations::join(
                type, name, "hardwareconfig.xml"));
        } catch (MSXException&) {
            throw e; // signal first error
        }
    }   
}

setConfig(loadConfig(filename)); calls:

XMLElement HardwareConfig::loadConfig(const string& filename)
{
    try {
        LocalFileReference fileRef(filename);
        return XMLLoader::load(fileRef.getFilename(), "msxconfig2.dtd");
    } catch (XMLException& e) {
        throw MSXException(
            "Loading of hardware configuration failed: " +
            e.getMessage());
    }   
}

Let's now focus on return XMLLoader::load(fileRef.getFilename(), "msxconfig2.dtd");. File config/XMLLoader.cc has:

XMLElement load(string_ref filename, string_ref systemID)
{
    MemBuffer<char> buf;
    try {
        File file(filename);
        auto size = file.getSize();
        buf.resize(size + rapidsax::EXTRA_BUFFER_SPACE);
        file.read(buf.data(), size);
        buf[size] = 0;
    } catch (FileException& e) {
        throw XMLException(filename + ": failed to read: " + e.getMessage());
    }   

    XMLElementParser handler;
    try {
        rapidsax::parse<rapidsax::trimWhitespace>(handler, buf.data());
    } catch (rapidsax::ParseError& e) {
        throw XMLException(filename + ": Document parsing failed: " + e.what());
    }   
    auto& root = handler.getRoot();
    if (root.getName().empty()) {
        throw XMLException(filename +
            ": Document doesn't contain mandatory root Element");
    }   
    if (handler.getSystemID().empty()) {
        throw XMLException(filename + ": Missing systemID.\n"
            "You're probably using an old incompatible file format.");
    }   
    if (handler.getSystemID() != systemID) {
        throw XMLException(filename + ": systemID doesn't match "
            "(expected " + systemID + ", got " + handler.getSystemID() + ")\n"
            "You're probably using an old incompatible file format.");
    }   
    return std::move(root);
}

config/HardwareConfig.hh contains a private function:

void setConfig(XMLElement config_) { config = std::move(config_); }

Variable config is private and like this:

XMLElement config;

That is how the emulated machine machineConfig2 is initially created.

But let us return to the function that we already saw in the beginning of our code analysis. Here it is again:

string MSXMotherBoard::loadMachine(const string& machine)
{
    assert(machineName.empty());
    assert(extensions.empty());
    assert(!machineConfig2);
    assert(!getMachineConfig());

    try {
        machineConfig2 = HardwareConfig::createMachineConfig(*this, machine);
        setMachineConfig(machineConfig2.get());
    } catch (FileException& e) { 
        throw MSXException("Machine \"" + machine + "\" not found: " + 
                           e.getMessage());
    } catch (MSXException& e) { 
        throw MSXException("Error in \"" + machine + "\" machine: " + 
                           e.getMessage());
    }    
    try {
        machineConfig->parseSlots();
        machineConfig->createDevices();
    } catch (MSXException& e) { 
        throw MSXException("Error in \"" + machine + "\" machine: " + 
                           e.getMessage());
    }    
    if (powerSetting.getBoolean()) {
        powerUp();
    }    
    machineName = machine;
    return machineName;
}

We have examined the first line of the first try block, so let's next see what setMachineConfig(machineConfig2.get()); does. It is simply like this:

void MSXMotherBoard::setMachineConfig(HardwareConfig* machineConfig_)
{
    assert(!getMachineConfig());
    machineConfig = machineConfig_;

    // make sure the CPU gets instantiated from the main thread
    assert(!msxCpu);
    msxCpu = make_unique<MSXCPU>(*this);
    msxCpuInterface = make_unique<MSXCPUInterface>(*this);
}

Let's move on the second try block:

machineConfig->parseSlots();
machineConfig->createDevices();

We have first:

void HardwareConfig::parseSlots()
{
	// TODO this code does parsing for both 'expanded' and 'external' slots
	//      once machine and extensions are parsed separately move parsing
	//      of 'expanded' to MSXCPUInterface
	//
	for (auto& psElem : getDevices().getChildren("primary")) {
		const auto& primSlot = psElem->getAttribute("slot");
		int ps = CartridgeSlotManager::getSlotNum(primSlot);
		if (psElem->getAttributeAsBool("external", false)) {
			if (ps < 0) {
				throw MSXException(
				    "Cannot mark unspecified primary slot '" +
				    primSlot + "' as external");
			}
			if (psElem->hasChildren()) {
				throw MSXException(
					"Primary slot " + StringOp::toString(ps) +
					" is marked as external, but that would only "
					"make sense if its <primary> tag would be "
					"empty.");
			}
			createExternalSlot(ps);
			continue;
		}
		for (auto& ssElem : psElem->getChildren("secondary")) {
			const auto& secSlot = ssElem->getAttribute("slot");
			int ss = CartridgeSlotManager::getSlotNum(secSlot);
			if ((-16 <= ss) && (ss <= -1) && (ss != ps)) {
				throw MSXException(
					"Invalid secundary slot specification: \"" +
					secSlot + "\".");
			}
			if (ss < 0) {
				if ((ss >= -128) && (0 <= ps) && (ps < 4) &&
				    motherBoard.getCPUInterface().isExpanded(ps)) {
					ss += 128;
				} else {
					continue;
				}
			}
			if (ps < 0) {
				if (ps == -256) {
					ps = getAnyFreePrimarySlot();
				} else {
					ps = getSpecificFreePrimarySlot(-ps - 1);
				}
				auto mutableElem = const_cast<XMLElement*>(psElem);
				mutableElem->setAttribute("slot", StringOp::toString(ps));
			}
			createExpandedSlot(ps);
			if (ssElem->getAttributeAsBool("external", false)) {
				if (ssElem->hasChildren()) {
					throw MSXException(
						"Secondary slot " + StringOp::toString(ps) +
						"-" + StringOp::toString(ss) + " is marked "
						"as external, but that would only make sense "
						"if its <secondary> tag would be empty.");
				}
				createExternalSlot(ps, ss);
			}
		}
	}
}

Functions for creating slots are pretty simple:

void HardwareConfig::createExternalSlot(int ps) 
{
    motherBoard.getSlotManager().createExternalSlot(ps);
    assert(!externalPrimSlots[ps]);
    externalPrimSlots[ps] = true;
}
void HardwareConfig::createExternalSlot(int ps, int ss) 
{
    motherBoard.getSlotManager().createExternalSlot(ps, ss);
    assert(!externalSlots[ps][ss]);
    externalSlots[ps][ss] = true;
}
void HardwareConfig::createExpandedSlot(int ps) 
{
    if (!expandedSlots[ps]) {
        motherBoard.getCPUInterface().setExpanded(ps);
        expandedSlots[ps] = true;
    }   
}

What about machineConfig->createDevices();? It seems to be very important, because it creates devices.

void HardwareConfig::createDevices()
{
    createDevices(getDevices(), nullptr, nullptr);
}

getDevices() refers to <devices> tag that was read from the XML-file /usr/share/openmsx/machines/Spectravideo_SVI-728.xml:

const XMLElement& HardwareConfig::getDevices() const
{
    return getConfig().getChild("devices");
}

Note that we now see a recursive function:

void HardwareConfig::createDevices(const XMLElement& elem,
    const XMLElement* primary, const XMLElement* secondary)
{
    for (auto& c : elem.getChildren()) {
        const auto& childName = c.getName();
        if (childName == "primary") {
            createDevices(c, &c, secondary);
        } else if (childName == "secondary") {
            createDevices(c, primary, &c);
        } else {
            auto device = DeviceFactory::create(
                DeviceConfig(*this, c, primary, secondary));
            if (device) {
                addDevice(move(device));
            } else {
                // device is nullptr, so we are apparently ignoring it on purpose
            }
        }
    }
}

else branch is interesting, because it does the work of creating devices instead of moving in the XML tree.

DeviceConfig(*this, c, primary, secondary) call is what we will inspect next.

File config/DeviceConfig.hh contains the constructor:

DeviceConfig(const HardwareConfig& hwConf_, const XMLElement& devConf_,
                 const XMLElement* primary_, const XMLElement* secondary_)
        : hwConf(&hwConf_), devConf(&devConf_)
        , primary(primary_), secondary(secondary_)
{   
}   

File DeviceFactory.cc contains the following very long, but simple and repetitive function. The result of the DeviceConfig constructor is passed to it:

unique_ptr<MSXDevice> DeviceFactory::create(const DeviceConfig& conf)
{
	unique_ptr<MSXDevice> result;
	const std::string& type = conf.getXML()->getName();
	if (type == "PPI") {
		result = make_unique<MSXPPI>(conf);
	} else if (type == "SVIPPI") {
		result = make_unique<SVIPPI>(conf);
	} else if (type == "RAM") {
		result = make_unique<MSXRam>(conf);
	} else if (type == "VDP") {
		result = make_unique<VDP>(conf);
	} else if (type == "E6Timer") {
		result = make_unique<MSXE6Timer>(conf);
	} else if (type == "HiResTimer") {
		result = make_unique<MSXHiResTimer>(conf);
	} else if (type == "ResetStatusRegister" || type == "F4Device") {
		result = make_unique<MSXResetStatusRegister>(conf);
	} else if (type == "TurboRPause") {
		result = make_unique<MSXTurboRPause>(conf);
	} else if (type == "TurboRPCM") {
		result = make_unique<MSXTurboRPCM>(conf);
	} else if (type == "S1985") {
		result = make_unique<MSXS1985>(conf);
	} else if (type == "S1990") {
		result = make_unique<MSXS1990>(conf);
	} else if (type == "ColecoJoystick") {
		result = make_unique<ColecoJoystickIO>(conf);
	} else if (type == "PSG") {
		result = make_unique<MSXPSG>(conf);
	} else if (type == "SVIPSG") {
		result = make_unique<SVIPSG>(conf);
	} else if (type == "SNPSG") {
		result = make_unique<SNPSG>(conf);
	} else if (type == "MSX-MUSIC") {
		result = make_unique<MSXMusic>(conf);
	} else if (type == "MSX-MUSIC-WX") {
		result = make_unique<MSXMusicWX>(conf);
	} else if (type == "FMPAC") {
		result = make_unique<MSXFmPac>(conf);
	} else if (type == "MSX-AUDIO") {
		result = make_unique<MSXAudio>(conf);
	} else if (type == "MusicModuleMIDI") {
		result = make_unique<MC6850>(conf);
	} else if (type == "FACMIDIInterface") {
		result = make_unique<MSXFacMidiInterface>(conf);
	} else if (type == "YamahaSFG") {
		result = make_unique<MSXYamahaSFG>(conf);
	} else if (type == "MoonSound") {
		result = make_unique<MSXMoonSound>(conf);
	} else if (type == "OPL3Cartridge") {
		result = make_unique<MSXOPL3Cartridge>(conf);
	} else if (type == "Kanji") {
		result = make_unique<MSXKanji>(conf);
	} else if (type == "Bunsetsu") {
		result = make_unique<MSXBunsetsu>(conf);
	} else if (type == "MemoryMapper") {
		result = make_unique<MSXMemoryMapper>(conf);
	} else if (type == "PanasonicRAM") {
		result = make_unique<PanasonicRam>(conf);
	} else if (type == "RTC") {
		result = make_unique<MSXRTC>(conf);
	} else if (type == "PasswordCart") {
		result = make_unique<PasswordCart>(conf);
	} else if (type == "ROM") {
		result = RomFactory::create(conf);
	} else if (type == "PrinterPort") {
		result = make_unique<MSXPrinterPort>(conf);
	} else if (type == "SVIPrinterPort") {
		result = make_unique<SVIPrinterPort>(conf);
	} else if (type == "SCCplus") { // Note: it's actually called SCC-I
		result = make_unique<MSXSCCPlusCart>(conf);
	} else if ((type == "WD2793") || (type == "WD1770")) {
		result = createWD2793BasedFDC(conf);
	} else if (type == "Microsol") {
		conf.getCliComm().printWarning(
			"Microsol as FDC type is deprecated, please update "
			"your config file to use WD2793 with connectionstyle "
			"Microsol!");
		result = make_unique<MicrosolFDC>(conf);
	} else if (type == "MB8877A") {
		conf.getCliComm().printWarning(
			"MB8877A as FDC type is deprecated, please update your "
			"config file to use WD2793 with connectionstyle National!");
		result = make_unique<NationalFDC>(conf);
	} else if (type == "TC8566AF") {
		result = make_unique<TurboRFDC>(conf);
	} else if (type == "SVIFDC") {
		result = make_unique<SVIFDC>(conf);
	} else if (type == "BeerIDE") {
		result = make_unique<BeerIDE>(conf);
	} else if (type == "SunriseIDE") {
		result = make_unique<SunriseIDE>(conf);
	} else if (type == "GoudaSCSI") {
		result = make_unique<GoudaSCSI>(conf);
	} else if (type == "MegaSCSI") {
		result = make_unique<MegaSCSI>(conf);
	} else if (type == "ESERAM") {
		result = make_unique<ESE_RAM>(conf);
	} else if (type == "WaveSCSI") {
		result = make_unique<ESE_SCC>(conf, true);
	} else if (type == "ESESCC") {
		result = make_unique<ESE_SCC>(conf, false);
	} else if (type == "Matsushita") {
		result = make_unique<MSXMatsushita>(conf);
	} else if (type == "VictorHC9xSystemControl") {
		result = make_unique<MSXVictorHC9xSystemControl>(conf);
	} else if (type == "CielTurbo") {
		result = make_unique<MSXCielTurbo>(conf);
	} else if (type == "Kanji12") {
		result = make_unique<MSXKanji12>(conf);
	} else if (type == "MSX-MIDI") {
		result = make_unique<MSXMidi>(conf);
	} else if (type == "MSX-RS232") {
		result = make_unique<MSXRS232>(conf);
	} else if (type == "MegaRam") {
		result = make_unique<MSXMegaRam>(conf);
	} else if (type == "PAC") {
		result = make_unique<MSXPac>(conf);
	} else if (type == "HBI55") {
		result = make_unique<MSXHBI55>(conf);
	} else if (type == "DebugDevice") {
		result = make_unique<DebugDevice>(conf);
	} else if (type == "V9990") {
		result = make_unique<V9990>(conf);
	} else if (type == "Video9000") {
		result = make_unique<Video9000>(conf);
	} else if (type == "ADVram") {
		result = make_unique<ADVram>(conf);
	} else if (type == "PioneerLDControl") {
#if COMPONENT_LASERDISC
		result = make_unique<PioneerLDControl>(conf);
#else
		throw MSXException("Laserdisc component not compiled in");
#endif
	} else if (type == "Nowind") {
		result = make_unique<NowindInterface>(conf);
	} else if (type == "Mirror") {
		result = make_unique<MSXMirrorDevice>(conf);
	} else if (type == "SensorKid") {
		result = make_unique<SensorKid>(conf);
	} else if (type == "FraelSwitchableROM") {
		result = make_unique<FraelSwitchableROM>(conf);
	} else if (type == "ChakkariCopy") {
		result = make_unique<ChakkariCopy>(conf);
	} else if (type == "MegaFlashRomSCCPlusSD") {
		result = make_unique<MegaFlashRomSCCPlusSD>(conf);
	} else if (type == "MusicalMemoryMapper") {
		result = make_unique<MusicalMemoryMapper>(conf);
	} else if (type == "Carnivore2") {
		result = make_unique<Carnivore2>(conf);
	} else if (type == "T9769") {
		// Ignore for now. We might want to create a real device for it later.
	} else {
		throw MSXException("Unknown device \"" + type +
		                   "\" specified in configuration");
	}
	if (result) result->init();
	return result;
}

I will not examine all relevant cases for Spectravideo SVI-728, but let's focus on "ROM" type next:

} else if (type == "ROM") {
        result = RomFactory::create(conf);

Now the interesting file is memory/RomFactory.cc. I will show only parts of unique_ptr<MSXDevice> create(const DeviceConfig& config):

unique_ptr<MSXDevice> create(const DeviceConfig& config)
{
    Rom rom(config.getAttribute("id"), "rom", config);

    // Get specified mapper type from the config.
    RomType type;
    // if no type is mentioned, we assume 'mirrored' which works for most
    // plain ROMs...
    string_ref typestr = config.getChildData("mappertype", "Mirrored");
    if (typestr == "auto") {
        // Guess mapper type, if it was not in DB.
        const RomInfo* romInfo = config.getReactor().getSoftwareDatabase().fetchRomInfo(rom.getOriginalSHA1());
        if (!romInfo) {
            auto machineType = config.getMotherBoard().getMachineType();
            if (machineType == "Coleco") {
                type = ROM_PAGE23;
            } else {
                type = guessRomType(rom);
            }
        } else {
            type = romInfo->getRomType();
        }
    } else {
        // Use mapper type from config, even if this overrides DB.
        type = RomInfo::nameToRomType(typestr);
        if (type == ROM_UNKNOWN) {
            throw MSXException("Unknown mappertype: " + typestr);
        }
    }   

The first line Rom rom(config.getAttribute("id"), "rom", config); does a whole lot although it seems simple. That line calls Rom class constructor with three arguments (const string& id has a default value {} declared in memory/Rom.hh).

See file memory/Rom.cc for this:

Rom::Rom(string name_, string description_,
         const DeviceConfig& config, const string& id /*= {}*/)
    : name(std::move(name_)), description(std::move(description_))
{
    // Try all <rom> tags with matching "id" attribute.
    string errors;
    for (auto& c : config.getXML()->getChildren("rom")) {
        if (c->getAttribute("id", {}) == id) {
            try {
                init(config.getMotherBoard(), *c, config.getFileContext());
                return;
            } catch (MSXException& e) {
                // remember error message, and try next
                if (!errors.empty() && (errors.back() != '\n')) {
                    errors += '\n';
                }
                errors += e.getMessage();
            }   
        }   
    }   
    if (errors.empty()) {
        // No matching <rom> tag.
        StringOp::Builder err;
        err << "Missing <rom> tag";
        if (!id.empty()) {
            err << " with id=\""<< id << '"';
        }
        throw ConfigException(err);
    } else {
        // We got at least one matching <rom>, but it failed to load.
        // Report error messages of all failed attempts.
        throw ConfigException(errors);
    }   
}

The crucial line is init(config.getMotherBoard(), *c, config.getFileContext());. However, the called init() function is very long and tedious, so I will not show it here. Suffice it to say that it tries to find the system ROM based on SHA1 hash, for example. And in my case it will find it as /home/kalevi/.openMSX/share/systemroms/svi-728_basic-bios1.rom. It will load the ROM contents to rom defined in memory/Rom.hh as private member:

const byte* rom;

unique_ptr<MSXDevice> create(const DeviceConfig& config) in memory/RomFactory.cc also contains code:

  unique_ptr<MSXRom> result;
    switch (type) {
    case ROM_MIRRORED:
        result = make_unique<RomPlain>(
            config, move(rom), RomPlain::MIRRORED);
        break;

And at the end:

return move(result);

Our Spectravideo SVI-728 system ROM is of type ROM_MIRRORED so file memory/RomPlain.cc interests us. It contains the constructor (int start has a default value -1 declared in memory/RomPlain.hh):

RomPlain::RomPlain(const DeviceConfig& config, Rom&& rom_,
                   MirrorType mirrored, int start)
	: Rom8kBBlocks(config, std::move(rom_))
{
	unsigned windowBase =  0x0000;
	unsigned windowSize = 0x10000;
	if (auto* mem = config.findChild("mem")) {
		windowBase = mem->getAttributeAsInt("base");
		windowSize = mem->getAttributeAsInt("size");
	}

	unsigned romSize = rom.getSize();
	if ((romSize > 0x10000) || (romSize & 0x1FFF)) {
		throw MSXException(StringOp::Builder() << rom.getName() <<
		    ": invalid rom size: must be smaller than or equal to 64kB "
		    "and must be a multiple of 8kB.");
	}

	unsigned romBase = (start == -1)
	                 ? guessLocation(windowBase, windowSize)
	                 : unsigned(start);
	if ((start == -1) &&
	    (!isInside(romBase,               windowBase, windowSize) ||
	     !isInside(romBase + romSize - 1, windowBase, windowSize))) {
		// ROM must fall inside the boundaries given by the <mem>
		// tag (this code only looks at one <mem> tag), but only
		// check when the start address was not explicitly specified
		throw MSXException(StringOp::Builder() << rom.getName() <<
		    ": invalid rom position: interval " <<
		    toString(romBase, romSize) << " must fit in " << 
		    toString(windowBase, windowSize) << '.');
	}
	if ((romBase & 0x1FFF)) {
		throw MSXException(StringOp::Builder() << rom.getName() <<
		    ": invalid rom position: must start at a 8kB boundary.");
	}

	unsigned firstPage = romBase / 0x2000;
	unsigned numPages = romSize / 0x2000;
	for (unsigned page = 0; page < 8; ++page) {
		unsigned romPage = page - firstPage;
		if (romPage < numPages) {
			setRom(page, romPage);
		} else {
			if (mirrored == MIRRORED) {
				setRom(page, romPage & (numPages - 1));
			} else {
				setUnmapped(page);
			}
		}

	}
}

Some parts of that constructor are clear to me, but I am not sure about many details.

But let's look at the earlier recursive createDevices() function again. We just examined the DeviceFactory::create() code.

void HardwareConfig::createDevices(const XMLElement& elem,
    const XMLElement* primary, const XMLElement* secondary)
{
    for (auto& c : elem.getChildren()) {
        const auto& childName = c.getName();
        if (childName == "primary") {
            createDevices(c, &c, secondary);
        } else if (childName == "secondary") {
            createDevices(c, primary, &c);
        } else {
            auto device = DeviceFactory::create(
                DeviceConfig(*this, c, primary, secondary));
            if (device) {
                addDevice(move(device));
            } else {
                // device is nullptr, so we are apparently ignoring it on purpose
            }
        }
    }
}

Next we will focus on addDevice(move(device));. In fact std::move() is new C++ 2011 code and, according to my understanding, it is used to pass the std::unique_ptr smart pointer ownership.

void HardwareConfig::addDevice(std::unique_ptr<MSXDevice> device)
{
    motherBoard.addDevice(*device);
    devices.push_back(move(device));
}

File MSXMotherBoard.cc contains simply this:

void MSXMotherBoard::addDevice(MSXDevice& device)
{
    availableDevices.push_back(&device);
}

File MSXMotherBoard.hh has a private member:

std::vector<MSXDevice*> availableDevices; // no ownership, no order

File config/HardwareConfig.hh has a private member:

std::vector<std::unique_ptr<MSXDevice>>devices;

I actually rushed a little bit and forgot to analyze the end of unique_ptr<MSXDevice> DeviceFactory::create(const DeviceConfig& conf):

if (result) result->init();

So the unique_ptr<MSXDevice> result contains function init() and it is called. It is declared in MSXDevice.hh:

protected:
    /** Every MSXDevice has a config entry; this constructor gets
      * some device properties from that config entry.
      * @param config config entry for this device.
      * @param name The name for the MSXDevice (will be made unique)
      */
    MSXDevice(const DeviceConfig& config, const std::string& name);
    explicit MSXDevice(const DeviceConfig& config);

    /** Constructing a MSXDevice is a 2-step process, after the constructor
      * is called this init() method must be called. The reason is exception
      * safety (init() might throw and we use the destructor to clean up
      * some stuff, this is more difficult when everything is done in the
      * constrcutor).
      * This is also a non-public method. This means you can only construct
      * MSXDevices via DeviceFactory.
      * In rare cases you need to override this method, for example when you
      * need to access the referenced devices already during construction
      * of this device (e.g. ADVram)
      */
    friend class DeviceFactory;
    virtual void init();

Keyword virtual means that derived classes can override this init() function, but it is not a pure virtual function that must be defined in the derived C++ classes.

File MSXDevice.cc has:

void MSXDevice::init()
{
    staticInit();

    lockDevices();
    registerSlots();
    registerPorts();
}

Our ROM creation did not override init() so the default function MSXDevice::init() gets called.

staticInit(); calls:

void MSXDevice::staticInit()
{
    static bool alreadyInit = false;
    if (alreadyInit) return;
    alreadyInit = true;

    memset(unmappedRead, 0xFF, sizeof(unmappedRead));
}
byte MSXDevice::unmappedRead[0x10000];

lockDevices() calls:

void MSXDevice::lockDevices()
{
    // This code can only handle backward references: the thing that is
    // referenced must already be instantiated, we don't try to change the
    // instantiation order. For the moment this is good enough (only ADVRAM
    // (an extension) uses it to refer to the VDP (inside a machine)). If
    // needed we can implement something more sophisticated later without
    // changing the format of the config files.
    for (auto& c : getDeviceConfig().getChildren("device")) {
        const auto& name = c->getAttribute("idref");
        auto* dev = getMotherBoard().findDevice(name);
        if (!dev) {
            throw MSXException(
                "Unsatisfied dependency: '" + getName() +
                "' depends on unavailable device '" +
                name + "'.");
        }
        references.push_back(dev);
        dev->referencedBy.push_back(this);
    }
}

registerSlots() calls:

void MSXDevice::registerSlots()
{
	MemRegions tmpMemRegions;
	for (auto& m : getDeviceConfig().getChildren("mem")) {
		unsigned base = m->getAttributeAsInt("base");
		unsigned size = m->getAttributeAsInt("size");
		if ((base >= 0x10000) || (size > 0x10000) || ((base + size) > 0x10000)) {
			throw MSXException(
				"Invalid memory specification for device " +
				getName() + " should be in range "
				"[0x0000,0x10000).");
		}
		tmpMemRegions.emplace_back(base, size);
	}
	if (tmpMemRegions.empty()) {
		return;
	}

	// find primary and secondary slot specification
	auto& slotManager = getMotherBoard().getSlotManager();
	auto* primaryConfig   = getDeviceConfig2().getPrimary();
	auto* secondaryConfig = getDeviceConfig2().getSecondary();
	if (primaryConfig) {
		ps = slotManager.getSlotNum(primaryConfig->getAttribute("slot"));
	} else {
		throw MSXException("Invalid memory specification");
	}
	if (secondaryConfig) {
		const auto& ss_str = secondaryConfig->getAttribute("slot");
		ss = slotManager.getSlotNum(ss_str);
		if ((-16 <= ss) && (ss <= -1) && (ss != ps)) {
			throw MSXException(
				"Invalid secundary slot specification: \"" +
				ss_str + "\".");
		}
	} else {
		ss = 0;
	}

	// This is only for backwards compatibility: in the past we added extra
	// attributes "primary_slot" and "secondary_slot" (in each MSXDevice
	// config) instead of changing the 'any' value of the slot attribute of
	// the (possibly shared) <primary> and <secondary> tags. When loading
	// an old savestate these tags can still occur, so keep this code. Also
	// remove these attributes to convert to the new format.
	const auto& config = getDeviceConfig();
	if (config.hasAttribute("primary_slot")) {
		auto& mutableConfig = const_cast<XMLElement&>(config);
		const auto& primSlot = config.getAttribute("primary_slot");
		ps = slotManager.getSlotNum(primSlot);
		mutableConfig.removeAttribute("primary_slot");
		if (config.hasAttribute("secondary_slot")) {
			const auto& secondSlot = config.getAttribute("secondary_slot");
			ss = slotManager.getSlotNum(secondSlot);
			mutableConfig.removeAttribute("secondary_slot");
		}
	}

	// decode special values for 'ss'
	if ((-128 <= ss) && (ss < 0)) {
		if ((0 <= ps) && (ps < 4) &&
		    getCPUInterface().isExpanded(ps)) {
			ss += 128;
		} else {
			ss = 0;
		}
	}

	// decode special values for 'ps'
	if (ps == -256) {
		slotManager.getAnyFreeSlot(ps, ss);
	} else if (ps < 0) {
		// specified slot by name (carta, cartb, ...)
		slotManager.getSpecificSlot(-ps - 1, ps, ss);
	} else {
		// numerical specified slot (0, 1, 2, 3)
	}
	assert((0 <= ps) && (ps <= 3));

	if (!getCPUInterface().isExpanded(ps)) {
		ss = -1;
	}

	// Store actual slot in config. This has two purposes:
	//  - Make sure that devices that are grouped under the same
	//    <primary>/<secondary> tags actually use the same slot. (This
	//    matters when the value of some of the slot attributes is "any").
	//  - Fix the slot number so that it remains the same after a
	//    savestate/loadstate.
	assert(primaryConfig);
	primaryConfig->setAttribute("slot", StringOp::toString(ps));
	if (secondaryConfig) {
		string slot = (ss == -1) ? "X" : StringOp::toString(ss);
		secondaryConfig->setAttribute("slot", slot);
	} else {
		if (ss != -1) {
			throw MSXException(
				"Missing <secondary> tag for device" +
				getName());
		}
	}

	int logicalSS = (ss == -1) ? 0 : ss;
	for (auto& r : tmpMemRegions) {
		getCPUInterface().registerMemDevice(
			*this, ps, logicalSS, r.first, r.second);
		memRegions.push_back(r);
	}

	// Mark the slot as 'in-use' so that future searches for free external
	// slots don't return this slot anymore. If the slot was not an
	// external slot, this call has no effect. Multiple MSXDevices from the
	// same extension (the same HardwareConfig) can all allocate the same
	// slot (later they should also all free this slot).
	slotManager.allocateSlot(ps, ss, getHardwareConfig());
}

registerPorts() calls:

void MSXDevice::registerPorts()
{
	for (auto& i : getDeviceConfig().getChildren("io")) {
		unsigned base = i->getAttributeAsInt("base");
		unsigned num  = i->getAttributeAsInt("num");
		const auto& type = i->getAttribute("type", "IO");
		if (((base + num) > 256) || (num == 0) ||
		    ((type != "I") && (type != "O") && (type != "IO"))) {
			throw MSXException("Invalid IO port specification");
		}
		for (unsigned port = base; port < base + num; ++port) {
			if ((type == "I") || (type == "IO")) {
				getCPUInterface().register_IO_In(port, this);
				inPorts.push_back(port);
			}
			if ((type == "O") || (type == "IO")) {
				getCPUInterface().register_IO_Out(port, this);
				outPorts.push_back(port);
			}
		}
	}
}

Conclusion

OpenMSX code appears to me as clear and logical. I did not understand every single line in the code analysis presented above, but I am confident I now know much more than before. I want to thank the openMSX developer team and MSX-community for this great emulator.

Kalevi Kolttonen <kalevi@kolttonen.fi>