How I used MAME's Lua integration to greatly improve my retro development experience
I'm currently working on a game for the Neo Geo, a game console from the 90s. The Neo Geo runs at 12mhz, has about 64kb of RAM, and no debugger or really any modern developer conveniences. Fortunately I can write my game in C instead of assembly thanks to the excellent ngdevkit, so at least there's that.
When something goes wrong, it's incredibly difficult to figure out why. Often the game just crashes causing the system to reset, or the game just does something bizarre. There is virtually no feedback at all. If I am are very lucky, I might get an error screen
This is possible thanks to the UniBios. An aftermarket BIOS developed by Razoola that adds some debugging capabilities.
This ancient, nearly 40 year old, console is just a black box. When my game had a bug in it, sometimes it would take me days to figure out why. Not to mention things like performance profiling are just not possible. Thankfully, MAME's Lua integration can fill a lot of gaps here.
Primitive Logging
Even just logging something is a challenge. When I first started on the game, I built a console that writes to the Neo Geo's screen.
It could also write a small snippet to an (x,y) location on screen, creating simple "overlays"
For the longest time, this was all I had. Neo Geo games run at 60 frames per second, and the screen resolution is only 320x224. If I wanted to log out something every frame, the screen would get filled with output instantly. It was better than nothing, but it was far from enough.
Primitive Assertions
I next added a primitive version of assert()
. It used a combination of the existing console system along with C's __FILE__
and __FUNCTION__
macros. Whenever an assertion failed, it would print an error message onto the screen which contained the file, line number and function name. Then it would put the game in an infinite loop, which effectively halts the game.
This was a huge boon! I put assertions all over the place, mostly to validate parameters to functions
void vram_spritesTruncate(u16 startingSpriteIndex , u16 count ) { ngassert( startingSpriteIndex > 0, "spriteIndex out of range: %d" , startingSpriteIndex ); ngassert( startingSpriteIndex + count <= 380, "spriteIndex out of range: %d" , startingSpriteIndex + count ); *REG_VRAMADDR = ADDR_SCB3 + startingSpriteIndex ; ...}
By being proactive with assert()
the number of bugs I created went down a lot. And a surprising failed assertion often let me root cause a bug much quicker. I was impressed by how much this helped me.
But development was still slow and tedious. I had cracked open the black box, but just a very tiny bit.
MAME's Lua scripting
It's now possible to control MAME with Lua scripts. This is a fairly recent addition and MAME devs have told me it's still a bit experimental and subject to change going forward. For example I wrote a script that visualizes all the sprites the Neo geo is currently using
I have started collecting these scripts into this GitHub repo.
Spying on memory reads and writes
One feature of MAME's Lua integration is write (and read) taps. For example, here is a very simple write tap that causes Puzzle Bobble's shooter to move at 4 times its normal speed
cpu = manager .machine.devices[":maincpu"]mem = cpu .spaces["program"]
-- this address is the "shooter delta", whatever gets -- set here will get added to the shooter's current angle address = 0x108212
function on_memory_write(offset, data ) -- by multiplying the value, --- the end result is the shooter travels -- much faster return data * 4end
mem_handler = mem :install_write_tap( address , address + 1, "writes", on_memory_write )
You can save this script to a file then launch MAME with it
mame -autoboot_script fastShooter.lua pbobblen
This script is spying on the memory location the game uses to store how much the game's shooter has rotated in the current frame. The tap receives the value the game is writing. We can cause the game to write a different value by returning a new one, so return data * 4
just causes the shooter to move 4 times farther than it normally would, resulting in a stupidly fast shooter.
Communicating with MAME via write taps
I use write taps in my game to send data over to MAME. For example, I replaced the onscreen console with one that writes to my PC's terminal.
In my game's code, I have defined some memory addresses
#ifdef NGLUADEBUG
#define NG_BASE ((char*)0x10d000)
// logging related #define NG_LINE_LENGTH 80
#define NG_CONSOLE_BUFFER (NG_BASE)#define NG_CONSOLE_SIG ((NG_CONSOLE_BUFFER) + NG_LINE_LENGTH + 2)
#endif
0x10d000
is a location in the Neo Geo's main RAM that my game is not using.
Then my ngprintf
function looks like this
void ngprintf(const char* format , ...) { va_list formatArgs ; va_start(formatArgs, format ); vsnprintf( NG_CONSOLE_BUFFER , NG_LINE_LENGTH , format , formatArgs ); va_end(formatArgs);
*NG_CONSOLE_SIG = 1;}
It takes in a format string and arguments, such as ngprintf("player x: %d", playerX)
, forms the output string using functions from the C standard library, and then writes the result to NG_CONSOLE_BUFFER
. That's just an address in main RAM that I defined just above. My game is just literally writing a string into memory, and that's it.
NG_CONSOLE_SIG
is a byte in memory that is used to signal a new string is ready for Lua to pick up.
Then my Lua script knows to spy on the signal address and act accordingly
function ng_to_stdout() local str = mem :read_range( NG_CONSOLE_BUFFER , NG_CONSOLE_BUFFER + NG_LINE_LENGTH , 8 ) print(str)end
ngstdout_handler = mem :install_write_tap( NG_CONSOLE_SIG , NG_CONSOLE_SIG + 1, "ngstdout", ng_to_stdout )
Lua will grab the string out of the Neo Geo's memory, and print()
it to my PC's terminal. Now I have proper logging from a 30 year old console!
Visualize all the stuff!
I have added many more write taps and now Lua can show me all kinds of things.
Here the orange dots show the player's past locations. This has been helpful in getting the controls to feel just right. The lower right shows the game's true current frame rate. This is different from the frame rate that MAME reports. If it ever dips from 60 to say 30, I know I have a performance problem. The blue, green, red and purple boxes show the bounding boxes of entities on the screen such as the terrain and the flag. This has been helpful in tracking down collision detection issues.
Visualizing performance
Here is a visualization of function timings every frame. The green area is my game waiting for the next frame to start. The black is my game dealing with the player itself, such as responding to inputs from the joystick and moving the player accordingly. The gray area is the player's collision detection routines running against the terrain. In this screenshot, as more terrain came onto the screen (the floating orange boxes), the game needed to spend more time doing collision detection, as seen by the gray area ramping up.
Here we can see the performance meter doubling in size. This is because there is so much terrain on the screen, the game needs to do terrain collision detection for so long it takes longer than one frame. The game is forced to wait until the next frame (to avoid graphical glitches, something this blog post is glossing over...), and thus the game has dropped to 30 frames per second. Dropping to 30 fps is really bad, and something I want to always avoid. This perf visualization helps a lot!
Also this screenshot shows which sprites in video RAM are currently in use (the bottom gray area with many lines), as I forgot to toggle that overlay off before taking the screenshot :)
Conclusion
What's really great about all of this is the overhead this adds to the actual game is virtually nothing. MAME can easily emulate the Neo Geo and handle my Lua scripts without breaking a sweat. So the visualizations I get remain very accurate.
My Lua scripts do a lot more, but you get the idea. This has been an absolute game changer! I am so grateful for this MAME feature. It's really allowing me to see exactly what the Neo Geo is up to, and enabling me to juuuust squeeze out a pretty modern and elaborate platformer game engine on ancient hardware.