Saturday, 17 January 2026

A Commodore 64 Emulator in Flutter: Part 15

Foreword

In the previous post we introduced the CIA as a separate class. Previously mimicked the CIA's operation, by just forcing a hard interrupt every 1/60th of a second, just to get our emulator to work, avoiding the complexities of implementing and scheduling timers.

Thus, in the previous post we delved deeper and implement the CIA. This was actually needed as a precursor to this post where we will be implementing Tape loading functionality, which require more granular operation of the CIA.

Adding Front end Interaction

The logical place to start, is to add functionality to our front end for attaching a tape image. So within main.dart, which is basically our front end code, we add two buttons for the RunningState front end:

...
} else if (state is RunningState) {
              return KeyboardListener(
                focusNode: context.read<C64Bloc>().focusNode,
                autofocus: true,
                onKeyEvent: (event) => {
                  if (event is KeyDownEvent) {
                    context.read<C64Bloc>().add(KeyC64Event(keyDown: true, key: event.logicalKey))
                  } else if (event is KeyUpEvent) {
                    context.read<C64Bloc>().add(KeyC64Event(keyDown: false, key: event.logicalKey))
                  }
                },
                child: Column(
                  children: [
                    Row(
                      children: [
                        IconButton(
                            icon: Icon(Icons.folder),
                            onPressed: () async {
                              context.read<C64Bloc>().add(LoadTapeRequested());
                            }),
                        IconButton(
                            icon: Icon(Icons.play_arrow),
                            onPressed: !state.tapeLoaded ? null : () async {
                              context.read<C64Bloc>().add(PlayTapeRequested());
                            })
                      ],
                    ),
                    RawImage(
                      image: state.image, scale: 0.5),
                ],
              ));
            }
...
As usual, we will add LoadTapeRequested and PlayTapeRequested in c64_event.dart.

Now we need to listen for every event within our Bloc class:

    on<PlayTapeRequested>((event, emit) {
      _tape.playTape();
    });

    on<LoadTapeRequested>((event, emit) async {
      final result = await FilePicker.platform.pickFiles(
        withData: true,
        type: FileType.custom,
        allowedExtensions: ['tap', 't64'],
      );

      if (result == null) return;
      tapeLoaded = true;
      _tape.setTapeImage(result.files.single.bytes!);
    });

For PlayTapeRequested we simulate the press of a play button. _tape is an instance of a class Tape, which we will define later.

With the LoadTapeRequested event, we Basically present a file dialogue where the user select the Tape image from the local file system and also pass it to the _tape instance.

The Tape Class

Let us start to implement the Tape class, which will emulate the functionality of Tape loading.

We start with a simple class:
class Tape implements TapeMemoryInterface {
  late Iterator _tapeImage;
  bool _playSelected = false;
  Alarms alarms;
  TapeInterrupt interrupt;
  Alarm? _tapeAlarm;

  Tape({required this.alarms, required this.interrupt});
}
Before we go into detail on how to implement this class, let us take a step back and think about how Tape loading works on a C64.

On a physical tape, you used back in the day to load games on a C64, you had pulses of varying lengths. It all boils down to basically two types of pulses: A short pulse or a long pulse, which corresponds to either a 0 or 1, which is a bit. The most basic element of data on a computer 😀.

Now, when considering the loading of the data from a physical tape on C64. The end of a pulse is indicated when it changes polarity from positive to negative or vice versa. This change of polarity causes an interrupt on the CPU, via the FLAG pin on CIA1. The tape loading routines inside the Kernal ROM use one of the CIA timers to measure the pulse widths, and decide based on that, if each bit is a zero or a one.

The tape image files you can download from the Internet of old games, are a sequence of pulse widths. With all this info at hand, it is starting to become apparent on what the tape class should do. Using these pulse width, it should schedule an alarm, the same structures we used previously within the CIA, for each pulse, and trigger an interrupt when it lapses. Looking at the private fields I defined above in the Tape class, it also hints towards this.

Let us have at the variable _tapeImage. It is of type Iterator. With this data structure we can basically iterate through the tape image pulse width by pulse width, without worrying about working with a counter that you need to update every time.

At this point we are ready to implement the method _setTapeImage(), which we mentioned previously:

  setTapeImage(type_data.Uint8List tapeData) {
    _tapeImage = tapeData.iterator;
    for (var i = 0; i < 21; i++) {
      _tapeImage.moveNext();
    }
    populateRemainingPulses();
  }

Uint8List variables provides you with an Iterator. In a tape image actual pulse width data actually starts after 21 bytes.

