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.

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.

Core 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

This section aims to describe the two core device classes and how they are used by the system.

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).

Inline configurations, by contrast, don't require an external structure. Instead, all the information needed to configure the device is specified inline via the machine configuration macros. The way this works is that the base device_config class has a small array m_inline_data[] of 64-bit values, which can be populated via the MCONFIG_TOKEN_DEVICE_INLINE_DATA* tokens. Each token specifies an index in the array, along with a 16-bit, 32-bit, or 64-bit data value to be stored there.

The raw macros used to emit the tokens that specify inline data look like this:

MDRV_DEVICE_INLINE_DATA16(index, data)
MDRV_DEVICE_INLINE_DATA32(index, data)
MDRV_DEVICE_INLINE_DATA64(index, data)

Note that the size (16, 32, 64) reflects the number of bits needed to hold the maximum data value. Regardless of the size specified here, the m_inline_data[] array is always an array of 64-bit values. Care should be used if a signed value is truncated to 16 bits via the MDRV_DEVICE_INLINE_DATA16() macro. When extracting the result from the m_inline_data[] array, it needs to be explicitly sign-extended.

The indexes that map which data is stored in which entry in the inline data array should be defined as a public enumeration within the device-specific configuration class. This keeps the global namespace less polluted and ensures no overlapping of indices.

In all cases, it is recommended that devices using inline data define nicer, more descriptive macros for specifying that data, rather than encouraging the user to operate with raw data and indexes. These custom macros can allow for more compact specification of the data, and can even be combined with the device's custom MDRV_DEVICE_ADD() macro to further simplify things for the user. Here's an example:

#define MDRV_SPEAKER_ADD(_tag, _x, _y, _z) \
    MDRV_DEVICE_ADD(_tag, SPEAKER, 0) \
    MDRV_DEVICE_INLINE_DATA32(speaker_device_config::INLINE_X, (_x) * (double)(1 << 24)) \
    MDRV_DEVICE_INLINE_DATA32(speaker_device_config::INLINE_Y, (_y) * (double)(1 << 24)) \
    MDRV_DEVICE_INLINE_DATA32(speaker_device_config::INLINE_Z, (_z) * (double)(1 << 24))

In this case, a single line in the machine configuration:

MDRV_SPEAKER_ADD("center", 0.0, 0.0, 1.0)

not only adds the device but also specifies all of its required parameters inline. (Note that because the parameters are floating-point values, they are converted to 8.24 fixed point first, since only integral values can be stored in the inline data array.)

Example #1: Simple Device with Static Configuration

This example is broken into four subsections, describing the configuration structure, the configuration class, the device configuration macros, and then finally the runtime class itself.

Static Configuration Structure

For this example, we will define a new device that uses a static configuration. Drivers using this device will need to declare a static const instance of the example1_device_config_data struct below and specify a pointer to that struct as part of the machine configuration.

Starting with the header file, we first define what the example1_device_config_data struct looks like:

struct example1_device_config_data
{
    int           m_device_integer_data;
    const char *  m_device_string_data;
};

This structure can contain pretty much anything. However, it is very important that the struct be a "plain old data" (or POD) type. This means that there should be no constructor and no virtual methods. The reason for this is that there are thousands of drivers defined in the system, and if each of them defined a static const structure like this that needed to execute its constructor on startup, it would adversely impact the overall startup time of the emulator. So just don't do it.

Configuration Class

Next, we define the device configuration class:

class example1_device_config : public device_config, 
                               public example1_device_config_data
{
    friend class example1_device;

    // construction/destruction
    example1_device_config(const machine_config &mconfig, device_type type, const char *tag,
            const device_config *owner, UINT32 clock);

public:
    // allocators
    static device_config *static_alloc_device_config(const machine_config &mconfig, 
            const char *tag, const device_config *owner, UINT32 clock);
    virtual device_t *alloc_device(running_machine &machine, const device_config &config) const;

    // basic information getters
    virtual const char *name() const { return "Example Device 1"; }

    // add accessors for any device-specific config that might be needed publically

protected:
    // device-level overrides
    virtual void device_config_complete();
    virtual bool device_validity_check(const game_driver &driver) const;

    // internal state
    int        m_additional_state;
};

