Thursday, January 2, 2020

My new Z80 emulator

So five years ago, I started speculating about how emulators could be improved and came up with several ideas.  One of them was to emulate the clock pin on CPUs (and other devices such as flip-flops or the PIA6821) for maximum accuracy.  I speculated that this would take a major performance hit but perhaps with multi-threading technology, this performance hit could be offset.

Well, I've been working on this secretly for several months now.  I've emulated almost every TTL chip on the Dragon's Lair logic board as a standalone device and tied them all together.  Performance at the moment is barely adequate on modern beefy hardware (75% CPU usage on a single thread on an AMD Ryzen) and not adequate on a Raspberry Pi 4.

I experimenting with using multi-threading solutions to speed this up, but the critical timing and the need to keep the whole system running in lock-step made me conclude that this must be run on a single thread.  I'll keep looking at ways to use other threads to offload things like video and sound processing which I think will work fine.

So the good news is that I believe I'll be able to design the "perfect" Dragon's Lair emulator and it will be able to run at full speed on modern CPUs (such as the aforementioned Ryzen) and possibly even older CPUs like the Intel i5.  When I say perfect, I mean that every single pin on every single IC in the system (including the Z80) will be emulated and have the exact timing of original hardware.  Where a device is digital, there will be no shortcuts.  (analog devices such as the sound chip will not be held to this same standard of perfection).  I'm pretty excited about this because I sometimes study/repair original game board sets and not having the ability to study how the hardware is supposed to work via emulation has been quite inconvenient.

The bad news is that Dragon's Lair is the simplest of laserdisc games, so a game like Star Rider (which has three 6809E CPUs and a ton of TTL chips) probably won't able to run at full speed even on the newest of hardware using this approach.  I'll be looking at thoughtful optimizations/compromises to make for these scenarios since obviously with enough compromises, any of these old games could be made to run at full speed.

Here's a brief description of how the Z80's pins work from an arbitrary instruction:

Here's every single pin of a Z80 captured via my logic analyzer.  When I embarked on the journey to emulate this beast, I thought "this is insane.  it will take so long to figure this stuff out."  but.. this is the kind of emulator I want Daphne to be, so I did it anyway.  now it's starting to make sense.

I had to do two separate captures because my logic analyzer can capture 32 pins at a time (which is a ton!) and the z80 has 40 pins.  that alone almost made it give up.

I've put colored lines to show how this instruction (LD (HL), 00h) works.

First section ("M1") is loading the instruction's opcode from the ROM program.  It sets the address to 1153h, lowers RD and MREQ lines, then the EPROM puts 36h on the data bus.  Z80 then raises RD/MREQ, then lowers MREQ/REFRESH, putting the number 0005h on the address bus which is used for dynamic ram to refresh itself (I think dragon's lair ignores it because it uses static ram).  That takes 4 clock cycles and is known in Z80 speak as "M1".  Next section ("M2") it needs to read the rest of the instruction, so it sets address to 1154h, lowers RD/MREQ again and the EPROM puts 00h on the data bus.  M2 is complete.  Now it has the full instruction.  It goes to the next section ("M3").  HL happens to have a value of A000h, so it sets the address bus to A000h, lowers MREQ but instead of lowering RD, it puts 00h on the data bus and lowers WR which tells the rest of the system that "Hey, I want to write a 00h to address A000h".  The rest of the system's hardware maps that address to the RAM and the RAM wakes up and grabs the data from the data bus and stores it.

The amazing thing is... how did they design the Z80 in 1976 or whenever?

Saturday, September 28, 2019

DL Euro + Dexter running inside of a cabinet!

I realize that I haven't posted any updates on Dexter working with Dragon's Lair Euro in a while.  Let me remedy by sharing this video and pictures from Matteo Marioni, running Dexter inside of his Dragon's Lair Euro cab.  As you can see, there are still a few minor glitches to fix, but overall, it's looking great!


Saturday, August 17, 2019

How to modify MAME to shutdown if idle too long

So if you read my last post, you know that I am working on a MAME/Daphne cab that my kids may use from time to time.  One problem I noticed is that some games (like Teenage Mutant Ninja Turtles) will sit on the start screen indefinitely if you insert a coin.  I can't trust my kids to shut off the games while I'm at work, so I need a way to protect my CRT from being destroyed.  The solution I came up with was to modify MAME to shutdown after no input has been received for 5 minutes.