Once we are at the actual pulse width data, we need to know the width of the first pulse. This is the function of the method populateRemainingPulses() :

  populateRemainingPulses() {
    var val = _tapeImage.current;
    if (val != 0) {
      _remainingPulseTicks = val << 3;
      _tapeImage.moveNext();
    } else {
      var byte0 = _tapeImage.current;
      _tapeImage.moveNext();
      var byte1 = _tapeImage.current;
      _tapeImage.moveNext();
      var byte2 = _tapeImage.current;
      _tapeImage.moveNext();
      _remainingPulseTicks = (byte2 << 16) | (byte1 << 8) | byte0;
    }
  }

Here we need to understand the TAP format a bit better. Usually every byte indicates one pulse width. We then need to multiply this value by 8 to get to the width in CPU clock cycles.

The excpetion to the rule is when the byte value is zero. Then the next three bytes indicate the pulse width as an absolute value of CPU clock cycles e.g. no multiplication by 8 necessary then.

You will see that I am assigning the calculated value to a private variable _remainingPulseTicks. We are following a similar approach here than with timers in the CIA which we implemented in the previous post. It functions almost as a count down timer, and is updated with the alarm subsystem.

At this point a key question is: What kicks off the tape loading process? The answer lies in memory location 1 of the C64 memory. This memory location is well known for the location of switching out banks of memory in and out of view. However, this memory location also host two bits for tape control:
  • Bit 4 - Cassette Switch Sense; 1 = Switch Closed
  • Bit 5 - Cassette Motor Control; 0 = On, 1 = Off
The key here is bit 5, turning the Cassette motor on and off, which acts as the starting point for the tape loading process. Bit 4 tells us when the user presses the play button, which we will cover later.

In this let us create the following method in our Tape class:

  @override
  setMotor(bool on) {
    if (on == _currentMotorOn) {
      return;
    }
    _currentMotorOn = on;
    if (on) {
      setupAlarms();
    } else {
      _tapeAlarm!.unlink();
      _remainingPulseTicks = _tapeAlarm?.getRemainingTicks();
    }
  }
This method will be invoke when we write to memory via our Memory class. We will deal with this plumbing later.

If the motor switched on, we need to setup alarms. This is similar what we did with timers in the previous post. Before we move onto the implementation of setupAlarms(), lets have a look at what happend in the else, when the motor is switched off. In that case we unlink the alarm from the list of alarms, and we set _remainingPusleTicks to the remaining ticks of the pulse. This is just to cater for when we resume the motor, we can carry on from where we left on in the pulse.

Now, let us look at setupAlarms():

  setupAlarms() {
    _tapeAlarm ??= alarms.addAlarm( (remaining) => processTapeAlarm(remaining));
    if (_tapeAlarm!.list == null) {
      alarms.reAddAlarm(_tapeAlarm!);
    }
    _tapeAlarm!.setTicks(_remainingPulseTicks);
  }

Here we see the actual use of _remainingPulseTicks, when the motor is resumed.

Let us now have a look at the method processTapeAlarm() :

  processTapeAlarm(int remaining) {
    interrupt.triggerInterrupt();
    populateRemainingPulses();
    _tapeAlarm!.setTicks(_remainingPulseTicks + remaining);
  }
This method is called when the pulse has expired. During this we trigger an interrupt and reschedule the next alarm.

Finally, there is one remaining method we need to implement:

  @override
  int getCassetteSense() {
    return _playSelected ? 0 : 0x10;
  }

This basically provides bit 4 of memory location 1, which will be used by our memory class. More on this later.

Changes to the CIA class

Let us now have a look at the changes required in our CIA class.

There is quite a few changes, so I will just cover it on a high level.

First of all, we will need to implement TimerB as well. The tape loading routine in Kernel ROM uses this timer quite extensively. All I will say here, is that it is basically a copy and paste excercise from TimerA.

Next, we will look at the method hasInterrupts(), which is used by our CPU class to trigger an interrupt:

  hasInterrupts() {
    if (timerAintOccurred && timerAinterruptEnabled) {
      return true;
    } else if (timerBintOccurred && timerBinterruptEnabled) {
      return true;
    } else if (tapeInterruptOccurred && tapeInterruptEnabled) {
      return true;
    } else {
      return false;
    }
  }

You will notice that I have included timerB interrupts and tape interrupts in the check as well.

Next, let us look at the setMem() function in the CIA class:

  setMem(int address, int value) {
...
      case 0xD:
        if ((value & 0x80) != 0) {
          timerAinterruptEnabled = ((value & 1) == 1) ? true : timerAinterruptEnabled;
        } else {
          timerAinterruptEnabled = ((value & 1) == 1) ? false : timerAinterruptEnabled;
        }
        if ((value & 0x80) != 0) {
          timerBinterruptEnabled = ((value & 2) == 2) ? true : timerBinterruptEnabled;
        } else {
          timerBinterruptEnabled = ((value & 2) == 2) ? false : timerBinterruptEnabled;
        }
        if ((value & 0x80) != 0) {
          tapeInterruptEnabled = ((value & 16) == 16) ? true : tapeInterruptEnabled;
        } else {
          tapeInterruptEnabled = ((value & 16) == 16) ? false : tapeInterruptEnabled;
        }
...
  }