Ok, there is a lot of good and subtle information here. Let's walk through the declaration step by step:

  • First thing to note is that the class is defined as inheriting from both device_config and example1_device_config_data (our static configuration structure). While it is not strictly necessary, it is convenient to do so because the members of the example1_device_config_data struct effectively become members of the device configuration class, making them easier to access without extra indirection. Making this work also implies copying the user provided data up into your configuration class, which is done later in the device_config_complete method.
  • Next you'll see we added example1_device as a friend class. In general, it is recommended to keep your configuration state private/protected, but allow the associated device to have free access to it by friending it. If external code needs to query your configuration directly, just add simple accessors to the configuration state (the need for this should be rare).
  • The constructor for the configuration class is kept private. The only way to allocate a new instance of a device configuration is via the static method static_alloc_device_config(). The parameters passed to the constructor in this example are the ones that need to be passed onto the base device_config class.

Walking through the methods defined in this class one by one, most of them are fairly simple and straightforward. First, the constructor:

example1_device_config::example1_device_config(const machine_config &mconfig, const char *tag, 
                const device_config *owner, UINT32 clock)
    : device_config(mconfig, static_alloc_device_config, tag, owner, clock),
      m_additional_state(0)
{
}

As you can see, most of the parameters just pass through to the base class. But be sure to initialize all configuration member variables here to ensure they get proper values. We could initialize the members of the example1_device_config_data class here as well, but we'll wait until the configuration step is complete.

Next up is the static device configuration allocator:

device_config *example1_device_config::static_alloc_device_config(const machine_config &mconfig, 
                const char *tag, const device_config *owner, UINT32 clock)
{
    return global_alloc(example1_device_config(mconfig, tag, owner, clock));
}

This function is required to be present, as the pointer to this function is used to identify the device type. Given just this function pointer, a configuration object can be constructed, and from there, the device can be created. Note that we need to use global_alloc here because the running_machine has not yet be created (and in fact never may be). Also note that we return a pointer to the base device_config class here, since the machine configuration only knows about the base classes.

The device allocator function follows, and it is also required:

device_t *example1_device_config::alloc_device(running_machine &machine) const
{
    return auto_alloc(&machine, example1_device(machine, *this));
}

In contrast to the configuration allocator, which is static and uses global_alloc, the device allocator is a virtual method that uses auto_alloc in order to allocate the device and assign its memory to the provided running machine. When we construct the actual device object, we pass the machine through along with a reference to ourself so that the newly created device can have access to our configuration information.

Once all the device configurations have been created and populated, the device_config_complete method is called. We override it here to set up our inherited copy of the static configuration structure:

void example1_device_config::device_config_complete()
{
    // copy static configuration if present
    if (m_static_config != NULL)
        *static_cast<example1_device_config_data *>(this) =
                       *reinterpret_cast<const example1_device_config_data *>(m_static_config);

    // otherwise, initialize it to defaults
    else
    {
        m_device_integer_data = DEFAULT_INT_VALUE;
        m_device_string_data = NULL;
    }
}

In the case where the user specified a pointer to a static configuration data structure, we copy it into ourselves. We use static_cast to find the pointer to where the inherited state lives within our structure and then copy the data pointed to by m_static_config. If no configuration data was provided, we take this opportunity to initialize the inherited configuration structure to default values.

Finally, if the device wishes to provide stronger validation of data -- verified both when starting a game as well as when running MAME with the -validate option -- it can ovverride the device_validity_check method:

bool example1_device_config::device_validity_check(const game_driver &driver) const
{
    bool error = false;

    // sanity check configuration
    if (m_device_integer_value < 0)
    {
         mame_printf_error("%s: %s device '%s' has invalid integer parameter\n", 
                           driver.source_file, driver.name, tag());
         error = true;
    }
    return error;
}

This method should examing the configuration data, output friendly errors and warnings, and return true if a fatal error was detected.

Configuration Macros

Now we need to declare how to reference the device from a machine configuration. This is done via special tokenizing macros:

MDRV_DEVICE_ADD("tag", DEVICE_TYPE, device_clock)
MDRV_DEVICE_REPLACE("tag", DEVICE_TYPE, device_clock)

So the first thing we need to define is our ALL_CAPS DEVICE_TYPE. As mentioned previously, the device type is simply a pointer to the static static_alloc_device_config() method, so it should be defined like so:

static const device_type EXAMPLE1 = example1_device_config::static_alloc_device_config;

Although this is all that is required at a minimum, in general it is considered a good idea to provide your own set of MDRV macros that are specific to your device. By defining our macros, we can more precisely guide the user to ensure all data is properly specified. Let's say for example that our static configuration is required (i.e., it is invalid to not specify anything). Using the raw macros, a driver writer would declare an instance of our device like this:

MDRV_DEVICE_ADD("tag", EXAMPLE1, 0)
MDRV_DEVICE_CONFIG(local_structure)

Given this, you can see that it would be easy to forget the second line and leave a device with no configuration data. Also, the user is forced to specify a dummy clock, even though our device doesn't need one. Instead, let's define our own macro for adding an EXAMPLE1 device, like so:

#define MDRV_EXAMPLE1_ADD(_tag, _config) \
    MDRV_DEVICE_ADD(_tag, EXAMPLE1, 0) \
    MDRV_DEVICE_CONFIG(_config)

and then the equivalent declaration becomes:

MDRV_EXAMPLE1_ADD("tag", local_structure)

By doing this, we get to provide a cleaner interface for declaration, and at the same time we ensure that a required parameter is specified.

Runtime Class

Next we move on to the runtime device class:

class example1_device : public device_t
{
    friend class example1_device_config;

    // construction/destruction
    example1_device(running_machine &machine, const example1_device_config &config);

public:
    // any publically acessible interfaces needed for runtime

protected:
    // device-level overrides (none are required, but these are common)
    virtual void device_start();
    virtual void device_reset();

    // internal device state goes here
    const example1_device_config &m_config;
    int                           m_device_state;
};

Going through the class definition, there are some similar patterns to the corresponding device configuration class:

  • Again, we derive from a common base class, in this case the device_t class. However, unlike the configuration class, we just have simple single inheritance here, since we don't need the example1_device_config_data struct.
  • We make our configuration class our friend, just as they made us their friend. Really, you should imagine these two classes as two halves of the entire device.
  • As with the configuration class, our constructor is kept private. A device should only be allocated via the corresponding configuration class's alloc_device() method. Since our configuration class is our friend, it can still allocate us.
  • There are no standard methods that need to be public in the runtime device. However, it is quite likely you will need some in order to interact with it (read/write handlers fall into this category, for example).
  • This example overrides two device-specific notification methods, one which is called at device start time (after all devices are constructed), and one which is called at reset time. These are optional, though common, to override.
  • At the bottom you'll see a reference to our configuration class. This is kept to enable easy access to the configuration data.

Looking at each of the methods above in a little detail, we first encounter the constructor:

example1_device::example1_device(running_machine &machine, const example1_device_config &config)
    : device_t(machine, config),
      m_config(config),
      m_device_state(0)
{
}

Here we initialize our parent class by passing down the reference to the running_machine and our configuration. We also stash a reference to our configuration into the m_config variable for later use, and we reset all our internal state variables to something well-defined. Note that we don't generally do much initialization in the constructor; that work is preferred to live in the device_start() method.

Speaking of which...

example1_device::device_start()
{
    // initialize state from configuration
    // locate any other devices
    //    if other devices not ready, throw device_missing_dependencies()
    // register for any save state
}

Okay, not much meat in the implementation above, but there are a number of things expected of a device during device_start() time, including consumption of the configuration, identification of related devices, and set up for save states. If a related device is located (at this point all devices are constructed) but it hasn't yet been started, you can throw a device_missing_dependencies() exception and your device_start() will be queued to the end of the list to be called again later.

Similarly,

example1_device::device_reset()
{
    // reset internal state to well-defined values
}

the device_reset() method is there to enable resetting a device's state in the event of a requested reset.

Best Practices