I modified MAME v0.211 which as of this writing is almost the latest version (but not quite).  As I am running MAME on a Raspberry Pi, I dug into the SDL code to add this hack.  I tried to find a more universal solution that would also work on Windows but couldn't really figure it out after an hour or so of digging so just gave up.

I ended up using global variables which are normally a big no-no in my book, but since building MAME takes so long on the Pi, I wanted a solution that would avoid modifying any .h files (and thus force me to have to potentially rebuild MAME from scratch depending on which .h files I changed).

Here are the relevant files that I modified and the changes that I made:


extern u32 g_last_time_seconds_when_input_received;
//  recompute_speed - recompute the current
//  overall speed; we assume this is called only
//  if we did not skip a frame
void video_manager::recompute_speed(const attotime &emutime)
and a little ways further down

        // if we're past the "time-to-execute" requested, signal an exit
        if (m_seconds_to_run != 0 && emutime.seconds() >= m_seconds_to_run)
                // create a final screenshot
                emu_file file(machine().options().snapshot_directory(), OPEN_FLAG_WRITE | OPEN_FLAG_CREATE | OPEN_FLAG_CR$
                osd_file::error filerr =, PATH_SEPARATOR "final.png");
                if (filerr == osd_file::error::NONE)
                        save_snapshot(nullptr, file);
                //printf("Scheduled exit at %f\n", emutime.as_double());
                // schedule our demise
        // START MPO
        u32 uTimeSeconds = machine().time().seconds();
        // if we've just barely started up, then we need to seed the value
        if (g_last_time_seconds_when_input_received == 0)
                g_last_time_seconds_when_input_received = uTimeSeconds;
                u32 diff = uTimeSeconds - g_last_time_seconds_when_input_received;
                // If the system hasn't received input lately, then we need to shut down
                // hard-coded since I am just doing a one-off feature for my own use
                if (diff > 300)
        // END MPO


void mame_ui_manager::display_startup_screens(bool first_time)
        const int maxstate = 3;
//      int str = machine().options().seconds_to_run(); // MPO        bool show_gameinfo = !machine().options().skip_gameinfo();
        bool show_warnings = true, show_mandatory_fileman = true;
        // disable everything if we are using -str for 300 or fewer seconds, or if we're the empty driver,
        // or if we are debugging
//      if (!first_time || (str > 0 && str < 60*5) || &machine().system() == &GAME_NAME(___empty) || (machine().debug_fla$
        // suppress start-up dialogs
                show_gameinfo = show_warnings = show_mandatory_fileman = false;