As you might remember previously, register D in the CIA is the interrupt mask register. Here we have added timerB and the Tape Inteerupt as some interrupts we can enable or mask out.

Finally, let us look at the getMem() method:

  int getMem(int address) {
...
  case 0xD:
        var value = 0;
        if (timerAintOccurred) {
          timerAintOccurred = false;
          value = value | 0x81;
        }
        if (timerBintOccurred) {
          timerBintOccurred = false;
          value = value | 0x82;
        }
        if (tapeInterruptOccurred) {
          tapeInterruptOccurred = false;
          value = value | 0x84;
        }
        return value;
    }
...
}

Here we are reading the same register from earlier, but reading doesn't return the masks, but the actual interrupts that occurred. Once again, we have added timerB and TapeInterrupt. Once this registere has been read, we also clear all occurred interrupts.

Changes to the Memory Class

Let us now have a look at the changes required in our memory class, for implementing tape loading.

First change is in the setMem() method:

  setMem(int value, int address ) {
    if ((address >> 8) == 0xDC) {
      cia1.setMem(address, value);
    } else if (address == 1) {
      _ram.setInt8(address, value);
      _tape.setMotor((value & 0x20) == 0 );
    } else {
      _ram.setInt8(address, value);
    }
  }
So, as mentioned earlier, bit 5 of memory location 1 controls the tape motor. Here we implement it, so that during a memory write to this location, we call setMotor appropriately.

Next, let us change the getMem() method:

  int getMem(int address) {
    _readCount++;
    if (address >= 0xA000 && address <= 0xBFFF) {
      return _basic.getUint8(address & 0x1fff);
    } else if (address >= 0xE000 && address <= 0xFFFF) {
      return _kernal.getUint8(address & 0x1fff);
    } else if (address == 0xD012) {
      return (_readCount & 1024) == 0 ? 1 : 0;
    } else if ((address >> 8) == 0xDC ) {
      return cia1.getMem(address);
    } else if (address == 1) {
      var value = _ram.getUint8(address) & 0xef;
      return value | _tape.getCassetteSense();
    } else {
      return _ram.getUint8(address);
    }
  }

Here we add the Cassette sense bit when reading the byte from memory location one. As mentioned previously, the cassette sense bit indicates if we pressed the play button.

The results

With everything coded, let us see how the screens looks like when we spin up our emulator. At startup, our screen looks like this:


Notice we have two new icons at the top, a folder icon and a play button. We use the folder icon to locate the tape image file from our local file system. Once we have selected a tape image, the play button becomes enabled.

The play button if actually ressembling the play button on a real C64 Datasette unit which was hooked to a C64. So, when the screen shows "Press Play on tape", and you hit the play button, the loading process commenced.

Lets do the whole sequence. With the tape image attached, type LOAD at the flashing cursor, and then hit ENTER. Your screen will now look like this:

Now press play button next to the folder button.

With the play button pressed, the folloing prompts will popup:


After a number of seconds, the screen will look like this:


This is the Hooray moment. When seeing FOUND DAN DARE, or what the file name of the tape image you used, you know you have implemented the tape loading correctly.

One thing that immediately felt off when testing the tape loading, was that it felt much longer than usual before it showed "FOUND...". So I did some comparative benchmarks.

First, to get a realistic time, I measured how long it takes to find the file in the Vice C64 emulator. It was about 17 seconds.

Then I did the measurement in my Flutter emulator. In my Emulator, it took 24 seconds. Quite a lot slower!

I did some further depth investigations. After lots of pain, I discovered that the speed issue was caused by not building the app in release mode. I made a subtle assumption that if I start it IntelliJ, and I start it with the Play button and not with the Debug button, every thing will be optimised. Was I wrong!

Let us see how to run our project in release mode. Firstly, open a terminal window and cd into your project folder. Then run the following command:

flutter build web --release
After the build is finished, you will find the result in build/web with the project. cd into this folder. We now need a web server to serve this, and the easiest one to use is Python. So, within the build folder, run the following command:

python -m http.server 8000
Now access the emulator in the browser with http://localhost:8000/

This time around our times match up with tape loading.

While I was trying to figure out why my emulator was slow, I also discover memory usage was steadily climbing. When I fixed the issue in release, I wondered if the memory leak issue was also fixed. So, I left it running for about half an hour, and then hovered over the tab in Chrome to see the memory usage, and sadly the memory leak was there:


If you leave it running longer, it will eventually go over 1G of memory usage.

In the next post we will tackle this issue.

In Summary

In this post we implemented Tape image loading. An unfortunate issue I encountered was a memory leak.

In the next post I will see if I can fix this memory leak.

Until next time!