MAME Device Basics
Overview
In MAME, a device is a mechanism for encapsulating behavior. While it is common to associate a device (in the MAME sense) with a physical device (in the real world), there does not necessarily need to be a 1:1 correspondance between the two.
Devices are important because they provide clean hooks into the MAME system. They are notified when key things in the system change, they encode their own configuration information, keep their own state, and can be instantiated multiple times within a given machine. They are easily located via a simple string tag, and are first-class citizens in memory maps, so they are easily read from and written to.
As of MAME 0.139, devices are implemented using a collection of C++ classes. In order to provide the flexibility necessary to describe the sorts of devices in MAME, the device model relies heavily on the multiple inheritance feature of C++ to extend devices with one or more device interfaces.
Basic Concepts
Every device in the project is built up out of two classes: a device-specific configuration class (derived from the device_config class) and a device-specific runtime class (derived from the device_t class).
The configuration class is responsible for encapsulating the device's configuration. The base device_config class automatically supports several core configuration properties, such as a short string tag to identify the device instance, a clock value which represents the input clock to the device, and an owner who serves as the device's parent. All device-specific configuration classes must be derived from the device_config class at their root.
Of course, most devices require more configuration than this, and so there are mechanisms for the device-specific configuration class to accept further configuration information, both inline in the MACHINE_CONFIG description, as well as externally in a static structure. This additional configuration data is stored in the device-specific class. More details on how this works come later in this article.
In addition to holding the configuration of a device, the device-specific configuration class also serves as a "factory" class that provides a mechanism for the MAME core to instantiate both new configuration objects, via a static method, and new runtime objects, via a required virtual method. (It is worth noting that the pointer to the static method that constructs configuration objects also serves as the device "type", which is a unique single entry point into the device.)
The runtime class, as its name implies, holds the runtime state of a device. The base device_t class provides a number of basic device concepts, including device initialization, reset, hooks into the save state system, clock scaling. It also holds a reference back to the corresponding device_config that begat the device.
The device-specific runtime class, which is required to derive from device_t, then contains all the runtime state of the device, along with methods to operate upon the live device. It can also override several internal methods of its parent class to gain access to hooks that are called during specific events in the machine's lifecycle.
Lifecycle of a Device
Configuration
Machine configurations in MAME are represented by a tokenizing mechanism wrapped by macros. A typical machine driver looks something like this (having removed some of the irrelevant details):
static MACHINE_DRIVER_START( pacman ) MDRV_CPU_ADD("maincpu", Z80, MASTER_CLOCK/6) MDRV_CPU_PROGRAM_MAP(pacman_map) ... MDRV_SCREEN_ADD("screen", RASTER) MDRV_SCREEN_FORMAT(BITMAP_FORMAT_INDEXED16) ... MDRV_SOUND_ADD("namco", NAMCO, MASTER_CLOCK/6/32) MDRV_SOUND_CONFIG(namco_config) MDRV_SOUND_ROUTE(ALL_OUTPUTS, "mono", 1.0) MACHINE_DRIVER_END
When the compiler processes this, the MDRV_* macros all map down to a set of 32-bit or 64-bit integral tokens which are stored as a stream for later processing.
It may not be immediately obvious, but the machine configuration above defines three separate devices: a CPU device called "maincpu", a video screen device called "screen", and a Namco sound device called "namco". Each device generally defines its own MDRV_*_ADD() macro which permits some flexibility in how each device is added. The MDRV_* macros that follow each device provide configuration information. More on configuration in a later chapter.
When a machine configuration is instantiated, it first takes the token stream and executes it, creating a device configuration whenever it sees an MCONFIG_TOKEN_DEVICE_ADD token (which is output by the MDRV_*_ADD() macro mentioned above), and populating the device configuration with data from subsequent macros.
One of the parameters to MCONFIG_TOKEN_DEVICE_ADD is a device type. In MAME a device type is a static function pointer which serves as the factory function for allocating a device configuration. So when we need to add a device, we simply call the factory function and ask it to allocate for us a new device configuration of the appropriate type.
Once the configuration is allocated, we continue to process tokens. Tokens within a certain well-defined range are known to be device configuration tokens, and these are handed off to the allocated device configuration for processing. Specific devices can also support their own custom-defined tokens if they need special behaviors (the MDRV_SOUND_ROUTE above does this) by overriding the device_process_token() method in the device-specific configuration class.
Upon encountering the end of the token stream, all the devices are notified that the configuration parsing is complete. This allows them to consolidate any configuration information or do any other work that needs to be done. Device-specific configuration classes can override the device_config_complete() method to hook into this event.
There are several situations in which the machine configuration and all the device configurations are created: to perform validity checks on all the drivers; to output information needed by the -listxml and other front-end functions; to check for vector screens when parsing .ini files; and finally, in preparation for starting an actual game. In all cases but the last one, the machine and device configurations are created and discarded without ever creating any runtime devices, so the device lifecycle can very well begin and end with the device configuration.
In the case where validity checks are performed, the device-specific configuration class has the option of performing its own validation by overriding the device_validity_check() method and outputting errors if any are found. For this reason, validation should happen here rather than in the device_config_complete(), so that errors can be reported in a consistent manner.
Runtime
When the time comes to create a running machine object and start up the devices, MAME will take the device list contained in the machine configuration and rip through it to allocate the runtime devices. The mechanism for allocating a runtime device is to call the device-specific configuration class's device_alloc() method, whose job is simply to auto_alloc an instance of the device-specific runtime class. This method is a required override.
Once the entire set of devices has been allocated, MAME will once again run through the list of devices one by one and request them to start. If a device has device-specific work to do at startup (such as allocating memory or consuming configuration information), it can override the device_start() method to do so. If a device has a dependency upon another device being started, and that other device isn't ready yet, you can throw a device_missing_dependencies exception from within the device_start function and you will be re-queued to the end of the initialization order.
An important thing to note is the explicit separation between allocation and startup. All the devices are allocated first, and then all of them are started. The intention is that most of the hard work is done at start time, leaving the class constructor mostly the job of ensuring all local variables are initialized to sane values.
After the devices are all allocated and started, MAME makes one more pass through them all to reset them. As with startup, a device-specific class can override the device_reset() method to hook into this notification. Unlike startup, which occurs exactly once, a reset may happen multiple times throughout a device's existence. In addition to the first call at startup, all devices are also implicitly reset when a soft or hard reset is performed by the user, or when a driver explicitly calls the device's reset() method.
Finally, when the emulation is complete, all the devices are destructed. Note that there is no separation between stopping and destruction, as there is between starting and allocation. This means that your device's destructor is responsible for cleaning up any allocations or other side-effects created during the device's lifetime, excepting those allocated via the auto_alloc macros, which are automatically destroyed shortly afterwards.
In addition to these basic interfaces, there are several other key times when a device is notified. Device-specific hooks are provided for each of these situations, so a simple override is all that is needed to react:
- Prior to saving the state of the system, all the devices are notified. This takes the place of registering handlers via state_save_register_presave() as was done previously. To hook this, simple override the device_pre_save() method.
- Similarly, immediately after loading a saved state, all devices are notified. Device-specific classes hook this via the device_post_load() method.
- If the emulation is started with the debugger enabled, there is a hook device_debug_setup() which is called to allow device-specific classes to register additional functions or other information with the debugger.
- Finally, if the clock of a device is changed, a notification is sent via the device_clock_changed() method. This is necessary because most clock management is handled generically in the base device_t class.
Configuring Devices
Device configuration comes in two flavors, static configuration and inline configuration. The decision as to which to use is fairly arbitrary -- and in fact both can be used at the same time! The example from the Lifecycle of a Device chapter demonstrates the use of both types of configuration:
static MACHINE_DRIVER_START( pacman ) MDRV_CPU_ADD("maincpu", Z80, MASTER_CLOCK/6) MDRV_CPU_PROGRAM_MAP(pacman_map) // inline configuration ... MDRV_SCREEN_ADD("screen", RASTER) MDRV_SCREEN_FORMAT(BITMAP_FORMAT_INDEXED16) // inline configuration ... MDRV_SOUND_ADD("namco", NAMCO, MASTER_CLOCK/6/32) MDRV_SOUND_CONFIG(namco_config) // static configuration MDRV_SOUND_ROUTE(ALL_OUTPUTS, "mono", 1.0) // AND inline configuration MACHINE_DRIVER_END
Let's start by examining how static configuration work, as they are the simplest to understand. A static configuration is simply a single pointer to a constant structure, defined by the game driver, and expressed via the token stream as an MCONFIG_TOKEN_DEVICE_CONFIG token, followed by a pointer to the structure.
Within a machine driver configuration, a static configuration is specified by a MDRV_DEVICE_CONFIG() macro. Certain devices and device types might also provide aliases to this, like the MDRV_SOUND_CONFIG() macro above. If you look at the pacman.c driver, you'll see the configuration structure:
static const namco_interface namco_config = { 3, /* number of voices */ 0 /* stereo */ };
When a driver specifies a static configuration, the pointer is extracted from the token stream and deposited into the void pointer m_static_config, stored by the device_config base class. The driver-specific configuration class can then consume this pointer when its device_config_complete() method is called, or it can leave it around for the device itself to consume when it is later instantiated (see the Best Practices section for recommendations on how to cleanly consume the static configuration).
Standard Interfaces
- Execute
- Memory
- State
- NVRAM
- Disassembly
- Sound