// START MPO, ugly global hack to avoid having to rebuild the entire code base (keeping changes inside of .cpp files only)
u32 g_last_time_seconds_when_input_received = 0;
//  sdl_keyboard_device
class sdl_keyboard_device : public sdl_device
        keyboard_state keyboard;
        sdl_keyboard_device(running_machine &machine, const char *name, const char *id, input_module &module)
                : sdl_device(machine, name, id, DEVICE_CLASS_KEYBOARD, module),
        void process_event(SDL_Event &sdlevent) override
                switch (sdlevent.type)
                case SDL_KEYDOWN:
                        keyboard.state[sdlevent.key.keysym.scancode] = 0x80;
                        if (sdlevent.key.keysym.sym < 0x20)
                                machine().ui_input().push_char_event(osd_common_t::s_window_list.front()->target(), sdlev$
                        // START MPO
                        // We've received keyboard input (which is all we care about), so reset the timer
                        g_last_time_seconds_when_input_received = machine().time().seconds();
                        // END MPO

Monday, July 29, 2019

How I created my MAME cab

Greetings all,

I have eight arcade game cabinets.  My wife informed me that I had reached my limit.  So in order to have more games, I needed to break down and build a MAME cab (which I hope will evolve into a MAME/Daphne cab in the future).

For whatever reason, the look of the monitor was really important to me.  So I found an existing 25" CRT monitor installed inside of a D&D "Tower of Doom" cab that had a control panel with a ton of buttons on it.  Not only did I like this game, but having 4 joysticks with 4 buttons each was what I needed for a good MAME experience.  The monitor had been tube swapped and recapped so was looking great.  So I brought it home and began working on it to MAME-ize it.

Another important factor for me was to be able to run JAMMA games inside of it in the future if I chose, so I didn't want to mess with the JAMMA harness or the extra kick harness (which was configured for D&D Tower of Doom).

Here are the links that I used for research:

I purchased a JPAC and an IPAC4 (which I probably didn't need) in order to get full inputs.  In hindsight, I probably could've got an IPAC2 (or whatever it's called) since I only needed to wire up controls for players 3 and 4.  However, despite the IPAC4 being overkill, it also made setup easier because I didn't need to remap the player 3 and player 4 inputs.

I also purchased a gert666 adapter and the latest Raspberry Pi (3b+ at the time of this writing, the 4 is still hard to obtain).  UPDATE: I can confirm that these instructions do NOT work on the Raspberry Pi 4.

The power supply for your Pi is important.  You can't just get any generic one (I got burned by this) or the voltage to the pi will drop at random spots and your games will go into slow motion mode randomly.  I can confirm that this power supply works well for my Raspberry Pi 3b+.

For audio, I went with this amp which may not have been the best choice because it's a stereo amp and JAMMA cabs are only mono.  I also added this 3.5mm to RCA and this ground loop isolator which was very necessary to reduce unwanted buzz/hum.  I still get some minor hum through my speakers, so I don't really know if my audio choice is to blame or not, but it mostly works and I don't notice the hum while playing games.  I powered the amp through the existing 12V line on the JAMMA power supply that came with the cab.  Not sure if this was the best choice.  I've got a lot to learn about analog audio.

To hook up players 3 and players 4, I referred to the kick harness section for D&D at this link.  I wired up my own connector to match what is on the D&D board, and secured the wires with heat shrink and a glue gun.  Now I feel safe that this thing isn't going to come loose, and if I ever acquire a real D&D JAMMA, I can plug it in and play without any other configuration.

Configuring the software for the Pi was probably the hardest part.

I installed the latest Raspbian ("Buster" at the time of this writing), enabled SSH/Wifi and disabled video overscan.

I tried RetroPi but was unsatisfied with the old version of MAME they included and the way they force input mapping.  So I decided to build the latest MAME from scratch instead.

At the time of this writing, the latest MAME version is 0.211 .

There are a few problems with building this version of MAME on the Pi but I was able to work around them:

  1. Uncomment "NOWERROR = 1" to not fail on warnings
  2. Uncomment "NO_USE_XINPUT = 1"
Follow the build instructions here.   When building SDL, you only need to build SDL and SDL_ttf, not the rest of the optional libraries.  In order to build SDL_ttf, you need the freetype-config script which is apparently not included with Raspbian Buster anymore, so I manually downloaded and installed freetype ( freetype-2.10.0 at the time of this writing ) which seemed to work and gave me the needed script.  NOTE that I just did the usual "./configure; make; sudo make install".  I did not uninstall the existing libfreetype6-dev Debian package, which might've caused conflicts but I got lucky.

I tried for several days to get cross-compiling working for MAME but eventually gave up and just followed choccyhobnob's instructions.  Building MAME on the Pi 3 takes forever (like 24-36 hours, literally).  I concluded that letting your pi churn away this long is probably still faster than messing around with cross compilation, so I'd just plan on waiting this long.

NOTE that you need to add "-video accel" to the mame command line to enable the hardware acceleration.  It seems to default to unaccelerated software mode which is too slow.

Once I had the software ready to go, I added this to /boot/config.txt to make the video output 15 khz 320x240 :

audio_pwm_mode = 2
hdmi_timings=320 1 16 30 34 240 1 2 3 22 0 0 0 60 0 6400000 1 #240p

I then plugged the gert666 VGA adapter onto the pi, plugged its VGA output into the JPAC VGA input, then plugged the JPAC into the JAMMA harness.  I powered on the cab and got a nice big boot screen.

Running MAME in this res is problematic because it will downscale some games (like Capcom's 384x224) which looks pretty crappy.

I found a neat solution to this problem here.

By running games at 1600x240 or 1600x224 and turning off bilinear filtering, the display looks pretty dang good and you don't need to mess around with a ton of different configs (such as 320x224 or 384x224).  Besides, I couldn't get 384x224 to work on my monitor anyway, so I had to use 1600x224.

I created a few scripts to launch games at the proper resolution.  I haven't yet tried to setup a frontend because I've been trying to get the games looking great first.

My script ( to convert display to 1600x224:


vcgencmd hdmi_timings 1600 1 70 150 215 224 1 10 14 16 0 0 0 60 0 32000000 1
fbset -depth 8 && fbset -depth 16 -xres 1600 -yres 224

My script ( to convert display to 1600x240:


vcgencmd hdmi_timings 1600 1 70 150 215 240 1 4 3 15 0 0 0 60 0 32000000 1
fbset -depth 8 && fbset -depth 16 -xres 1600 -yres 240

My script to convert back to 320x240:


vcgencmd hdmi_timings 320 1 16 30 34 240 1 2 3 22 0 0 0 60 0 6400000 1
fbset -depth 8 && fbset -depth 16 -xres 320 -yres 240

My script to launch Rastan (320x240) :


cd code/mame
./mame rastan -video accel -noka -nofilter

My script to launch Golden Axe (320x224):


cd code/mame
./mame goldnaxe -video accel -noka -nofilter

My script to launch Teenage Mutant Ninja Turtles (224 lines) :


cd code/mame
./mame tmnt -video accel -noka -nofilter

My script to launch D&D Tower of Doom (384x224):


cd code/mame
./mame ddtod -video accel -noka -nofilter

You get the idea.

Adding -noka tells MAME to fill the whole screen.  -nofilter tells MAME to disable bilinear filtering to give it that authentic CRT look.  The 1600 pixel width is enough so that the nearest neighbor scaling isn't noticeable (at least to my eyes).

Performance is very good all things considered.  All of the games I mentioned play at full speed except during some busy parts of the game.  Rastan looks especially good.  You'd never know that you weren't playing on the original JAMMA board.

I hope this helps someone :)

PS - You may notice black bars above and below my display.  This is due to the way I've adjusted the monitor pots.  There is some problems with the monitor when certain bright colors are displayed at the top and I decided I'd rather have the black bars rather than have to see the problems.  I don't think this is an issue with my Pi/JPAC config.

Monday, June 17, 2019

Dexter Sony LDP-1450 bug finally found and fixed!

Some of you know that I had to roll back a Dexter firmware upgrade a while ago due to that game Crime Patrol not working correctly with the update.

Well, I've been working behind the scenes to figure out what the bug was and pulling my hair out in frustration looking at my code.

It turns out that the bug had nothing to do with my changes that caused the problem but instead was a buffer overflow bug in some other code that hadn't been a problem by sheer luck :)

Once I added some new serial port code, this buffer overflow was overwriting my memory.  I finally had to attach an AVR Dragon (AVR programmer and debugger) and set a memory breakpoint to catch the problem.

Here is the code that caused the problem.  It's part of the LDP-1000/1450 interpreter:

uint16_t g_ldp1000i_tx_buf[12]; // this should be small enough so as to not waste space but big enough to hold things like current frame number
uint16_t *g_ldp1000i_pTxBufStart = 0;
uint16_t *g_ldp1000i_pTxBufEnd = 0;
const uint16_t *g_ldp1000i_pTxBufLastGoodPtr = (g_ldp1000i_tx_buf + sizeof(g_ldp1000i_tx_buf) - 1);

The fix is to change the last line to:

const uint16_t *g_ldp1000i_pTxBufLastGoodPtr = (g_ldp1000i_tx_buf + (sizeof(g_ldp1000i_tx_buf)/sizeof(uint16_t)) - 1);

The problem with working in C is that these types of mistakes are easy to make and the compiler often won't even warn you about it.  This is the price of working with lower level languages :o

Using the sizeof keyword is kind of dangerous because it always returns a value in bytes which doesn't work when your pointer is a uint16_t :)  I still use sizeof all over the place, though, because most of my pointers are uint8_t's :)

For future reference, when adding to a pointer, if you are going to use sizeof, you need to divide by the size (in bytes) of the element that makes up the array.  In this case, the TX buffer is 12 2-byte elements, so sizeof would return a 24 (12*2 is 24).  But when adding, I actually wanted to just add 12, not 24, because I wanted to add the number of elements, not the number of bytes.  So I needed to divide by the number of bytes per element (2 in this case).

After some testing, I expect to finally release some new firmware for Dexter in preparation for officially supporting the European version of Dragon's Lair (woohoo!).

Friday, May 24, 2019

How to Dockerize ld-decode (rev5)

If you are interested in the Domesday Duplicator, you may have come across the problem of installing the ld-decode software.  This is much easier using Docker because you can just use the Dockerfile that I already created.

Step 1:

Save the following text to a file called 'Dockerfile', in an empty subdirectory.

# known to work with 18.04, other versions may have incompatibilities
FROM ubuntu:18.04

ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update && \
    apt-get -y -qq install apt-utils sudo && \
        apt-get -y -qq install locales && locale-gen en_US.UTF-8 && \
    apt-get -y -qq install git \
        libopencv-dev \
        libfann-dev \
        python3-pip \
        python3-tk \
        clang \
        imagemagick wget

# more schlop to install
RUN apt-get -y -qq install libqt5charts5 libqt5charts5-dev qt5-default unzip

# create a normal user so we're not running as root
RUN export uid=1000 gid=1000 && \
    mkdir -p /home/developer && \
    echo "developer:x:${uid}:${gid}:Developer,,,:/home/developer:/bin/bash" >> /etc/passwd && \
    echo "developer:x:${uid}:" >> /etc/group && \
    echo "developer ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/developer && \
    chmod 0440 /etc/sudoers.d/developer && \
    chown ${uid}:${gid} -R /home/developer

# switch to user so it installs from the user's context
USER developer
ENV HOME /home/developer

# python3 schlop
RUN pip3 install numpy pandas scipy matplotlib

# set user's home directory as current directory because that's where we'll do work
WORKDIR /home/developer

# clone and build the rev5 commit
RUN wget
RUN unzip
RUN mv ld-decode-8e8b9f2c18136a1bcdbd3d659d72e42e23135771 ld-decode
RUN rm

RUN cd ld-decode && make && cd tools && qmake && make all && sudo make install

Step 2:

Build docker image using this command:

docker build -t lddecode_rev5 -f Dockerfile .

Step 3:

Run docker image using this command:

docker run --rm -v $HOME:/shared -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY -e "QT_X11_NO_MITSHM=1" -ti lddecode_rev5 /bin/bash

From inside the image, the /shared folder will point to your native host's home directory.

Follow instructions here for usage.  Also, check out the "ld-analyse" UI app, it is very slick.

The nice thing about using the docker container is that you don't have to worry about installing a bunch of ld-decode dependencies to your native host operating system.  To uninstall, you simply remove the docker image.  Also, you can run more than one container in parallel if desired.

Friday, March 1, 2019

My ironic story involving a Triangular Scale

So I was mentioning that I had mis-measured the dimensions on the Dragon's Lair ROM board (see previous blog post) and Shaun Wood suggested that I look into getting a Triangular Scale.

He linked to this one:

In case the link expires, it is showing a "Staedtler Mars 12" Architect Triangular Scale (987 19-31BK)".

The picture (if you click on it) clearly shows that an inch is divided into tenths which makes for convenient PCB measuring since PCB manufacturers often use tenths of inches as their scale.

Well, I tried to find this piece on Amazon for free shipping and couldn't.  So I just googled it and found that an Office Depot local to me (on my way home, in fact!) carried it.  So I thought "Great!"

I went to the Office Depot, walked inside and quickly turned down a random isle so that I wouldn't attract attention.  (For some reason, I kinda get nervous about an employee walking up to me and asking if I need help finding anything.)  So I went down a random isle, pulled out my phone, and pulled up the link that Shaun Wood had provided.  I took note that it was the model "98719-31BK" then said to myself "Ok, now to find where it is in this store!"

I looked up from my phone and literally right in front of me was what I was looking for.  Haha!  I've never had that happen before: going down a random isle and winding up exactly where I wanted to be.  That's the first irony of the story.

The second irony is that as I examined the "98719-31BK", I noticed that it was divided into 16th's , not 10th's.  "What the heck?" I muttered to myself, as I looked at the back.  On the back of the packaging, it said that the 98719-31BK had standard 'ruler' type divisions such as 1/8, 1/4, 3/16, 3/8, 1/2, etc.  And I also noted that the similarly branded 98719-34BK was the one I actually wanted, which was divided into 1/10, 1/20, 1/30, etc.

"Oh, great," I grumbled.  "I came all this way to find the 98719-31BK only to learn that it's not the one I even want!"

Then I looked up and right next to the 98719-31BK was hanging a 98719-34BK, the very item that I wanted!

I felt kind of bemused.  Shaun inadvertently links me to the wrong item but it has the right picture.  I go to the Office Depot to get the wrong item (unknowingly) only to find out that they have the right item right next to the wrong item.  And as a bonus, I randomly walk right up to it without even paying attention to where I am going.  It felt awesome and kind of bewildering all at the same time.

That's all my story.  Good day.