MCS
MCS
MCS (Mame for C Sharp) is the MAME emulator re-written in C#.
The code is hosted on GitHub here.
Reason why I started this.
I have been a fan of MAME since its initial release over 20 years ago. In the past, I have written my own emulators, but never really learned the internals of MAME.
My motivation for starting this was two-fold:
- To see if was possible to port MAME to C#.
- To learn MAME.
The advantages of C# are many, and discussed plenty elsewhere. Regarding C# vs C++ in the context of MAME development (and more specifically, developing under Windows using Visual Studio):
- C# is similar to C++ in language syntax and functions (relative to other languages).
- C# is much faster to compile (for iterating during development).
- C# is much easier and more stable to debug.
The biggest disadvantage is performance. But, my motivations didn’t require performance. Just to see if could be done, and to learn the codebase.
Approach
The approach of this port was:
- Do a straight port. Don’t try to re-factor the C++. Don’t get too fancy with C# features. Since there is no automated way to merge improvements, this provides the best way to keep the C# source sync’d with the latest official version.
- Create a library. Keep all MAME code as a .NET library. Implement the OSD interface in the client app.
- Use Pure C#. Don’t use pinvoke. Don’t use unsafe. Don’t use anything platform specific. Avoid 3rd party libraries. This keeps the code portable, and easier to maintain.
Influences
There are a few projects I evaluated before starting this project. Their existence gave the possibility that this type of thing could be possible. They were:
- libmame - MAME compiled as a C++ library.
- imame4all-Reloaded - iOS and Android port of an old version of MAME.
- CPS1.NET - A CPS1 emulator written in C#, with code taken from MAME.
- xnamame036 - C# port of an old version of MAME.
General Comments
MAME is the most well-maintained open-source C++ project I have seen. Considering its size, that's quite an achievement.
It's obvious that the authors value this work, and do a great job maintaining this part of arcade history through emulation. I have a lot of respect for the time and effort spent in the development of MAME over the years.
The porting process
The basic process is to start with a game. Start with that driver. Comment out everything in that driver and make sure it compiles. Then, uncomment lines one at a time, fixing compiler errors as they come up. Early on, this created quite a rabbit trail of unimplemented functions. But eventually I would end up with a compiled version. After all lines were uncommented, I would cross my fingers and run it. This led to many unimplemented exceptions and bugs that I would have to track down and fix. Eventually, the game works!
Challenges
Overall, the process wasn’t too difficult, just a bit time consuming.
However, there were a few challenges that can be pointed out:
- Multiple Inheritance - C++ has it, C# does not. MAME uses this extensively, especially in their device interfaces. My strategy for this in general is to use object composition, similar to what's described here. This gets rather messy at times, but it works. It could be cleaned up at some point.
- Pointers - C++ has them, (pure) C# does not. In general, this is a benefit and should help clean up the code. Memory management is not a concern. Smart pointers are not needed. Function prototypes are made simpler. You don't have to worry about whether or not to de-reference the pointer when accessing it. However, you get into trouble in certain scenarios:
- When there are pointers to POD data types. In some places, I had to use an IntRef class (something I made up) because the class used a pointer to an int, when it was used in multiple places. Basically, I'm manually boxing the int reference.
- It is sometimes confusing when the C++ code intends to use the data by ref rather than making a copy.
- If the data is being passed to a function as a pointer, it is sometimes difficult to know if the data is a pointer to a single variable, or if it's an array. Lately though, the C++ code is being re-factored to use more C++ collection classes, which helps with this dramatically.
- Bit fiddling. The C++ source does quite a bit of this (sorry, bad pun). It's very convenient with emulator code. However, C# doesn't have bit fiddling overloads for small data types. (eg, you can't do 'byte result = byte1 | byte2;' without casting.) The reason is described here and here. So, code that manipulates bytes and shorts ends up looking very messy with a bunch of casts to the correct data type. And with all of that type promotion and casting to smaller types, it's unclear whether or not it does what the original code expected. Especially code that expects certain behavior related to overflow.
- Flags and Enums. The C++ has a bunch of enums and flags. Sometimes the enums are treated as flags! C# has a few annoyances here:
- Enums need the name of the enum as a prefix. This uglies up the code.
- Enums can't be used as an integer unless cast. More ugly.
- Enums can't be used as an index in an array unless cast.
- Enums can't act like a flag unless cast.
- When a flag is checked, the result must be an explicit bool. (This is true of any C# conditional statement.) So, a lot of ' != 0' was added everywhere.
- In general, all of this makes C++ code like:
- if ((item[0].flags & FLAG_UI_DATS))
- look like this in C#:
- if ((item[0].flags & (UInt32)FLAG.FLAG_UI_DATS) != 0)
- C# is supposed to make things easier!
- if ((item[0].flags & FLAG_UI_DATS))
- No constant suffixes for byte/sbyte. Minor, but you can't do 'byte result = istrue() ? byte1 : 42'. C# thinks 42 is an int, and can't convert it to byte. However, there's no suffix for byte. You are forced to do '(byte)42', which is kinda ridiculous. This type of code happens frequently in the C++ source.
- No common template for int/uint/byte. This is by far where the C# code gets extremely messy. The C++ source uses templates extensively in the memory code like devcb and emumem. The way the C++ code uses them is not compatible with the way C# does templates. Often, this leads to just duplicating the entire class, simply because you can't use a template for an 'int' vs a 'short'. Some more info about this here and here and here.
- More C# Template limitations: new() restraints can’t take ctor parameters. Often times, the C++ code will define a template class that dynamically new()'s different types inside the class. The new()'d types expect parameters, and C# doesn't allow that. You can only new() a type in a template class/function if the ctor doesn't take parameters. This leads to some messy C# code to workaround this. More recently, the C++ source has started using a new C++ feature that allows passing arbitrary number of args within template definitions. This makes things even more difficult to port. eg:
- inline DeviceClass &device_type_impl<DeviceClass>::operator()(machine_config &mconfig, char const *tag, Params &&... args) const { return dynamic_cast<DeviceClass &>(*mconfig.device_add(tag, *this, std::forward<Params>(args)...)); }
- Global code and Macros. A lot of the C++ source exists in the global space. eg, things like global constants, macros, etc. A lot of this is simply for convenience, but it is simply not allowed in C#. So on the C# side, many things get put into a _global static class in each file. This uglies up the code, but is manageable. One other issue is that macros are not typed. So, some of the ported macros have to have multiple variations to support different types. Recent efforts on the C++ has cleaned up some of this code. Seems like global code and macros are getting phased out, preferring to use true const variables and functions. This is good.
- More recently the C++ source is making heavy use of the enable_if keyword in template parameters. When I see code that looks like this, my eyes start to glaze over:
- template <typename T> std::enable_if_t<is_transform<output_t, Result, T>::value, transform_builder<transform_builder, std::remove_reference_t<T> > > transform(T &&cb)
What’s included
mcs
This is the C# .NET Class Library containing the base emulation code and drivers.
- Configured for Visual Studio 2017.
- Open mcs.sln to build the library under Debug or Release.
mcsForm
This is a Windows Forms example that uses the MCS library.
- Configured for Visual Studio 2017.
- Open mcsForm.sln to build the app under Debug or Release.
- It looks for roms under a \roms folder under the solution directory.
mcsUnity
This is a Unity Project showing that the MCS library can be used in the Unity game engine.
- Configured for Unity 2018.1.5f1.
- Tested under these platforms:
- Win64
- Android
Thinking about what this is doing is quite amazing. MCS is a project that has ported the original C++ code to C#. In Unity, this runs inside a Mono scripting sandbox. When a build is generated, that code is compiled to IL, then compiled back to C++ (through IL2CPP), then compiled to JS (on the webgl target). When run in a browser, the JS is then converted to native code via JIT. The wonders of modern technology!
Getting Started
mcsForm Quickstart
- Clone the GitHub repo
- git clone https://github.com/fasteddo/mcs.git
- Open mcsForm\mcsForm.sln in Visual Studio
- Set the build configuration to Release | Any CPU
- Build -> Build Solution
- Debug -> Start Without Debugging
Original MAME build instructions
A fork of the original MAME project is included in the MCS repo as a submodule. Make sure you do a submodule update and you'll pull it down in the original clone.
- Requires msys64 to be present on the system.
- Download and run: https://github.com/mamedev/buildtools/releases/download/2.0/msys64-2017-02-05.exe
- Place in c:\msys64
- From a CMD prompt:
- Run setpath.bat (adds msys64 to the PATH)
- Run msvcbuild.bat or run msvcbuild-sources.bat
- msvcbuild configures the build for the entire MAME codebase. Building this take a *very* long time.
- msvcbuild-sources builds a very small subset of the MAME drivers, making it much fast to build and easier to debug things.
- Open mame\build\projects\windows\mame\vs2017\mame.sln in Visual Studio.
- Note there is a compiler error with the latest Visual Studio 2017 (15.8 and later). See here: https://developercommunity.visualstudio.com/content/problem/345010/internal-compiler-error-with-mame-emulator-devdele.html
- Recommended to use Visual Studio 15.6 or 15.7.
- Set the build configuration to Release | x64.
- Project -> Retarget Solution. Hit Ok.
- Build -> Build Solution.
- Debug -> Start Without Debugging.
Current Status
A very small amount of games have been emulated. An even smaller number of devices.
Currently the list stands at:
- 1942 (no sound)
- Centipede
- Dig-Dug
- Donkey Kong (no sound)
- Galaga
- Galaxian (no sound)
- Ms. Pacman
- Pacman (and Pacman Plus)
- Frogger (partial)
- Moon Patrol (partial)
- Xevious (partial)
If you run into an emu_unimplemented exception, this means the function/feature has not been ported yet.
The code has been sync’d with 0.206 of MAME.
History
I started this project in early 2015 and worked off and on until released on GitHub in 2018.
I started with MAME 0.157 and have been manually merging changes up to the current release.
I initially chose games based on the z80, since that was a CPU I had previous experience with. I was porting both the specific devices and the MAME architecture, so this took quite a long time. Now that a lot of the architecture code has been ported, I'm hoping future games will take less time to port.
License
This project is released under the BSD-3-Clause License.
Who am I?
My name is Eddie Fast and I am a professional software developer. I have personal interests in emulation and this project was developed independently during my free time.