Monday, 28 April 2025

A Commodore 64 Emulator in Flutter: Part 12

Foreword

In the previous post we successfully ran the Klaus Dormann Test Suite.

In this post we will be trying to boot the C64 system with its ROM's.

Enjoy!

Inserting the ROMS

Inserting the ROM's... Now that sounds like plugging and unplugging game cartridges 😂. In our case, this means loading the C64 ROM images from files into memory, and making sure our emulated CPU can access the contents.

We start by dumping the ROM images into the asset folder:


Usually for the C64 ROMS you get for download on the internet, the file names have always some version numbers in it. In my case, I just gave them simple names. Also, notice that I have removed the file program.bin we used in previous posts.

  C64Bloc() : super(InitialState()) {
    on<InitEmulatorEvent>((event, emit) async {
      final basicData = await rootBundle.load("assets/basic.bin");
      final characterData = await rootBundle.load("assets/characters.bin");
      final kernalData = await rootBundle.load("assets/kernal.bin");
      memory.populateMem(basicData, characterData, kernalData);
...
So, we load the different ROM's, waiting for the loading of each file to complete, and then going to the next file for loading.

Now, you might notice that from previous posts, that we now pass more ROMS to memory.populateMem. So let us delve a bit deeper in our Memory class to see what changes are required:

...
  late type_data.ByteData _basic;
  late type_data.ByteData _character;
  late type_data.ByteData _kernal;
...
  final type_data.ByteData _ram = type_data.ByteData(64*1024);
...
  populateMem(type_data.ByteData basicData, type_data.ByteData characterData,
      type_data.ByteData kernalData) {
    _basic = basicData;
    _character = characterData;
    _kernal = kernalData;
  }
...
Fairly straightforward. Each ROM that is passed through, we store in a variable.

Something else we do, is do define a 64KB array that will act as our RAM, the significant characteristic of the C64.

So, next, let us add some address mapping:

  setMem(int value, int address ) {
    _ram.setInt8(address, value);
  }

  int getMem(int address) {
    if (address >= 0xA000 && address <= 0xBFFF) {
      return _basic.getUint8(address & 0x1fff);
    } else if (address >= 0xE000 && address <= 0xFFFF) {
      return _kernal.getUint8(address & 0x1fff);
    } else {
      return _ram.getUint8(address);
    }
  }

For memory writes, we write straight to the ram array. For reads, we do it the usual C64 setup:

  • Addresses A000-BFFF: We read from basic ROM
  • Addresses E000-EFFF: We read from Kernal ROM
  • All other addresses we read from RAM

Booting the C64 System

We are now close to booting the C64 system with all its ROM's.

First things first. Our periodic timer current runs once every second, executing 1 000 000 millions cycles worth of CPU instructions. However, we want to reduce to a 60th of a second, so that later on we can draw a frame every time our time executes, yielding 60 frames a second, which is the frame rate of a native C64:

    on<RunEvent>((event, emit) {
      timer = Timer.periodic(const Duration(milliseconds: 17), (timer) {
          int targetCycles = _cpu.getCycles() + 16666;
          do {
            _cpu.step();
          } while (_cpu.getCycles() < targetCycles);
      });
    });

Every time we also execute 16666 cycles, which is the number of CPU cycles in a 1/60th of a second.

To boot the C64 ROM's, is actually fairly straightforward. You basically set the program counter to the value of the reset vector. For his we just we just create the following method:

  reset() {
    pc = memory.getMem(0xfffc) | (memory.getMem(0xfffd) <<< 8);
  }
So, here we populate the program counter with the reset vector at adress FFFC and FFFD.

We still need to call this method. We do this just after we have loaded all the ROM's:

  C64Bloc() : super(InitialState()) {
    on<InitEmulatorEvent>((event, emit) async {
      final basicData = await rootBundle.load("assets/basic.bin");
      final characterData = await rootBundle.load("assets/characters.bin");
      final kernalData = await rootBundle.load("assets/kernal.bin");
      memory.populateMem(basicData, characterData, kernalData);
      _cpu.reset();
...
Now, we can finally boot the C64 System. We wait for a minute, and then hit stop to view the registers:


We see the program stabilise at address FF61. Let us have a look at the Kernal disassembly listing what is going on at this address:
 
As seen here, we get stuck in a loop with the memory address D012 not changing. We can expect that such a thing can happen at the moment, because with our current emulator setup that address will write and read to raw RAM, and thus nothing will happen.

In reality D012 maps to the VIC-II display registers and provide info on which rasterline on the screen we are currently at. It is quite an undertaking to implement such a raster counter, so for now, let us see if we can quickly hack together something, so that the Address D012, can just change sometimes, just to get past that loop. Here is my quick hack:

  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 {
      return _ram.getUint8(address);
    }
  }

So, the hack is simply just a counter that keeps count of the number of reads, and we look at bit 9 of the counter. If it is set, we return a 1, otherwise a zero. In effect we will have a 1 for about a thousand counts, and then a zero for another thousand counts.

Let us now see where our program counter lands. This time it lands at E5D4. Lets look again at the disassembly listing for this address:


Here it seems we are in a waiting loop, waiting for the enter key to be pressed on the keyboard. I think this is a pretty decent place for our emulator to be and probably means that all initialisation has been completed, and we should have the welcome message in screen memory.

We want to check if the welcome message is in screen memory, but our debug dump current just show the first two pages of memory. We could, however, inspect the ram array in debug mode in Intellij.

So, startup the emulator in debug mode and press the play button to let the C64 system run at full speed. Wait for about a minute and then put a breakpoint on the first line of the getMem method in our Memory class. With the system running at full speed, that breakpoint will be hit almost instantaneously.

Open up an evaluate window and enter the following:


Here we inspect address 1024 of screen memory, which is the first byte of it. In this case the value is 32, which is a space. Inspecting addresses further in screen memory will reveal the welcome message.

In the next section we will render the contents of the screen at real time.

Rendering screen memory

We will now try and render screen memory in real time, showing a display similar to the C64 in text mode.

We ultimately need a mechanism that would allow us to work efficiently with image data on a pixel level. Flutter ultimately provide it to us via the RawImage widget, together with ui.Image with which you can work with an array of RGBA values.

Let us unpack this a bit. Let us start working with the raw array of RGBA values, where we will produce the frame for display, based on the screen memory and the character ROM.

Both the character rom and screen memory is present in our Memory class, so for now we will do the frame rendering in that class.

Firstly, let us define the byte buffer we are going to use over and over again:

class Memory {
...
    final type_data.ByteData image = type_data.ByteData(320*200*4);
...
}
So, as we can see, we have a resolution 3200x200, which is the resolution of a real C64 screen. We multiply the end result by 4, because each pixel is bytes in our buffer, one byte each for red, blue green and the alpha channel.

Next, let us write method for rendering a screen to the byte array:

  type_data.ByteData getDisplayImage() {
    const rowSpan = 320 * 4;
    for (int i = 0; i < 1000; i++ ) {
      var charCode = _ram.getUint8(i + 1024);
      var charAddress = charCode << 3;
      var charBitmapRow = (i ~/ 40) << 3;
      var charBitmapCol = (i % 40) << 3;
      int rawPixelPos = charBitmapRow * rowSpan + charBitmapCol * 4;
      for (int row = /*charAddress*/ 0 ; row < /*charAddress +*/ 8; row++ ) {
        int bitmapRow = _character.getUint8(row + charAddress);
        int currentRowAddress = rawPixelPos + row * rowSpan;
        for (int pixel = 0; pixel < 8; pixel++) {
          if ((bitmapRow & 0x80) != 0) {
              image.setUint32(currentRowAddress + (pixel << 2), 0x000000ff);
          } else {
              image.setUint32(currentRowAddress + (pixel << 2), 0xffffffff);
          }
          bitmapRow = bitmapRow << 1;
        }
      }

    }
    return image;
  }

So, here we loop through all thousand characters codes in screen memory and rendering everyone. Each character code is actually an index into character ROM, every character is its own 8x8 pixel bitmap.

Now, this method is invoke everytime when our perioc timer runs:

...
import 'dart:ui' as ui;
...
   on<RunEvent>((event, emit) {
      timer = Timer.periodic(const Duration(milliseconds: 17), (timer) {
          int start = DateTime.now().millisecondsSinceEpoch;
          int targetCycles = _cpu.getCycles() + 16666;
          do {
            _cpu.step();
          } while (_cpu.getCycles() < targetCycles);
          ui.decodeImageFromPixels(memory.getDisplayImage().buffer.asUint8List(), 
             320, 200, ui.PixelFormat.bgra8888, setImg);
      });
    });
ui.decodeImageFromPixels is a menthof within the dart:ui library of flutter. It will create an Image object from a pixel buffer, which in this case is the rendered screen buffer.

We also pass ui.PixelFormat.bgra8888 as a parameter, indicating our buffer is in the format with byte each for red, green, blue, green and alpha.

We also pass a callback method, setImg in this case, which will be called once we have the generated Image object.

So, let us implement this callback method:

    void setImg(ui.Image data) {
      emit(RunningState(image: data, frameNo: frameNo++));
    }
Here you can see we are emitting the image in a state object, so our BlocBuilder can pick up the change and render the image. You will also notice that we have a frameNo Property that we modify with each new image, so our BlockBuilder can easily pick up the change.

You will recall that from previous posts, that we did define RunningState previously, which we applied changes to now. Here is the revised version:

class RunningState extends C64State {
  RunningState({required this.image,
    required this.frameNo});

  final int frameNo;
  final ui.Image image;
  @override
  List<Object> get props => [frameNo];
}
Finally, let us modify our BlocBuilder:

...
        body: BlocBuilder<C64Bloc, C64State>(
          builder: (BuildContext context, state) {
            if (state is InitialState) {
              return const CircularProgressIndicator();
            } else if (state is DataShowState) {
              return Column(
                children: [
                  Text(getRegisterDump(state.a, state.x, state.y, state.n,
                      state.z, state.c, state.i, state.d, state.v, state.pc)),
                  Text(
                    getMemDump(state.memorySnippet),
                    style: const TextStyle(
                      fontFamily: 'RobotoMono', // Use the monospace font
                    ),
                  ),
                ],
              );
            } else if (state is RunningState) {
              return RawImage(
                  image: state.image, scale: 0.5);
            } else {
              return const CircularProgressIndicator();
            }
          },
        ),
...
So, if the state is RunningState, we return a RawImage widget, which will be displayed on the screen. We pass the image in the state to the RawImage widget. We also use a scale of 0.5, with which we basically doubles the displayed size. The native resolution of 320x200 of a C64 frame display very small on a modern display, so at least with the scale, it can appear bigger.

With everything coded we can now give it a test run. The startup sequence appear to take more or less the same time as a real C64, and eventually the welcome screen appear:

We are making progress, but still, there is no flashing cursor.

Getting the cursor to flash

Let us see if we can get the cursor to flash. 

If you go down the bowls of the C64 system, you will found that the core of a standard C64 system that just started up, is that there is a timer interrupt every 60th of a second. This interrupt does a couple of things, like checking if any key was pressed or released and updating the status of the cursor.

So, let us see if we we can put a hack together, that off the bat we just force an interrupt every 60th of a second, without worrying for now to implement emulation of the full CIA chip with a timer.

The easiest way is in the step() method of our CPU class:

  step() {
    if ((_cycles > 1000000) &&((_cycles % 16666) < 30) && (_i == 0)) {
      push(pc >> 8);
      push(pc & 0xff);
      push((_n << 7) | (_v << 6) | (2 << 4) | (_d << 3) | (_i << 2) | (_z << 1) | _c);
      _i = 1;
      pc = memory.getMem(0xfffe) | (memory.getMem(0xffff) << 8);
    }
...
  }
So, we wait for a second before triggering interrupts in 1/60 second intervals. With the change, the cursor actually flashes:

In Summary

In this post we managed to boot the C64 system with all its ROMs and managed to render screen memory in real time, showing the welcome message and the flashing cursor. 

The source code for post is available in the following Github tag: https://github.com/ovalcode/c64_flutter/tree/c64_flutter_part12

In the next post we will add some keyboard interaction with our emulator.

Until next time!


Wednesday, 9 April 2025

A Commodore 64 Emulator in Flutter: Part 11

Foreword

In the previous post we ran the Klaus Dormann Test Suite on our emulator. In this process we found a couple of issues with our emulator. We fixed a couple of issues, but found a couple of more issues we still need to fixed.

In this post we will look at the remaining issues. Solving these remaining issues wasn't so much of a deal at all, so this post will be shorter normal.

The remaining fixes

One of the major issues I found while running the Klaus Dormann Test Suite on my emulator, was some incorrect values for some of the CPU data tables. This include some instructions having the incorrect address mode and incorrect instruction lengths.

The other issue I experienced, was failed test cases because decimal mode wasn't implemented. Implementing Decimal mode is fairly straightforward. We start with implementing the following methods:

  int adcDecimal(int operand) {
     int l = 0;
     int h = 0;
     int result = 0;
     l = (_a & 0x0f) + (operand & 0x0f) + _c;
     if ((l & 0xff) > 9) l += 6;
     h = (_a >> 4) + (operand >> 4) + (l > 15 ? 1 : 0);
     if ((h & 0xff) > 9) h += 6;
     result = (l & 0x0f) | (h << 4);
     result &= 0xff;
     _c = (h > 15) ? 1 : 0;
     _z = (result == 0) ? 1 : 0;
     _n = 0;
     _v = 0;
     return result;
   }
 
   int sbcDecimal(int operand) {
     int l = 0;
     int h = 0;
     int result = 0;
     l = (_a & 0x0f) - (operand & 0x0f) - (1 - _c);
     if ((l & 0x10) != 0) l -= 6;
     h = (_a >> 4) - (operand >> 4) - ((l & 0x10) != 0 ? 1 : 0);
     if ((h & 0x10) != 0) h -= 6;
     result = (l & 0x0f) | (h << 4);
     _c = ((h & 0xff) < 15) ? 1 : 0;
     _z = (result == 0) ? 1 : 0;
     _n = 0;
     _v = 0;
     return (result & 0xff);
   }

We modify the applicable instruction selectors:

       case 0x69:
         adc(arg0);
         if (_d == 1) {
           _a = adcDecimal(arg0);
         } else {
           adc(arg0);
         }
       case 0x65:
       case 0x75:
       case 0x6D:
       case 0x7D:
       case 0x79:
       case 0x61:
       case 0x71:
         adc(memory.getMem(resolvedAddress));
         if (_d == 1) {
           _a = adcDecimal(memory.getMem(resolvedAddress));
         } else {
           adc(memory.getMem(resolvedAddress));
         }
         
      case 0xE9:
         sbc(arg0);
         if (_d == 1) {
           _a = sbcDecimal(arg0);
         } else {
           sbc(arg0);
         }
       case 0xE5:
       case 0xF5:
       case 0xED:
       case 0xFD:
       case 0xF9:
       case 0xE1:
       case 0xF1:
         sbc(memory.getMem(resolvedAddress));
         if (_d == 1) {
           _a = sbcDecimal(memory.getMem(resolvedAddress));
         } else {
           sbc(memory.getMem(resolvedAddress));
         }

Test Results

With everything fixed, we can see if all the tests passed.

The Test Suite runs for about two minutes on my emulator. After the two minutes, when hitting stop, the register window will look as follows:


From this point the program counter remains at 3469. Lets have a look at the assembly listing to see what is at this address:

So, this is confirmation that our emulator passed all the tests!

In Summary

In this post we confirm that we implemented all the CPU instructions correctly in our emulator, using Klaus Dormann's Test Suite.

Here is a link to the tag of this post's source code: https://github.com/ovalcode/c64_flutter/tree/c64_flutter_part11

In the next post we will start writing some more code to boot the C64 ROM's.

Until next time!